npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nominee

v3.0.0

Published

The pause is where tools break: token minted at execution, bound to the args a human saw, spendable once, sealed in a hash-chained receipt.

Readme


Installation

npm i nominee

No signup. No SaaS account. No vendor lock-in. Zero runtime dependencies.


What it does

Your framework decides which tool the agent requests. nominee checks your rules before the tool function runs.

nominee sits between the model and your tools, in-process, and gives every tool call:

  1. Policy — declarative allow / deny / ask rules: glob patterns, argument-level conditions, call budgets. The model cannot talk its way past a deny.
  2. Approvalsask pauses the call until a human decides, with the full arguments in front of them.
  3. Receipts — every decision (including refusals) sealed into a hash-chained (HMAC), tamper-evident log — tamper-evident against a downstream log editor, not non-repudiation against the agent host. Inputs hashed, never stored.

Don't have rules yet? Start by looking

Observe mode wraps your existing tools with no policy required and records deny, ask, and budget decisions without enforcing them. It records which tool callbacks actually start, into the same hash-chained receipts, and reports argument shapes, numeric ranges, and hashed cardinalities without enumerating string values. The JSON report also inventories callable tools supplied to observe(), including tools that never ran, so a policy generator can distinguish unused authority from unknown authority. Runtime and integrity failures still fail closed.

import { Nominee, formatObservations } from 'nominee'

const nominee = new Nominee({ mode: 'observe' })
const tools = nominee.observe({ readOrder, issueRefund, exportCustomers })

// …run your agent as usual, then:
console.log(formatObservations(nominee.observations()))
nominee observe — 9 call(s) across 3 tool(s)
ENFORCEMENT WAS OFF: every observed call reached its tool callback.

  tool              calls  kind
  refund.issue          5  mutate
                      ↳ amount: number, observed 5–2000 (median 40)  [unbounded]
  orders.read           3  read
  customers.export      1  unknown

It is a discovery tool, not a security control, and it says so: startup prints an unmissable notice that enforcement is off, every receipt carries enforcement: 'observe', and production: true refuses to construct with it. See docs/observe.md.

Save that report and generate an editable starter policy with npx nominee-cli generate observations.json. Generated thresholds are observations, not security recommendations; review every rule before enforcing it.


Quickstart

import { Nominee, allow, deny, ask, lte } from 'nominee'

const nominee = new Nominee({
  policy: {
    rules: [
      allow('orders.read'),
      allow('refund.issue', { when: lte('amount', 50) }),
      ask('refund.issue', { when: ({ input }) => input.amount <= 500 }),
      deny('refund.issue'),
      deny('customers.export'),
    ],
    fallback: 'deny',
  },
  onApprovalRequest: (req) => notifySlack(req), // req.approve() / req.deny()
})

// One line. Works with plain functions or any framework's { execute } tools.
const tools = nominee.guard(
  {
    'orders.read': readOrder,
    'refund.issue': issueRefund,
    'customers.export': exportCustomers,
  },
  { user: 'alice' },
)

Denied calls throw PolicyDeniedError before the tool runs. An ask can resolve inline or surface ActionPendingError with a durable action id for later resume. Every outcome lands on the receipt chain.

Run the production refund example with Vercel AI SDK tools, durable approvals, and PostgreSQL stores: examples/support-refund-agent.

The supporting prompt-injection proof shows the same enforcement boundary against untrusted model input: examples/prompt-injection-blocked.

When the approval outlives the request

If the human cannot decide inside the current request, the call does not hang — it throws ActionPendingError immediately, and the pending action lives on in your action store. Resume it later, possibly from a different process:

try {
  await tools['refund.issue']({ orderId: 'ord_1', amount: 200 })
} catch (e) {
  if (e instanceof ActionPendingError) {
    // Persist BOTH: the action id and the exact input you called with.
    await db.pending.save({ actionId: e.actionId, input: { orderId: 'ord_1', amount: 200 } })
  }
}

// …minutes later, possibly in a different process:
await nominee.resolveActionApproval(actionId, { decision: 'approved', approver: '[email protected]' })
const resumed = await nominee.resumeAction(actionId) // { status: 'ready', capability } — does NOT run the tool
await nominee.executeCapability(resumed.capability, originalInput, (ctx) => issueRefund(ctx.input))

