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

@keelcodes/policy

v0.1.0

Published

Account-agnostic agent authorization policy: declarative DSL, canonical commitment hash and off-chain pre-check/simulation.

Readme

@keelcodes/policy

Account-agnostic authorization policy for agent accounts.

A policy declares what an agent may do with an account: which target contracts it may call, which methods on them, and the value / frequency ceilings. The model contains no account-specific encoding, so the same Policy works across Kernel, Nexus and Safe7579.

This package is the off-chain core: the DSL, a canonical commitment hash, a pre-check / simulation layer, and multi-session lifecycle management. The on-chain carrier for enforcement is a Keel ERC-7579 hook module (account-agnostic: a hook sees the execution of any 7579 account without parsing account-specific call data). The hook recomputes the same commitment, so the two layers cannot disagree about what a policy means.

Policy DSL

import { normalizePolicy, policyCommitment } from '@keelcodes/policy';

const policy = normalizePolicy({
  validAfter: 0n,          // unix seconds; 0 = immediately
  validUntil: 1_800_000_000n, // 0 = never
  rules: [{
    target: USDC, selectors: ['0xa9059cbb'],
    maxPerTx: 10n ** 6n, maxDaily: 10n ** 7n, maxCalls: 50,   // native value / count
    tokenLimits: [{ token: USDC, maxPerTx: 10n ** 6n, maxDaily: 10n ** 7n }], // ERC-20 amount
  }],
});

policyCommitment(policy); // keccak256(abi.encode(version, validAfter, validUntil, rules))

Each rule scopes to one target with optional selectors (empty = any method) and ceilings: maxPerTx (native value per call), maxDaily (native value per day), maxCalls (call count). 0 / omitted means unlimited.

normalizePolicy fills every optional with its zero default and lower-cases addresses, so { maxPerTx: 0n } and {} produce the same commitment. A malformed policy throws PolicyError at definition time.

ERC-20 token limits

tokenLimits adds per-token amount ceilings (maxPerTx / maxDaily) on top of the native-value caps. The token must equal the rule's target — a cap can only be enforced on calls made directly to the token, so a limit for any other address is rejected as dead config.

Amounts are read from the standard transfer / approve / transferFrom call data (the amount is the final uint256 word). While a limit is configured:

  • transfer / approve are capped by the token ceilings;
  • transferFrom is refused (token-transfer-from-blocked) — bounding a pull from an arbitrary address is ambiguous, so the conservative choice is to reject it;
  • a malformed standard call is refused (token-amount-unparsable);
  • any other selector is left to the rule's selectors whitelist.

Token caps apply to the immediate call target only; a token moved inside a router call is not covered by this rule (use a rule on the token itself).

Pre-check & simulation

evaluateCall applies validity, rule matching (first match in declared order) and the ceilings. simulateCalls dry-runs a batch, accumulating usage so a batch that only breaches a cap together is caught, and returns the first denial with a machine-readable reason — before a bundler round-trip that would end in a validation failure.

import { evaluateCall, simulateCalls, toCall } from '@keelcodes/policy';

const call = toCall({ target: USDC, value: 0n, data: transferCalldata });
evaluateCall(policy, { now: 1_700_000_000n, usage: [] }, call);
// → { allowed: true, ruleIndex: 0 }

PolicyState.usage is aligned to policy.rules by index; callers reset dailySpent / tokenSpent when the day window rolls over.

Hook install payload

The on-chain hook installs one session at a time. encodeInstallData(sessionId, policy) builds the exact payload it decodes — abi.encode(bytes32 sessionId, bytes policyData) with policyData = encodePolicy(policy). The session's on-chain commitment is therefore keccak256(policyData) === policyCommitment(policy), so an installed session can be verified against what was signed.

import { encodeInstallData } from '@keelcodes/policy';

encodeInstallData(session.id, session.policy); // → onInstall bytes

Multi-session lifecycle

An account may hold many concurrent sessions — one policy grant each — which fixes the old "one account, one session" limitation. A session is an immutable record carrying the policy, its commitment and lifecycle metadata, and it holds no key material: it references a policy, not a private key, so key custody stays with the caller (KMS or the client).

import { InMemorySessionStore, issueSession, revokeSession, rotateSession } from '@keelcodes/policy';

const store = new InMemorySessionStore();
const session = await issueSession(store, { account, policy, now });
await revokeSession(store, session.id, now);
const { previous, next } = await rotateSession(store, session.id, { policy, now });

sessionStatus(session, now) derives pending / active / expired / revoked from the record, and listSessions(store, account, now) returns an account's sessions with their status. Revocation is append-only (the record is kept with revokedAt, so it stays auditable), and a rotation cross-links the predecessor and successor (rotatedFrom / rotatedTo) — the off-chain half of the migration "uninstall old module + install new" (docs/KEEL_PLAN.md §7.5). Persistence goes through the async SessionStore port, so a database-backed store drops in; InMemorySessionStore is the minimal reference (no tenancy — one Keel stack per product, see §6.3).

Scope

  • ✅ Declarative DSL + normalisation + validation
  • ✅ Canonical commitment hash (ABI encoding shared with the on-chain carrier)
  • ✅ Off-chain pre-check + batch simulation
  • ✅ ERC-20 token limits (maxPerTx / maxDaily per token)
  • ✅ ERC-7579 hook module (KeelPolicyHook, Solidity) enforcing the same rules on-chain, many sessions per account (see contracts/)
  • ✅ Multi-session lifecycle (issue / revoke / rotate) + pluggable SessionStore

Pre-alpha. The public API is not stable yet.

Out of scope

  • Account implementations (see @keelcodes/adapters)
  • Billing and multi-tenancy