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

@bounded-authority-protocol/verifier

v0.7.0

Published

Provider-neutral, deterministic verifier SDK for bounded proof-of-possession authority — a typed TypeScript reimplementation of the Bounded Authority Protocol verification profiles (wire contract-majors 1, 2, and 3) for Node.js, browsers, Deno and Bun.

Readme

@bounded-authority-protocol/verifier

npm CI license

Deterministic, fail-closed verification for the Bounded Authority Protocol — bounded proof-of-possession authority for services and AI agents, in pure TypeScript.

The SDK verifies the protocol's three wire profiles (contract-majors 1, 2, and 3): compact-JWS grants, holder proofs, consumption chains, boundary anchors, key transitions, and archived exports. It is a TypeScript reimplementation of the reference profiles — derived from the published specifications and certified conformance corpora. It runs on Node.js, in browsers, and on Deno, Bun and workers with identical verdicts: on Node.js, Ed25519, ES256 and SHA-256 run on node:crypto; everywhere else a portable pure-JavaScript backend runs them over three audited, exactly pinned runtime dependencies (@noble/ed25519, @noble/curves, @noble/hashes). Canonicalization is hand-rolled from the RFCs. It also carries the protocol's byte-distinct sibling profiles for role attestations (bap-role-attestation/1, under the roleAttestation namespace) and content assertions (bap-content-assertion/1, under the contentAssertion namespace), parsed by no contract-major and parsing none of them.

It verifies; it never authorizes. A successful result proves that caller-supplied bytes satisfy caller-supplied trusted inputs and expected context — nothing more. There is no allowed, no authorized, no decision anywhere in the API: results are value-bearing, redacted facts, and every failure is a single closed rejection.

Install

npm install @bounded-authority-protocol/verifier

Runs on Node.js >= 22, current browsers, Deno and Bun (see Browsers, Deno, Bun and workers). Three runtime dependencies, pinned exactly and MIT-licensed: @noble/ed25519 3.2.0, @noble/curves 2.4.0 and @noble/hashes 2.4.0, loaded only by the portable backend.

Quickstart — verify a grant

import { verifyGrant } from "@bounded-authority-protocol/verifier";

// The raw compact-JWS grant bytes (ASCII), produced out-of-band by an issuer.
const grantCompact = Buffer.from(
  "eyJhbGciOiJFZERTQSIsInR5cCI6ImJhK2NhcCIsImtpZCI6Imlzc3Vlci1rZXkifQ." +
    "<payload>.<signature>",
  "ascii",
);

const result = verifyGrant(
  grantCompact,
  {
    keyId: "issuer-key",            // must match the grant header `kid` exactly
    publicKey: issuerPublicKey32,   // raw 32-byte Ed25519 public key — public keys only
  },
  {
    issuer: "https://issuer.example",
    audience: "https://resource.example",
    evaluationTime: 1_731_728_000,  // caller-supplied, seconds since epoch — the SDK reads no clock
    clockSkew: 60,
  },
);

if (result.ok) {
  const facts = result.value;       // GrantFacts — value-bearing, redacted
  facts.holderThumbprint;           // Uint8Array(32) — the bound holder key
  facts.authorization;              // "not_evaluated" — verification is not authority
} else {
  // { ok: false } — every structural, signature, header, claim, or time-window
  // failure lands here. Closed and value-free: no reason, no partial data.
}

Every public function returns the same Result<T> shape — { ok: true, value } or { ok: false } — mirroring the reference's {:ok, value} | {:error, :invalid}.

Strict Ed25519 (rule S1)

Every Ed25519 verification in the package goes through one gate, then the backend's cofactorless equation. A signature is accepted only if the key A and the signature point R are canonical encodings (RFC 8032 section 5.1.3), neither is one of the eight small-order encodings, and S < L. A key that is non-canonical, of small order, or off the curve is refused wherever it enters (trusted issuer and archive keys, proof-header JWKs, producers, thumbprints) with the closed error.

