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/signer

v0.5.0

Published

Holder/issuer companion signer for the Bounded Authority Protocol — signs proofs, grants, boundary anchors, and key transitions through a caller-owned key handle whose private key never enters the library.

Readme

@bounded-authority-protocol/signer

npm CI license

Try it in your browser — no install: the envelope playground runs this package and the verifier package for real, in the page, with a key your browser generates. For the "why" (what this adds over OAuth for MCP servers, agents, and APIs), start at the playground's landing sections.

The holder/issuer companion signer for the Bounded Authority Protocol in TypeScript — the port of the Elixir bounded_authority_report_adapter (GitHub). This package signs contract-major 1 (the byte-frozen v1 profile and the local-loopback profile), contract-major 2 (the v2 profile: v: 2 payloads, BAP2-* separators and the lte/gte range selectors, Ed25519) and contract-major 3 (the BAP3-ES256-SHA256 suite: ECDSA P-256 with SHA-256, low-S normalized, defined by ADR 0035). It runs on Node.js, in browsers, in Deno and in Bun with no Node API (see Browsers, Deno, Bun and workers).

The protocol's verifier package produces each object's deterministic signing input and refuses to sign. This library takes a caller-owned key handle and a report and produces the signed compact form. The private key never enters the library: callers supply a handle whose methods reach their own custody — an HSM, a KMS, or an in-process key in tests. Every handle method may be async; remote custodians are first-class.

It signs; @bounded-authority-protocol/verifier verifies. It is not a decision maker, not a transport, and not the authority runtime.

Install

npm install @bounded-authority-protocol/signer

Runs on Node.js >= 22 (the repository develops on the pinned 22.23.1 — see .tool-versions — and runs CI on 22.23.1 and 24.19.0), current browsers, Deno and Bun. One runtime dependency: the verifier package.

Protocol family

  • @bounded-authority-protocol/verifier (GitHub) — the verify side; this library signs, that package verifies. The dependency is one-directional (this signer depends on the verifier, never the reverse), and the verifier's README carries a worked "Pairing with the signer" example — the three-role production flow with this package's five signers, executed against published versions.
  • baselabs/bounded_authority_protocol — the protocol monorepo: the wire specs this package signs (spec/bap-v1.md, spec/bap-local-loopback-http-v1.md, spec/bap-v2.md, spec/bap-v3.md) and the governing ADRs.
  • baselabs/bounded_authority_report_adapter — the Elixir reference implementation this package ports; signing behavior tracks it, and divergences carry a recorded decision.

The five signers

| Function | Object | Role | |---|---|---| | signReport | holder proof (the grant passes through untouched) | holder | | signLocalLoopbackReport | local-loopback application proof (ba+loopback-proof) | holder | | signAnchor | boundary anchor | role-agnostic | | signKeyTransition | key transition | role-agnostic | | signGrant | grant | issuer-only, structurally gated |

Every result is { ok: true, value } or { ok: false, error } with a closed error-code set (invalid_report, invalid_anchor, invalid_grant, invalid_transition, invalid_key_handle, signing_failed, producer_error) — no key material, nonce values, or report content ever appears in an error.

The four v2 signers

| Function | Object | Role | |---|---|---| | signV2Report | holder proof (the v2 grant passes through untouched) | holder | | signV2Anchor | boundary anchor | role-agnostic | | signV2KeyTransition | key transition | role-agnostic | | signV2Grant | grant | issuer-only, structurally gated |

Before 0.5.0 contract-major 2 had no signing surface in this package. The v2 functions take the same Ed25519 KeyHandle as the v1 functions and compose through the verifier package's public v2 signing inputs (v2.grantSigningInput, v2.proofSigningInput, v2.boundaryAnchorSigningInput, v2.keyTransitionSigningInput) and v2.assembleCompact, so the bytes are the verifier's v2 format and nothing more: v: 2 payloads, the BAP2-* domain separators, and the five-kind selector algebra (all, equals, one_of, lte, gte; a range bound is integer- or float-tagged). The same gates apply: the C1 role gate, the wrong-key guard, the atomic identity snapshot, and the weak-key refusal. The local-loopback profile is contract-major-1-bound and has no v2 form. Each major verifies only its own bytes: checkEnvelope refuses a v2 envelope and v2.checkEnvelope refuses a v1 one.