The durable action record stores the input's hash, not the input — persist the original arguments yourself, because executeCapability re-hashes what you supply and throws AuthorizationInputChangedError if it differs from what the approver reviewed. Full walkthrough: Approvals that outlive the request.


Policy semantics

  • First match wins within a policy; rules are checked in order.
  • No match → fallback (default 'ask'; set 'deny' for default-deny).
  • when predicates see { tool, input, user, tenant, resource, chain }.
  • Budgets: allow('search.*', { maxCalls: 20 }) — a lifetime call count (no time window, never resets) for that policy version/rule/tenant/user; the 21st call escalates to a human (ask), not a deny. max is a deprecated alias.
  • Delegation can only narrow: across delegate() chains the strictest outcome wins (deny > ask > allow).
const researcher = nominee.delegate('researcher', {
  policy: [deny('email.*'), deny('github.merge_*')],
})
// researcher's receipts carry chain: ['orchestrator', 'researcher']

// Dry-run without consuming budgets or asking anyone:
await nominee.check({ tool: 'repo.delete', user: 'alice' }) // → { effect: 'deny', … }

Receipts

const nominee = new Nominee({
  policy,
  receipts: {
    key: process.env.RECEIPT_KEY,          // optional HMAC seal
    delivery: 'strict',                    // default — fail closed if the async sink fails
    onReceipt: (r) => auditLog.write(r),   // may return a Promise
  },
})

nominee.receipts          // the chain so far (getter, not a method)
await nominee.verifyReceipts()  // { ok: true, checked: 128 } — async; with an atomic store it verifies the durable stream too
await nominee.flushReceipts()

// Later, offline, from your exported log:
import { verifyReceipts, formatReceipts, formatReceiptsCsv } from 'nominee'
formatReceipts(nominee.receipts)
formatReceipts(nominee.receipts, { verbose: true }) // includes rule + reason
formatReceiptsCsv(nominee.receipts) // spreadsheet projection; includes enforcement; still verify the JSON chain
verifyReceipts(exported, { key })  // { ok: false, brokenAt: 41, reason: '…' }

Each receipt's hash covers its content plus the previous hash — editing or deleting any record breaks verification of everything after it. New receipts include v: 1 (the receipt schema version) in that hashed content. verifyReceipts still accepts unversioned records sealed before this field existed, including mixed chains; an unknown v fails closed. This is not policyVersion, which versions the policy set. A mixed chain cannot hide tampering by stripping v: that changes the record's hash, so the next receipt's prev no longer matches. Inputs are recorded as inputHash by default: you can prove what an approver saw without writing user data into logs.

For a threat model that includes whole-database rollback, anchor the hash-chained stream tip in an external append-only system; a valid older chain is still a valid chain without that checkpoint.

A durable or hibernating agent that reconstructs its Nominee instance across restarts can persist receipts itself and pass receipts: { resume: { seq, prev } } — the sequence number and hash to continue from — so the new ledger picks up the same chain instead of starting a second genesis.


Human-in-the-Loop Approvals

// Legacy single-process API: blocks until the user responds.
await nominee.approve({ user: 'alice', action: 'repo.delete', detail: { repo: 'a/b' } })

// Settle from your webhook (Slack button, push notification, UI):
nominee.resolveApproval(approvalId, 'approved') // or 'denied'

For durable workflows, use prepareAction(), persist the pending action id, then call resolveActionApproval() / resumeAction() after the user decides. Strategies can carry native approval flows — nominee-auth0 does resumable CIBA approvals.


Tokens (the supporting act)

Tools that act on third-party APIs need credentials — fresh ones, at call time, never in the model's context:

import { Nominee, tokens } from 'nominee'

const nominee = new Nominee({
  policy,
  strategy: tokens(({ user, connection }) => db.getFreshToken(user, connection)),
})

await nominee.run(
  {
    tool: 'github.issue.close',
    input: { repo, issue },
    user: 'alice',
    resource: `repo:${repo}#${issue}`,
    connection: 'github',
    scopes: ['issues:write'],
  },
  ({ token }) => closeIssue({ repo, issue, token }),
)