Three helpers are exported for hosts:

import { verifyStrict, acceptablePublicKey, torsionFree } from "@bounded-authority-protocol/verifier";

verifyStrict(message, signature64, publicKey32); // boolean; never throws
acceptablePublicKey(publicKey32); // canonical, not small order, on the curve
torsionFree(publicKey32);         // acceptable, and [L]A = O (under 1 ms; never on the verify path)

Verification itself has no subgroup check. A host MUST call torsionFree at key enrollment, at trust install, and before any deny-list or revocation comparison, so that one secret has one key encoding.

The ES256 sibling is es256VerifyStrict(message, signature64, publicKey65): synchronous and total (it never throws), it returns true only for an uncompressed, on-curve P-256 key, a raw r||s signature with 0 < r < n and 0 < s <= (n - 1) / 2 (low-S), and a valid ECDSA P-256 / SHA-256 signature over the message. It is an SDK helper that mirrors REQ3-SIGNING-*; the Elixir reference has no public counterpart yet.

Browsers, Deno, Bun and workers

The package is ESM-only, has no side effects on import ("sideEffects": false), and uses no Node API outside one module. Its cryptography sits behind an internal backend that the package's "imports" conditions select, so the API stays synchronous everywhere and no WebCrypto is used:

| Runtime | Condition that matches | Backend (BACKEND_ID) | |---|---|---| | Node.js (default) | node | "node": node:crypto | | Node.js with --conditions=bap-portable | bap-portable | "portable": @noble | | Deno | deno | "portable" | | Bun | bun | "portable" | | Cloudflare Workers and other builds with the workerd or worker condition | workerd, worker | "portable" | | Browsers and bundlers targeting them | default | "portable" |

The conditions are matched in the order bap-portable, deno, bun, workerd, worker, node, so a build that sets any of the first five gets the portable backend even when it also sets node. A build that sets node and none of them gets the node backend. Common cases:

  • webpack's Electron targets (electron-main, electron-preload, electron-renderer), which add the node condition;
  • esbuild with --platform=node (with a workerd or worker condition added, the portable backend);
  • Rollup with @rollup/plugin-node-resolve and exportConditions: ["node"];
  • Vite SSR builds, which resolve the node condition.

To force the portable backend in any of these, add the bap-portable condition. BACKEND_ID names the implementation, not the platform library under it: "node" means node:crypto of the running runtime, whether that is linked against OpenSSL (Node.js) or BoringSSL (Electron), and it cannot tell the two apart. Verdicts do not depend on it, because every protocol gate runs before the backend. The bap-source condition is for this repository's own tests: its targets are in src/, which the package does not ship, so a consumer must not set it.

import { BACKEND_ID } from "@bounded-authority-protocol/verifier";
// "node" or "portable": which implementation this build resolved. Verdicts are the same in both.

Both backends sit behind the same profile gates (rule S1 for Ed25519; the point, range and low-S checks for ES256), the portable Ed25519 check is an explicit cofactorless one (encode([S]B - [k]A) == R, never a cofactored verify), and the portable P-256 verify leaves the low-S rule to the profile gate. Every certified corpus produces byte-identical verdict digests on Node 22 and 24 with either backend, on Deno and Bun, and in Chromium, checked case by case against the Node verdicts. CI runs the same browser lane in Chromium, Firefox and WebKit on Linux; for this release only Chromium has been observed (locally, Chromium 153), and no Firefox or WebKit result is claimed here.

In-page verification is display-only, never a trust decision. A result computed in a web page is for showing to a person. It must not authorize anything, feed a trust decision, or be sent anywhere as evidence of authority: the page's own scripts, browser extensions and injected code share its origin and can change both the inputs and the answer. The trusted decision stays with the host runtime that holds the trusted keys. As on Node, trusted keys, time and expected context are explicit inputs; a page must never take them from the artifact it is inspecting.