const grant = await signV2Grant({
  issuer: "https://issuer.example.test", grantId: "urn:example:grant:2",
  audiences: ["https://resource.example.test"],
  issuedAt: 1_731_728_000, notBefore: 1_731_728_000, expiresAt: 1_731_736_000,
  holderThumbprint: holderThumbprintB64,
  operations: [{ name: "transfer", selectors: [{ kind: "lte", path: ["amount"], value: { t: "int", v: 10_000 } }] }],
}, issuerHsmHandle);
// then signV2Report(...) with the holder's handle, and v2.checkEnvelope(...) on the receiving side

The four v3 signers (ES256)

| Function | Object | Role | |---|---|---| | signV3Report | holder proof (the v3 grant passes through untouched) | holder | | signV3Anchor | boundary anchor | role-agnostic | | signV3KeyTransition | key transition | role-agnostic | | signV3Grant | grant | issuer-only, structurally gated |

The v3 surface mirrors the v1 functions under the BAP3-ES256-SHA256 suite: alg: "ES256" protected headers, payload v: 3, the BAP3-REQUEST\0 digest prefix, the EC proof JWK {"crv":"P-256","kty":"EC","x":…,"y":…} with the RFC 7638 EC thumbprint as cnf.jkt, the five-kind selector algebra (including lte/gte), 65-byte uncompressed-SEC1 raw public keys (0x04 || x || y), and 64-byte raw r || s signatures with low-S normalization — s is replaced by n − s when high, so every emitted signature satisfies 0 < s ≤ (n−1)/2 and cannot be re-spelled into a second valid encoding (ECDSA malleability; the backend emits high-s about half the time). The v3 key-handle contract (Es256KeyHandle) keeps the same custody boundary with the suite's widths. The local-loopback profile is contract-major-1-bound and has no v3 form. The v3 producing profile is authored in this package (delegating composition to the verifier's v3 producers remains a recorded, owner-gated switch), and the compacts CI produces under it cross-verify through the verifier package's own v3 surface — no self-round-trip claims.

Quickstart — an edge agent proves a request

import { signGrant, signReport } from "@bounded-authority-protocol/signer";

// Issuer side: an issuer-role handle signs the capability grant.
const grantResult = await signGrant(
  {
    issuer: "https://issuer.example.test",
    grantId: "urn:example:grant:1",
    audiences: ["https://resource.example.test"],
    issuedAt: 1_731_728_000, notBefore: 1_731_728_000, expiresAt: 1_731_736_000,
    holderThumbprint: holderThumbprintB64, // RFC 7638 thumbprint of the holder key
    operations: [{ name: "transfer", selectors: [{ kind: "all" }] }],
  },
  issuerHsmHandle, // { signingIdentity(): { role: "issuer", keyId, publicKey }, sign, ... }
);
if (!grantResult.ok) throw new Error(`grant signing failed: ${grantResult.error}`);

// Holder side: the agent's handle signs the proof binding THIS request.
const proofResult = await signReport(
  {
    grantCompact: grantResult.value.grant, operation: "transfer", method: "POST",
    targetUri: "https://resource.example.test/invoke",
    invocationId: "550e8400-e29b-41d4-a716-446655440000",
    castArguments: { t: "object", v: new Map([["amount", { t: "int", v: 5000 }]]) },
  },
  holderHandle,
  { proofId: "urn:example:proof:1", issuedAt: 1_731_728_030 },
);
if (!proofResult.ok) throw new Error(`proof signing failed: ${proofResult.error}`);
const envelope = proofResult.value; // { grant, proof }

