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

@lit-protocol/flows-policy

v0.3.1

Published

The Flows policy + approval engine — pure, deterministic one-gate evaluator. Two hosts: the Flows backend (pre-flight) and the CID-pinned Lit Action (authoritative seat).

Readme

@lit-protocol/flows-policy

The Flows policy + approval engine: a pure, deterministic "one-gate" evaluator plus the intent-hashing, JWS-approval, step-up, and EVM-UserOp primitives the gate is built from. No I/O, no clock, no DB — every input arrives as a fact, so the same bytes produce the same verdict everywhere.

Why this package exists (the two-host invariant)

A Flows fund-movement decision must be made in two places and they must agree:

  1. The Flows backend runs evaluate() as an advisory pre-flight ("would a live run be allowed?") so the UI can show a verdict before anything signs.
  2. The CID-pinned Lit Action runs the authoritative seat inside the TEE — it is the only host whose verdict can release a signature.

If those two ran different code, the preview would lie. So this package is the single source of truth: the backend imports it directly, and the action embeds the compiled bytes of a narrow entry (src/action-userop.ts) as an inlined IIFE (the TEE has no module resolution at execution). A CI sync test fails if the committed action bundle drifts from a fresh build. Same source → same compiled logic → identical verdicts.

This is also why you can build your own gated Action on top of these helpers and get the exact policy semantics the platform enforces — see Build your own gated Action below.

Install

npm install @lit-protocol/flows-policy

Inside this monorepo the package is consumed via a file: link ("@lit-protocol/flows-policy": "file:packages/policy" in the root package.json), so changes are picked up without re-publishing. Run npm run build in packages/policy/ (or npm run build:packages at the root) to refresh dist/.

Quickstart — the one gate

evaluate() takes an intent (what wants to move), the org policy (the law), the day's spend state, and the action's capability manifest, and returns a Verdict: allow | allow_with_notify | hold | deny, each with a human-readable trace.

import { evaluate, type Intent, type PolicyDoc, type SpendState, type CapabilityManifest } from '@lit-protocol/flows-policy'

const NOW = '2026-06-22T12:00:00.000Z'

const policy: PolicyDoc = {
  schema: 'flows-policy-v1',
  orgId: 'org_1',
  version: 1,
  caps: { maxNotionalPerOrderUsd: 2_000, maxNotionalPerDayUsd: 8_000 },
  circuits: { withdrawalsAlwaysHold: true, holdAssurance: 'L2', holdTtlSec: 86_400, policyLoosening: 'hold' },
}

const intent: Intent = {
  schema: 'flows-intent-v1',
  intentId: 'int_1',
  orgId: 'org_1',
  initiator: { id: 'dev_1', class: 'human', role: 'OPERATOR' },
  verb: 'trade-spot',
  params: { symbol: 'BTC/USDT', side: 'buy', qty: 0.01 },
  notionalUsd: 1_000,
  symbol: 'BTC/USDT',
  reason: 'rebalance',
  mode: 'live',
  createdAt: NOW,
}

const spend: SpendState = { schema: 'flows-spend-state-v1', date: '2026-06-22', orgDayUsd: 0 }
const manifest: CapabilityManifest = { schema: 'flows-capability-manifest-v1', verbs: ['trade-spot'] }

const verdict = evaluate({ intent, policy, policyHash: 'sha256-of-jcs(policy)', spend, manifest, now: NOW })
console.log(verdict.kind)   // 'allow'  (a $1,000 trade under a $2,000/order, $8,000/day cap)

Push the same intent to notionalUsd: 5_000 and the verdict becomes hold (over the per-order cap → needs a second person); switch initiator.class to 'agent' with no agent allowance and it denys as initiator.agent.unleashed. See examples/policy-gated-action/ for a runnable version of all three.

Verifying the policy hash

The gate trusts the policy doc you hand it; the host is responsible for proving that doc is the registered one. Compute the canonical hash with the same helper the registry uses:

import { sha256Jcs } from '@lit-protocol/flows-policy'
const policyHash = sha256Jcs(policy)   // sha256 over RFC 8785 (JCS) canonical JSON

Public surface