| Strategy | Use case | |---|---| | tokens(fn) | Simple function — env vars, your DB, a literal string | | OAuth2({ connections }) | Generic refresh-token flow, zero deps. onRefreshToken persists rotation (GitHub Apps, Google, Okta, Auth0) | | Memory({ tokens }) | Dev & test in-memory store | | nominee-supabase | Provider tokens stored in Supabase (optional) | | nominee-auth0 | Auth0 Token Vault + CIBA push approvals (optional) | | nominee-postgres | Durable actions, budgets, capabilities, outcomes, and receipt streams |

Proof that naive refresh breaks under rotation + concurrency (7/8 fail; nominee 8/8): examples/token-refresh-correctness.


Full API

// Decision-bound execution (recommended; required in production mode)
await nominee.run({ tool, input, user, resource, tenant, connection, scopes }, execute)
const prepared = await nominee.prepareAction({ tool, input, user })
await nominee.resolveActionApproval(actionId, { decision: 'approved', approver, via })
const resumed = await nominee.resumeAction(actionId)
await nominee.executeCapability(resumed.capability, input, execute) // execute receives { action, input, token? }

// Authorization
await nominee.authorize({ tool, input, user })   // allow | throws PolicyDeniedError / ApprovalDeniedError
await nominee.assertUnchanged(authorization, input) // bind a manual authorize to execution
await nominee.check({ tool, input, user })       // dry-run: the decision, no side effects
nominee.guard(tools, { user })                   // wrap once, enforce everywhere

// Approvals
await nominee.approve({ user, action, detail })
nominee.resolveApproval(id, 'approved' | 'denied')

// Receipts
// `nominee.receipts` is a getter — read the property, don't call it
nominee.receipts
nominee.verifyReceipts()
formatReceipts(nominee.receipts)
formatReceiptsCsv(nominee.receipts)
await nominee.flushReceipts()
await nominee.verifyDurableReceipts() // verify durable stream + checkpoint
verifyReceipts(receipts, { key })

// Observability
const observed = nominee.observe(rawTools)        // report-only: never enforces
nominee.observations()                            // JSON: tool inventory, cardinality + ranges
// `onGovernedAction` is a constructor option, not a method:
//   new Nominee({ onGovernedAction: (event) => metrics.record(event) })
// or: usageReporter() for opt-in measurement — see docs/measurement.md

// Delegation (policies can only narrow; shared receipt chain)
const sub = nominee.delegate('research-agent', { policy })

// Read durable action state
await nominee.getAction(actionId)

// Tokens
await nominee.token({ user, connection })
await nominee.exchange({ user, connection, actor, scopes }) // RFC 8693
nominee.invalidate(user, connection)

// Fine-grained authz via strategy (e.g. Auth0 FGA)
await nominee.can({ user, action, resource })

// Audit stream (in-process listeners, alongside receipts)
const unsub = nominee.on((event) => log(event))

Errors: PolicyDeniedError, ApprovalDeniedError, ActionPendingError, AuthorizationInputChangedError, CapabilityInvalidError, ExternalAuthorizationDeniedError, ActionOutcomePersistenceError, ActionNotFoundError, ActionStateError.


Adapters

| Where your agent runs | Integration | |---|---| | Vercel AI SDK | nominee-aiguardTools() / nomineeTool() | | Vercel Eve | nominee-evenomineeTool() | | Cloudflare Agents | via nominee-ai | | OpenAI Agents SDK | nominee-openai — native resumable approvals | | Mastra | nominee-mastra — native or portable approvals | | MCP servers | nominee-mcpregisterNomineeTool() | | LangChain JS | nominee-langchainnomineeTool() |


For high-impact paths, use production: true with nominee-postgres. Production mode fails construction without a default-deny policy, durable action state, atomic durable receipts, strict delivery, and durable provider approval state. Review the repository's security guidance and production runbook before launch.

Contributing

PRs for community strategies and framework adapters are enthusiastically welcome — see CONTRIBUTING.md.

MIT License · github.com/bharath31/nominee