The package works under a strict Content Security Policy (default-src 'none'; script-src 'self'; connect-src 'self', no 'unsafe-eval'): it uses no eval, no Function constructor and no dynamic import(), and the browser lanes run under exactly that policy. Linux WebKit in CI is evidence for this pure-JavaScript verifier only, not for Safari.

Bundle size (esbuild, minified, browser target, tree-shaken from the package entry): every export 263 KB (70 KB gzip); { v3 } 121 KB (36 KB gzip); { checkEnvelope } (v1) 52 KB (19 KB gzip). CI fails a release that grows any of these by more than 10%.

Wire contract-majors

The protocol versions its wire formats as complete, parallel contract-majors. All three live in this package; each verifies only its own bytes — there is no fallback or downgrade in any direction.

| Major | Import | Selector kinds | Suite | Notes | |---|---|---|---|---| | 1 | import { verifyGrant, ... } | all, equals, one_of | BAP1-Ed25519-SHA256 | The original profile | | 2 | import { v2 } from "@bounded-authority-protocol/verifier" | + lte, gte | BAP2-Ed25519-SHA256 | Adds inclusive same-tag range selectors (intervals compose conjunctively) | | 3 | import { v3 } from "@bounded-authority-protocol/verifier" | same five as v2 | BAP3-ES256-SHA256 | The ES256 suite: ECDSA over NIST P-256, raw 65-byte SEC1 public keys, low-S raw r\|\|s signatures |

The v2 namespace mirrors the full v1 surface — v2.verifyGrant, v2.checkEnvelope, v2.grantSigningInput, and so on — under the major-2 separators, suite name, and v: 2 payloads. The v3 namespace mirrors it again under the ES256 suite: alg: "ES256", the 65-byte uncompressed-SEC1 raw public-key form, the {crv, kty, x, y} proof JWK with its RFC 7638 thumbprint, and the RFC 7518 §3.4 raw r||s signature form with low-S enforced at verification (both crypto backends accept the malleable high-S counterpart; the profile gate is load-bearing). Every v1 and v2 artifact rejects under v3 with the single closed error, and vice versa.

import { v2 } from "@bounded-authority-protocol/verifier";

const result = v2.checkEnvelope(grantV2, proofV2, {
  trustedIssuer: { keyId: "issuer-key", publicKey: issuerPublicKey32 },
  issuer: "https://issuer.example",
  audience: "https://resource.example",
  method: "POST",
  targetUri: "https://api.example.test/invoke",
  invocationId: "…", operation: "transfer",
  castArguments: { t: "object", v: new Map([["amount", { t: "int", v: 5000 }]]) },
  evaluationTime: 1_731_728_000, clockSkew: 60, proofMaxAge: 300,
  nonce: { kind: "not_required" },
});
// The grant above may carry { kind: "lte", path: ["amount"], value: 5000 } —
// amount 5001 rejects; 5000 verifies inclusively; cross-tag operands never match.

castArguments uses the SDK's tagged JSON algebra (int/float/string/object tags carry the selector operand domain); the Tagged type is exported from the package root.

API surface

The v1 façade (the v2 namespace mirrors it):

| Function | Returns | Purpose | |---|---|---| | verifyGrant | Result<GrantFacts> | Verify a compact grant against a trusted issuer + expected context | | checkEnvelope | Result<EnvelopeFacts> | Re-verify the grant and bind the holder proof, request, nonce, and selectors | | decodeGrant / decodeProof | Result<GrantDecoded \| ProofDecoded> | Structural decode (verification: not_evaluated) | | untrustedKeyLocator | Result<KeyLocator> | Header-only key id (trust: not_evaluated) | | requestDigest | Uint8Array(32) | Typed, type-preserving request hash (major-bound domain prefix) | | encodeConsumptionEntry / checkChain | Result<EncodedConsumptionEntry \| ChainFacts> | Canonical consumption rows + range verification | | grantSigningInput / proofSigningInput | Result<SigningInput> | Deterministic producer signing inputs | | assembleCompact | Uint8Array | External signature assembly — the SDK holds no private keys | | boundaryAnchorSigningInput / keyTransitionSigningInput | Result<SigningInput> | Anchor and historical-key-transition producers | | encodeAnchoredExport / verifyAnchoredExport | Result<EncodedAnchoredExport \| AnchoredExportFacts> | Deterministic archive framing + atomic verification | | verifyHistoricalAnchor / verifyKeyTransition | Result<AnchorFacts \| KeyTransitionFacts> | Historical boundary + authenticated rollover verification |