// The receiver verifies with the verifier package, never trusting this library:
// checkEnvelope(envelope.grant, envelope.proof, expected) returns cryptographic facts.

Every signer returns a result to check before use: { ok: true, value } or { ok: false, error }. The verifier README's Pairing with the signer section runs this flow through checkEnvelope with every expected input spelled out.

The key-handle contract

interface KeyHandle {
  sign(message: Uint8Array): Uint8Array | Promise<Uint8Array>;        // 64-byte Ed25519
  publicKey(): Uint8Array | Promise<Uint8Array>;                       // 32 bytes
  thumbprint(): string | Promise<string>;                              // RFC 7638, base64url
  keyIdentity?(): { keyId, publicKey } | Promise<...>;                 // anchor + transition
  signingIdentity?(): { role: "issuer" | "holder", keyId, publicKey }; // grants
}

The v3 suite takes the same contract shape under Es256KeyHandle with the suite's fixed widths: sign() returns the 64-byte raw r || s (a high-s from the custodian is fine — the library normalizes before emission), publicKey() the 65-byte uncompressed-SEC1 point, and thumbprint() the RFC 7638 EC thumbprint.

Two load-bearing rules port unchanged from the Elixir adapter (both v1 and v3):

  • The C1 gate — signGrant resolves the handle's role, key id, and public key as ONE atomic signingIdentity() snapshot and fails closed before sign() is ever called unless the role is issuer. A holder handle can never mint its own capability.
  • The wrong-key guard — the shared signing tail verifies every signature against the resolved public key before assembly, with the verifier package's strict functions (verifyStrict for Ed25519, es256VerifyStrict for ES256, which also refuses a high-s signature, so a missed low-S normalization fails closed). A handle whose sign() used a different key (a rotation race, a misconfigured signing service) fails loudly as signing_failed, never a silent false-success deferred to the verifier. A handle that returns r or s outside (0, n) is signing_failed too. The handle gets its own copy of the message (it may rewrite or transfer it), and the library keeps private copies of the message, the resolved key and the returned signature, so nothing the handle still holds can change what is checked or assembled. A handle method or getter that throws, a null snapshot or a Proxy-wrapped key is invalid_key_handle; a throwing onEvent listener is ignored.

The local-loopback profile is selected by function name — never inferred: the nonce is required, and the target must be exactly http://127.0.0.1[:port]/… or http://[::1][:port]/… (localhost and every non-canonical spelling fail closed).

Browsers, Deno, Bun and workers

The package is ESM-only, has no side effects on import ("sideEffects": false), and uses no Node API: no node:* module, no Buffer, no process. Every cryptographic operation it performs itself (the post-sign verify, SHA-256) comes from the verifier package, which picks its own backend through its "imports" conditions, so a browser bundle, Deno and Bun get the verifier's pure-JavaScript portable backend and Node.js keeps node:crypto:

| Runtime | Verifier BACKEND_ID | |---|---| | Node.js (default conditions) | "node" | | Node.js with --conditions=bap-portable | "portable" | | Deno, Bun | "portable" | | Browsers and bundlers targeting them, workers | "portable" |

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

The signer uses no WebCrypto and never touches a private key: signing is always your handle's sign(), wherever and however that handle is implemented.

In-page verification is display-only, never a trust decision. If a page signs and then verifies an envelope (as the playground does), that result 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.

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(). The default proofId comes from crypto.randomUUID(), which browsers provide only in a secure context (HTTPS or localhost). On a plain-HTTP page, pass proofId explicitly; without it the report signers return the closed producer_error.

Bundle size (esbuild, minified, browser target, tree-shaken from the package entry, verifier included): every export 184 KB (49 KB gzip); { signReport } 113 KB (31 KB gzip); { signV2Report } 111 KB (30 KB gzip); { signV3Report } 69 KB (25 KB gzip). CI fails a change that grows any of these by more than 10%.

