{"_id":"@agentic-research/dpop","_rev":"3-ab1bf5b16598124f1e083d13c137555c","name":"@agentic-research/dpop","dist-tags":{"latest":"0.4.0"},"versions":{"0.2.0":{"name":"@agentic-research/dpop","version":"0.2.0","keywords":["dpop","rfc9449","oauth2","jwt","proof-of-possession","webcrypto"],"license":"Apache-2.0","_id":"@agentic-research/dpop@0.2.0","maintainers":[{"name":"jamestexas","email":"jamestexasgardner@gmail.com"}],"homepage":"https://github.com/agentic-research/notme/tree/main/packages/dpop","bugs":{"url":"https://github.com/agentic-research/notme/issues"},"dist":{"shasum":"13c15742304d0c862bcbefedcdbbc039683bf63c","tarball":"https://registry.npmjs.org/@agentic-research/dpop/-/dpop-0.2.0.tgz","fileCount":7,"integrity":"sha512-c0l/kGk1LwosKbVv405n5i9O0IoAerwnIq3JW7jmkRO9Wnt0gcGST27RV89dsJV/GcxEf0UPoVWTzJnXjbKsiA==","signatures":[{"sig":"MEUCIBhJ+fptlHIwUOvsm9jBDSkZxOK9B3LiZTJRtzjd2KI0AiEAubCQxpMt402uE0fRXRWSSlC7a4fGuiDDbGVSxUffXG8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":80403},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"a74fcd963e94e484e0cd92c7c756711f924d5a2e","scripts":{"build":"tsc -p tsconfig.json","prepack":"pnpm run build","prepublishOnly":"tsc -p tsconfig.json"},"_npmUser":{"name":"jamestexas","email":"jamestexasgardner@gmail.com"},"repository":{"url":"git+https://github.com/agentic-research/notme.git","type":"git","directory":"packages/dpop"},"_npmVersion":"11.12.1","description":"DPoP (RFC 9449) utilities and resource-server verifier SDK for notme-issued access tokens. Zero dependencies, pure Web Crypto — runs on Cloudflare Workers, Node, Deno, and browsers.","directories":{},"sideEffects":false,"_nodeVersion":"25.9.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/dpop_0.2.0_1784825389210_0.38757544197859795","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@agentic-research/dpop","version":"0.3.0","keywords":["dpop","rfc9449","oauth2","jwt","proof-of-possession","webcrypto"],"license":"Apache-2.0","_id":"@agentic-research/dpop@0.3.0","maintainers":[{"name":"jamestexas","email":"jamestexasgardner@gmail.com"}],"homepage":"https://github.com/agentic-research/notme/tree/main/packages/dpop","bugs":{"url":"https://github.com/agentic-research/notme/issues"},"dist":{"shasum":"c1cd3dbe469b2b5079bd1546ee5465471615f6ae","tarball":"https://registry.npmjs.org/@agentic-research/dpop/-/dpop-0.3.0.tgz","fileCount":12,"integrity":"sha512-8f5IVOSN4N3zvDxTkHjRcOM2SHJd5oMDbLHTgweYLFwRChb1kQaITu2SpJo4XQG6SYAOLMDRZmjKN0SjoLxiCA==","signatures":[{"sig":"MEQCIA9qjJjuHMJ+lxyt3YwfLKxPd+CbE4Nr3HQFXH+Yt9PjAiBna0F6rfNPvteSCyzDkRPjx886jWlimwhI5pO2OH9qgA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":108347},"main":"./dist/index.js","type":"module","_from":"file:agentic-research-dpop-0.3.0.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc -p tsconfig.json","test:package":"node ./scripts/check-package.mjs"},"_npmUser":{"name":"jamestexas","email":"jamestexasgardner@gmail.com"},"_resolved":"/private/var/folders/h3/m8zflqfn203fxwfvhwqbt_c40000gn/T/7809864e33d738336ec290e318ded448/agentic-research-dpop-0.3.0.tgz","_integrity":"sha512-8f5IVOSN4N3zvDxTkHjRcOM2SHJd5oMDbLHTgweYLFwRChb1kQaITu2SpJo4XQG6SYAOLMDRZmjKN0SjoLxiCA==","repository":{"url":"git+https://github.com/agentic-research/notme.git","type":"git","directory":"packages/dpop"},"_npmVersion":"11.12.1","description":"DPoP (RFC 9449) utilities and resource-server verifier SDK for notme-issued access tokens. Zero dependencies, pure Web Crypto — runs on Cloudflare Workers, Node, Deno, and browsers.","directories":{},"sideEffects":false,"_nodeVersion":"25.9.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/dpop_0.3.0_1784936176168_0.4258934817552418","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@agentic-research/dpop","version":"0.4.0","description":"DPoP (RFC 9449) utilities and resource-server verifier SDK for notme-issued access tokens. Zero dependencies, pure Web Crypto — runs on Cloudflare Workers, Node, Deno, and browsers.","type":"module","license":"Apache-2.0","repository":{"type":"git","url":"git+https://github.com/agentic-research/notme.git","directory":"packages/dpop"},"homepage":"https://github.com/agentic-research/notme/tree/main/packages/dpop","main":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc -p tsconfig.json","test:package":"node ./scripts/check-package.mjs","prepack":"pnpm run build","prepublishOnly":"tsc -p tsconfig.json"},"devDependencies":{"typescript":"^5.9.3"},"keywords":["dpop","rfc9449","oauth2","jwt","proof-of-possession","webcrypto"],"sideEffects":false,"publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"types":"./dist/index.d.ts","gitHead":"640218bdc632da91dca107304c3a09a9429b5875","_id":"@agentic-research/dpop@0.4.0","bugs":{"url":"https://github.com/agentic-research/notme/issues"},"_nodeVersion":"22.23.1","_npmVersion":"11.19.0","dist":{"integrity":"sha512-etkZBF3mTV2wHOofOAuMF+9Kg333QrJXhSg00v55K1NlsSNEmKF9KLgMDHNHY9LmZ9dx7ryr1qNl2xUAR3MFqA==","shasum":"bf25929783ddb2a53c73445ea59d6817cbac2a35","tarball":"https://registry.npmjs.org/@agentic-research/dpop/-/dpop-0.4.0.tgz","fileCount":11,"unpackedSize":114564,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@agentic-research%2fdpop@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIDAncwcEPO8hRP/AJRzWejWMJXiXdbg4jt6UuT9VuVDxAiBTyT4EzlNmRU7znHFfdkuUcDLbd7tf9f5UUU3iO1oA5A=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:62d878d8-54f7-4863-9800-28b15d7826c8"}},"directories":{},"maintainers":[{"name":"jamestexas","email":"jamestexasgardner@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dpop_0.4.0_1785891527664_0.25305340527536857"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-23T16:49:49.106Z","modified":"2026-08-05T00:58:48.143Z","0.2.0":"2026-07-23T16:49:49.364Z","0.3.0":"2026-07-24T23:36:16.396Z","0.4.0":"2026-08-05T00:58:47.797Z"},"bugs":{"url":"https://github.com/agentic-research/notme/issues"},"license":"Apache-2.0","homepage":"https://github.com/agentic-research/notme/tree/main/packages/dpop","keywords":["dpop","rfc9449","oauth2","jwt","proof-of-possession","webcrypto"],"repository":{"type":"git","url":"git+https://github.com/agentic-research/notme.git","directory":"packages/dpop"},"description":"DPoP (RFC 9449) utilities and resource-server verifier SDK for notme-issued access tokens. Zero dependencies, pure Web Crypto — runs on Cloudflare Workers, Node, Deno, and browsers.","maintainers":[{"name":"jamestexas","email":"jamestexasgardner@gmail.com"}],"readme":"# `@agentic-research/dpop`\n\nVerify DPoP-bound access tokens ([RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449))\non a resource server. Built for services that accept tokens issued by notme\n(`auth.notme.bot`), including self-hosted deployments.\n\nIf you are protecting an API endpoint, the function you want is\n[`verifyDPoPToken`](#verify-a-dpop-bound-token). It takes the access token, the\nDPoP proof, and the request's method and URL, and either returns the verified\nclaims or throws a `DPoPVerificationError` with a stable `code`.\n\nZero runtime dependencies. Uses only Web Crypto (`crypto.subtle`), `fetch`, and\n`URL`, so it runs on Cloudflare Workers, Node, Deno, and browsers. Apache-2.0.\n\n## Install\n\n```bash\nnpm install @agentic-research/dpop\n```\n\n```bash\npnpm add @agentic-research/dpop\n```\n\n## Verify a DPoP-bound token\n\n```ts\nimport { verifyDPoPToken } from \"@agentic-research/dpop\";\n\nconst claims = await verifyDPoPToken({\n  token, // the access_token, without the \"DPoP \" prefix\n  proof, // the DPoP proof JWT from the request's DPoP header\n  method: request.method, // preserve the request method's case\n  url: request.url, // pass the full request URL\n  jwksUrl: \"https://auth.notme.bot/.well-known/jwks.json\",\n  audience: \"your-resource-server\", // required and non-empty\n  issuer: \"https://auth.notme.bot\",\n  checkAndRecordJti: (jti) => ledger.checkAndRecord(jti), // true when already recorded\n});\n\n// claims: { sub, scope, aud, exp, jti }\n```\n\n`audience` is required and enforced at runtime, not just in the types. A\nresource server that omits it would accept a token minted for a *different*\nresource server by the same issuer — a confused-deputy. An empty string or\nempty array throws `CONFIG_AUDIENCE_REQUIRED`.\n\n`issuer` is optional and unchecked by default, so the SDK works against\nself-hosted notme deployments on other domains. Set it if you know it.\n\n### `checkAndRecordJti` is the replay boundary\n\nA DPoP proof is single-use. This hook is the only thing that makes that true,\nand it must **atomically** check and record the proof's `jti` in durable,\nshared storage. Return `true` if the `jti` was already present; record it and\nreturn `false` otherwise. A read-then-write KV sequence is not atomic and two\nconcurrent replays can both observe \"not seen.\"\n\nThe hook is optional. Without it, nothing but the proof's ±60-second `iat`\nwindow bounds replay.\n\nThe verifier calls the hook **last**, only after every stateless token and\nproof check has passed, so a malformed or forged request cannot burn a\nlegitimate proof's `jti`.\n\n### What `verifyDPoPToken` checks\n\nIn order, throwing `DPoPVerificationError` on the first failure:\n\n1. **Access token** — three-part JWT, EdDSA signature against the JWKS key,\n   header `typ` pinned to `at+jwt`, and an `exp` claim present.\n2. **Token claims** — `exp`, `nbf`, `iat`, `iss`, `aud`, and a required `sub`,\n   via the shared `validateClaims`. Clock tolerance defaults to 60 seconds;\n   pass `clockTolerance: 0` to tighten it.\n3. **Proof** — header `typ` of `dpop+jwt`, an `alg` of `ES256` or `EdDSA`, an\n   embedded public `jwk`, and a valid signature. A proof JWK carrying private\n   members (`d`, `p`, `q`, …) is rejected.\n4. **Proof claims** — `jti` required; `iat` required and within ±60 seconds;\n   `htm` compared case-sensitively against `method`; `htu` compared against\n   `url` after normalization (query and fragment stripped, percent-encoding\n   canonicalized); `ath` required and matched against the base64url SHA-256 of\n   the exact token string.\n5. **Key binding** — the token's `cnf.jkt` must equal the RFC 7638 thumbprint\n   of the proof's JWK.\n6. **Replay** — `checkAndRecordJti`, when provided.\n\n## Handle errors by code\n\n```ts\nimport { DPoPVerificationError, verifyDPoPToken } from \"@agentic-research/dpop\";\n\ntry {\n  const claims = await verifyDPoPToken(options);\n} catch (error) {\n  if (error instanceof DPoPVerificationError) {\n    console.error(error.code, error.message);\n  }\n  throw error;\n}\n```\n\nMatch on `error.code`, never on the message. Every code is a member of the\nexported `VerifyErrorCode` union — `PROOF_REPLAY`, `CNF_JKT_MISMATCH`,\n`CLAIM_AUD_MISMATCH`, and so on. Messages are for humans and may be reworded.\n\n## Redirect-only Bearer tokens\n\nUse `verifyAccessToken` only for an unbound token received through a redirect\nflow, where the DPoP keypair was ephemeral and is gone by the time the token\narrives. It verifies the signature and claims but not a proof.\n\nIt **rejects** any token carrying a `cnf` claim with\n`BEARER_TOKEN_DPOP_BOUND`. Without that check, stripping the DPoP header from\na stolen bound token would downgrade it to a Bearer token that this function\nwould happily accept.\n\n## Cache the JWKS\n\nBoth verifiers accept an optional `kv` store and cache the fetched JWKS under\none key for one hour. The `KVLike` interface is just `get` and `put` — it is\ncompatible with Cloudflare KV without being coupled to it.\n\n```ts\nawait verifyDPoPToken({ ...options, kv: env.MY_KV });\n```\n\nPassing `publicKey` (an imported Ed25519 `CryptoKey`) instead skips the JWKS\nfetch entirely, which is useful in tests.\n\n## PKCE (RFC 7636)\n\n*Added in 0.4.0.*\n\nPKCE is a two-sided protocol: one party derives a challenge from a verifier,\nthe other recomputes it and compares. The two sides must agree byte for byte,\nand when they don't, the drift is invisible — a padded base64 or a different\nlength bound produces a challenge that never matches, and the only symptom is\nan opaque `invalid_grant`. These helpers are exported so both sides can run\nthe same function instead of two implementations that agree until they don't.\n\nOnly `S256` is supported. `plain`, where the verifier *is* the challenge,\nwould put the verifier in the URL and defeat the purpose.\n\n**Client** — starting an authorization-code flow:\n\n```ts\nimport { generateCodeVerifier, codeChallengeS256 } from \"@agentic-research/dpop\";\n\nconst verifier = generateCodeVerifier(); // 43 chars, 32 CSPRNG bytes as base64url\nconst challenge = await codeChallengeS256(verifier); // unpadded, per §4.2\n\n// Keep `verifier` server-side. Send only `challenge` in the authorize URL.\n```\n\n**Authority** — validating:\n\n```ts\nimport {\n  isValidCodeChallenge,\n  isValidCodeVerifier,\n  sha256Base64url,\n} from \"@agentic-research/dpop\";\n\n// Check the challenge where the flow STARTS, so a malformed one is a\n// diagnosable error here instead of an invalid_grant a round-trip later.\nif (!isValidCodeChallenge(challenge)) return badRequest();\n\n// Check the verifier's shape rather than assuming it. PKCE's security\n// argument is the verifier's entropy, so accepting a short one hands back a\n// working flow with none of the protection.\nif (!isValidCodeVerifier(verifier)) return badRequest();\n\nconst ok = (await sha256Base64url(verifier)) === storedChallenge;\n```\n\n`isValidCodeVerifier` enforces the §4.1 length range\n(`MIN_CODE_VERIFIER_LENGTH` 43 to `MAX_CODE_VERIFIER_LENGTH` 128, both\nexported) and the unreserved alphabet, rejecting `+`, `/`, and `=`.\n`isValidCodeChallenge` requires exactly 43 unreserved characters, the only\nlength a base64url-encoded 32-byte digest can have. Both are TypeScript type\nguards and both reject non-strings.\n\n`codeChallengeS256` is pinned in the test suite against the RFC 7636\nAppendix B vector rather than round-tripped through this package's own code —\na self-consistent but wrongly-encoded implementation passes a round-trip test\nand then fails against every real peer.\n\n`sha256Base64url` is also useful for storing an authorization code as a digest\nrather than in the clear: a code is a live credential until redeemed, so a\nread of your storage should not yield redeemable ones.\n\n<details>\n<summary>Full export surface</summary>\n\n**Verifiers** — `verifyDPoPToken`, `verifyAccessToken`\n\n**Errors** — `DPoPVerificationError`, `VerifyErrorCode` (type)\n\n**Types** — `VerifyDPoPTokenOptions`, `VerifyAccessTokenOptions`,\n`VerifiedTokenClaims`, `KVLike`, `ValidateClaimsOptions`\n\n**Claim validation** — `validateClaims`, for verifying JWT claims\n(`exp`/`nbf`/`iat`/`iss`/`aud`/`sub`) outside the two token verifiers.\n\n**Thumbprints** — `computeJwkThumbprint`, an RFC 7638 JWK thumbprint over\n`EC`, `RSA`, and `OKP` keys.\n\n**PKCE** — `generateCodeVerifier`, `codeChallengeS256`, `sha256Base64url`,\n`isValidCodeVerifier`, `isValidCodeChallenge`, `MIN_CODE_VERIFIER_LENGTH`,\n`MAX_CODE_VERIFIER_LENGTH`\n\n**Encoding** — `base64urlEncode`, `base64urlDecode`, `jsonParseSafe`\n\n</details>\n\n## Guides\n\n<!-- These links are pinned to the release TAG, not main, so a reader of a\n     published version sees the docs that shipped with it. npm renders only\n     README.md — never docs/*.mdx — so these links are the entire discovery\n     path from the package page. Bump the tag when cutting a release; the\n     `docs links pinned to release tag` test in scripts/ guards the drift. -->\n- [Verification](https://github.com/agentic-research/notme/blob/dpop-v0.4.0/packages/dpop/docs/verification.mdx)\n- [Replay protection](https://github.com/agentic-research/notme/blob/dpop-v0.4.0/packages/dpop/docs/replay-protection.mdx)\n- [Errors](https://github.com/agentic-research/notme/blob/dpop-v0.4.0/packages/dpop/docs/errors.mdx)\n- [Migrating to 0.3](https://github.com/agentic-research/notme/blob/dpop-v0.4.0/packages/dpop/docs/migration-0.3.mdx)\n\n## Breaking changes in 0.3\n\n- `seenJti` is renamed to `checkAndRecordJti` and must be atomic.\n- DPoP proofs require `ath`; `audience` must be non-empty.\n- Access-token clock tolerance defaults to 60 seconds (set `0` explicitly for none).\n- `htu` is normalized, `htm` remains case-sensitive, and errors expose stable codes.\n\n## License\n\nApache-2.0\n","readmeFilename":"README.md"}