Plus the versioned primitives (jwkEncodePublic, thumbprint, uriNormalize, boundsNew, jcsEncode, base64urlDecode/Encode, the tagged JSON algebra), the byte-distinct local-development proof profile (localLoopbackHttpProofSigningInput and friends — literal 127.0.0.1/[::1] targets only, mandatory nonce; standard dpop+jwt rejects its bytes), and the standalone role-attestation sibling profile (bap-role-attestation/1): the roleAttestation namespace's four surfaces — attestationSigningInput (external signature only), assembleAttestationCompact, decodeAttestation, and verifyAttestation — bind a subject key to a role (issuer/holder) for a window contained in the attestor key's validity, with self-attestation rejected and facts carrying trust: "not_evaluated" and no authorization marker. The full export list is src/index.ts.

Content assertions

The separately selected contentAssertion namespace implements bap-content-assertion/1 under the existing Ed25519 suite. It binds exact content bytes by digest to an issuer, one audience, a stable subject lineage, a pinned semantic profile/schema digest, a generation/predecessor, and a validity window. It neither parses the content nor supplies operational authority.

The public operations are assertionSigningInput, assembleCompact, decodeAssertion, verifyAssertion, contentDigest, assertionDigest, and verifySuccessor. They return the SDK's closed Result<T> shape; successor comparison returns Result<void>. contentDigest hashes BAP1-CONTENT, one zero byte, and exact content bytes, with a tightenable content_bytes maximum of 65,536. assertionDigest checks this profile's structure and hashes its exact compact; it makes no signature claim.

ExpectedContentAssertion requires attestor, issuer, audience, subject, profile, raw 32-byte profileDigest and contentDigest, explicit now, and bounds. A key window's unbounded upper endpoint is null, following the existing HistoricalPublicKey contract. A consumer must validate its content schema and supply the digest of the exact retained bytes; signature verification alone does not validate a semantic profile.

Verified facts are redacted on inspection and carry trust: "not_evaluated". verifySuccessor compares complete facts and exact predecessor history, without requiring an expired predecessor to be currently valid. Facts reconstructed from storage remain caller-provenanced values. Trust admission, revocation knowledge, fork/bootstrap policy, durable generation/clock state, and restore continuity remain host responsibilities. The verifier does not derive key windows or walk key transitions. See the public profile specification and ADR 0037.

Pairing with the signer

This package verifies; it never signs and never holds a private key. Its sibling @bounded-authority-protocol/signer produces the signed bytes — see the whole three-role flow run live in your browser at the envelope playground — and it does so by calling THIS package's producer functions (grantSigningInput, proofSigningInput, boundaryAnchorSigningInput, keyTransitionSigningInput) and delegating the cryptography to a caller-owned key handle. The dependency direction is one-way: the signer depends on the verifier at runtime through a caret range on the verifier's current minor, never the reverse. Compatibility between the two is governed by the wire contract-majors (the signer produces majors 1 and 3; this package verifies 1, 2, and 3), not by package version numbers.

A production flow has three roles: the issuer mints a grant, the holder binds one invocation of it with a proof, and the resource verifies the pair through this package.

// Issuer — mint a grant. The private key never enters the signer; your handle
// routes each sign() to your own custody (KMS, HSM, or an in-process test key).
import { signGrant, signReport, type KeyHandle } from "@bounded-authority-protocol/signer";
import { checkEnvelope } from "@bounded-authority-protocol/verifier";