Evidence

Every compact this library produces is verified in CI through the independent verifier package (checkEnvelope, verifyGrant, verifyHistoricalAnchor, verifyKeyTransition, the loopback profile's checkEnvelope, and the v2 and v3 namespaces' equivalents) — no self-round-trip claims. The v1 and v2 profiles compose through the verifier package's own producers; the v3 producing profile is authored here (delegating composition to the verifier's v3 producers remains a recorded, owner-gated switch), and its compacts cross-verify through the verifier package's v3 surface — the flip the former gap pin anticipated — while every produced signature is additionally pinned at the byte level: verified over the exact RFC 7515 signing input under node:crypto, both in the raw ieee-p1363 form and re-encoded as DER. The closure gates (C1 role gate, wrong-key guard, loopback nonce and canonical-target admission, atomic-identity requirement, and the v3 additions: low-S normalization and the P-256 on-curve check) are red-capable: mechanically removing the check fails its test. CI runs the full gate on ubuntu-24.04 only: CI is Linux only. The clone → build → test contract holding on macOS and Linux is a developer-setup property, proven on a developer machine, not a CI matter. Windows developers use WSL2 (clone inside the WSL filesystem, not under /mnt/c), which is the Linux path; no native-Windows developer path is maintained.

The sign-then-verify battery (v1, loopback, v2 and v3 envelopes, grants, anchors and key transitions; a wrong-key handle on every major; 64 ES256 reports from a handle that emits high-s about half the time, each normalized and verified) runs on Node 22.23.1 and 24.19.0 with each verifier backend, on Deno 2.5.6 and Bun 1.3.14, and in Chromium, Firefox and WebKit (Linux CI, Playwright, under the strict CSP above); every lane first asserts the verifier BACKEND_ID it expects. A Chromium lane on a non-secure origin checks the closed producer_error for a missing default proofId. Guards keep Node APIs out of src/: a browser typecheck with no ambient types, lint rules (Node modules and globals, dynamic code, @noble and WebCrypto), a scan of the browser bundle for unresolved globals, and a packed-tarball consumer typecheck with skipLibCheck: false. Linux WebKit is evidence for this pure-JavaScript code only, not for Safari. The publish lane is separate: it re-verifies (build, typecheck, lint, guards, tests, the runtime, tarball and browser lanes, and npm audit signatures) on Node 24 — the npm >= 11.5 that trusted publishing requires — before staging a release that a human approves.

Versioning and compatibility

Package versions follow SemVer. Before 1.0 the minor position is the breaking boundary, so npm caret ranges such as ^0.2.1 never select 0.3.0.

0.5.x depends on @bounded-authority-protocol/verifier ^0.7.0, runs in browsers, Deno and Bun, adds the contract-major-2 signers, and checks every ES256 signature with the verifier's strict es256VerifyStrict after low-S normalization. Its "exports" map keeps deep imports such as @bounded-authority-protocol/signer/dist/src/v3.js resolving through a "./*" passthrough, but deep imports are unsupported API and may be closed in a future 0.x minor; import from the package root. 0.4.x depends on @bounded-authority-protocol/verifier ^0.6.0 and refuses small-order and non-canonical Ed25519 key handles. 0.3.x depends on @bounded-authority-protocol/verifier ^0.5.0 and signs through its producers, so it inherits that release's stricter identifier admission. Identifiers with repeated fragment delimiters, raw brackets in userinfo or outside authority, or malformed bracketed IPv6 hosts such as http://[abc]/x now return producer_error instead of producing bytes a verifier would reject. Valid identifiers, including bracketed IPv6 literals such as http://[::1]/, sign as before. Compatibility with other implementations is governed by the wire contract-majors (this package signs majors 1, 2 and 3), not by package version numbers. Release notes are in CHANGELOG.md.

License

Apache-2.0. See LICENSE and NOTICE.