| Export | What it does | | --- | --- | | evaluate(input) | The one-gate decision. Pure; returns a Verdict. | | classifyPolicyEdit(from, to) | 'tighten' vs 'loosen' — drives instant-apply vs hold/timelock. | | verifyApproval(opts) | Email-approval-v2 JWS gate (assurance L1–L3). | | sha256Jcs(v), intentHashFor(intent), canonicalize(v) | RFC 8785 canonicalization + the portable intent/policy hashing both hosts bind with. | | signJws, verifyJws, addressFromPrivateKey | Detached-payload JWS used for approvals/attestations. | | verifyStepUp, signStepUpEoa, stepUpDigest | Step-up (re-auth) proof verification. | | executionIdempotencyKey(...) | Deterministic dedupe key for a single execution. | | gateYieldUserOp(...), validateEvmYieldCalls(...) | The EVM smart-account (ZeroDev Kernel) UserOp signing gate — the bytes the sign-userop action embeds. | | yieldUserIntentHash, yieldUserIntentChallenge, verifyYieldUserIntentProof | Bind a yield UserOp to a signed user intent. | | computeUserOpHashV07, decodeKernelExecute, keccak256, functionSelector, decodeExecutionBatch, decodeExecutionSingle | EVM/UserOp primitives the gate is built from. | | AAVE_V3_POOLS, ENTRY_POINT_V07 | Address constants. |

All types are exported alongside the functions (Intent, PolicyDoc, Verdict, SpendState, CapabilityManifest, GranteeAllowance, EvaluateInput, …).

Per-grantee leashes (standing authorizations)

Beyond the org-wide caps and the per-class initiatorClassAllowances, the gate enforces a named-grantee leash when the host passes one. The host looks up an ACTIVE, non-expired StandingAuthorization for the authenticated initiator (member / agent / API key) and passes its bounds in as granteeAllowance, with requireGranteeAllowance: true to fail closed if no grant matched:

const verdict = evaluate({
  intent, policy, policyHash, spend, manifest, now: NOW,
  requireGranteeAllowance: true,                       // this initiator MUST act under a grant
  granteeAllowance: {                                  // the matched grant's leash
    caps: { maxNotionalPerOrderUsd: 500, maxNotionalPerDayUsd: 500 },
    verbAllowlist: ['trade-spot'],
    venueAllowlist: ['binance'],
    spentTodayUsd: 0,
  },
})

The leash is enforced as a most-restrictive bound (∩ org ∩ install) — it can only narrow, never widen. A defined-but-empty allowlist ([]) is deny-all on that axis (this is how two disjoint grants intersect to deny-all rather than allow-all).

Build your own gated Action

The TEE runs a single source blob and CIDs it — there is no import at execution time. To reuse this engine inside your own Lit Action you inline the compiled gate as an IIFE, exactly like lit-actions/wallet/sign-userop.js does:

  1. Add (or reuse) a narrow entry under packages/policy/src/ that re-exports only the gate functions your action needs — see action-userop.ts. Keeping it narrow lets esbuild tree-shake the rest (no @noble/curves, no jwt/approval) so the action stays small.
  2. Bundle it to an IIFE that exposes a FlowsPolicy global and inline it into a thin shim at a // __FLOWS_POLICY_IIFE__ marker. The repo's scripts/build-wallet-actions.mjs does this; packages/policy/scripts/bundle.mjs emits a generic dist/flows-policy.iife.js (+ ESM) with an SRI hash for the CDN-import path.
  3. In your action, call the global: const verdict = FlowsPolicy.evaluate({ ... }) and only sign when verdict.kind === 'allow'.

Because the action embeds the same bytes the backend imports, your pre-flight preview and your in-TEE seat can never disagree.

Scripts

npm run build       # tsc → dist/ (the importable package)
npm run bundle      # esbuild → dist/flows-policy.iife.js + .mjs (+ SRI hashes)
npm test            # vitest (evaluate, approval, stepup, userOp, idempotency, canonicalize, seat)
npm run typecheck   # tsc --noEmit

Design notes

  • Pure & deterministic. No network, no Date.now() in the decision path — now is an input. This is what lets the verdict be reproduced and attested.
  • Fail closed. Unpriceable notional, a missing required grant, an unverifiable axis, or an unavailable screening all resolve to deny/hold, never a silent allow.
  • Intersection semantics. The manifest, org policy, install bounds, and grantee leash all narrow; nothing a host passes can widen what the policy permits.

License: MIT.