const issuerHandle: KeyHandle = {
  sign: (message) => issuerCustody.sign(message),  // Ed25519, 64 bytes
  publicKey: () => issuerPublicKey32,
  thumbprint: () => issuerThumbprint,              // RFC 7638, base64url
  // Grants additionally require an atomic issuer-role identity — the C1 gate:
  // a holder-role handle fails closed before sign() is ever called.
  signingIdentity: () => ({ role: "issuer", keyId: "issuer-key", publicKey: issuerPublicKey32 }),
};

// SignerResult mirrors this package's Result<T>: check ok before reading value.
const grantResult = await signGrant(
  {
    issuer: "https://issuer.example",
    grantId: "g-1",
    audiences: ["https://resource.example"],
    issuedAt: 1_731_728_000,
    notBefore: 1_731_728_000,
    expiresAt: 1_733_728_000,
    holderThumbprint,  // RFC 7638 base64url of the holder's public JWK (the thumbprint primitive)
    operations: [
      { name: "transfer", selectors: [{ kind: "equals", path: ["amount"], value: { t: "int", v: 5000 } }] },
    ],
  },
  issuerHandle,
);
if (!grantResult.ok) throw new Error(`signing failed: ${grantResult.error}`);
const grantCompact = grantResult.value.grant;

// Holder — bind one invocation of that grant with a holder proof. The holder's
// handle is constructed exactly like the issuer's (reports need no
// signingIdentity — proofs are holder-signed by definition), and the proof
// timestamp is pinned the same way: this verifier reads no clock.
const proofResult = await signReport(
  {
    grantCompact: grantCompact,
    operation: "transfer",
    method: "POST",
    targetUri: "https://api.example.test/invoke",
    invocationId: "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",  // a lowercase RFC 4122 UUID — the wire requires it
    castArguments: { t: "object", v: new Map([["amount", { t: "int", v: 5000 }]]) },
  },
  holderHandle,
  { issuedAt: 1_731_728_030 },
);
if (!proofResult.ok) throw new Error(`signing failed: ${proofResult.error}`);
const envelope = proofResult.value;

// Resource — THIS package, and nothing else. Facts, never a decision.
const result = checkEnvelope(envelope.grant, envelope.proof, {
  trustedIssuer: { keyId: "issuer-key", publicKey: issuerPublicKey32 },
  issuer: "https://issuer.example",
  audience: "https://resource.example",
  method: "POST",
  targetUri: "https://api.example.test/invoke",
  invocationId: "9a1b2c3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
  operation: "transfer",
  castArguments: { t: "object", v: new Map([["amount", { t: "int", v: 5000 }]]) },
  evaluationTime: 1_731_728_060,
  clockSkew: 60,
  proofMaxAge: 300,
  nonce: { kind: "not_required" },
});

Two properties make the pairing safe to build against:

  • Key custody stays at the caller. The signer's KeyHandle is a callback interface (sign/publicKey/thumbprint, plus atomic keyIdentity/signingIdentity snapshots for anchors, transitions, and role-gated grant signing). A handle fault or a wrong-key rotation race fails loudly as signing_failed — the signer verifies every signature against the resolved public key before assembly. The full contract is documented in the signer's README.
  • The two packages cross-validate in CI. The signer's test oracle produces compacts through its own surface and verifies every one of them through THIS package before release — signing-side drift is caught in the signer's CI against the verifier version its lockfile resolves, and verifier-side changes are gated by this package's certified conformance corpora.

Certified, not self-tested

Every release of this SDK is verified against the protocol's published, cryptographically certified conformance corpora — 283 vectors for contract-major 1, 268 for contract-major 2, 292 for contract-major 3, 40 for the bap-role-attestation/1 sibling profile, and 131 assertion, 9 digest, and 14 successor cases for the bap-content-assertion/1 sibling profile, plus 2410 strict-Ed25519 cases (rule S1: canonical, non-small-order keys and points, cofactorless verification) — recomputing every verdict from scratch, with each corpus index.json SHA-256 asserted at load so a drifted snapshot fails loudly. Permissiveness bugs (a parser accepting what the spec forbids) are invisible to corpus agreement by construction, so the suite additionally carries a per-language mutation gate that proves each closure red-capable: mechanically remove a check, watch its test fail.

Linux CI verification includes strict typecheck, build, lint (including a library purity rule — no I/O, clock, or randomness in src/ — and the browser-safety rules: no Node module or global, no dynamic code), a browser typecheck, a scan of the browser bundle for unresolved globals, a bundle-size budget, license check, unit tests, the mutation gate, and all seven conformance corpora against the vendored snapshots, on Node 22.23.1 and Node 24.19.0 with the node backend and the portable backend; the same corpora on Deno and Bun; a browser lane that CI runs in Playwright Chromium, Firefox and WebKit under a strict CSP; a consumer typecheck of the packed tarball; and npm audit signatures.

Releases are published only from this repository's release workflow, via npm trusted publishing (GitHub Actions OIDC — no long-lived tokens) with npm provenance on every artifact.

Versioning

Package versions follow SemVer. Wire contract-majors are a separate axis: a new contract-major lands additively (a minor release — this package already carries majors 1, 2, and 3 side by side), and a package major is owed only when a shipped profile or public API is removed or changes verdicts. Wire artifacts self-declare their major (payload v, the BAP<n>-… suite name, major-bound domain separators), so the package number never needs to encode it.

Before 1.0, the minor position is the breaking boundary: npm caret ranges such as ^0.4.1 never select 0.5.0. Release 0.5.0 is therefore the package-major step for its shipped-profile verdict change. It tightens identifier admission in the v1, v2, v3, and role-attestation profiles: repeated fragment delimiters, raw brackets in userinfo or outside authority, and malformed bracketed IPv6 hosts such as http://[abc]/x are now rejected. Valid compressed IPv6, final IPv4 groups, and escaped userinfo brackets keep their verdicts and exact identifier bytes. Target-URI normalization keeps its existing reference behavior. The same release adds the content-assertion namespace.

What this SDK does not do

It is a verifier, not an authority runtime. It holds no private keys, makes no network or filesystem calls, reads no clock, and performs no replay reservation, revocation check, or policy decision. Selecting trusted keys, reserving replay, and granting execution belong to the host — a facts value is evidence, never a credential.

Protocol documentation

Related packages

Development

pnpm install
pnpm typecheck && pnpm build && pnpm lint && pnpm license-check
pnpm test                    # unit + struct + façade corpus-vector tests
pnpm test:permissiveness     # the mutation gate
pnpm conformance             # 283/283 + key census (vendored v1 snapshot)
pnpm conformance:v2          # 268/268 + key census (vendored v2 snapshot)
pnpm conformance:v3          # 292/292 + key census (vendored v3 snapshot)
pnpm conformance:role-attestation # 40/40 + cross-profile legs (vendored sibling-profile snapshot)
pnpm conformance:content-assertion # 131 + 9 + 14 + producer/assembly byte agreement (vendored sibling-profile snapshot)
pnpm conformance:strict-ed25519 # 2410/2410 strict-Ed25519 verdicts (vendored additive snapshot)
pnpm conformance:local-loopback-http # 44/44 local-loopback proof profile verdicts
pnpm test:portable           # unit + mutation gate under --conditions=bap-portable
pnpm conformance:portable    # every corpus under the portable backend (same verdict digests)
pnpm typecheck:browser       # src/ under DOM + ES2022, no ambient types
pnpm guard:browser           # browser bundle: portable backend only, no unresolved globals or eval
pnpm size                    # browser bundle-size budget
pnpm test:tarball            # packed tarball: consumer typecheck and Node/Deno/Bun runs
pnpm test:runtimes           # Deno and Bun conformance lanes
pnpm test:browser            # Playwright lanes (BAP_BROWSERS=chromium,firefox,webkit)
pnpm check:currency          # dependency-currency gate (latest-first)

License

Apache-2.0. See LICENSE and NOTICE.