{"_id":"@agentwares/web-bot-auth","_rev":"2-9281ffb831e3927e3c66f62b1feaff41","name":"@agentwares/web-bot-auth","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@agentwares/web-bot-auth","version":"0.1.0","keywords":["web-bot-auth","http-message-signatures","rfc9421","ed25519","cloudflare","verified-bots","signature-agent","ai-agents","jwks"],"license":"MIT","_id":"@agentwares/web-bot-auth@0.1.0","maintainers":[{"name":"umerbukhari","email":"umer.bukhari@gmail.com"}],"homepage":"https://github.com/agentwares/libs/tree/main/packages/web-bot-auth#readme","bugs":{"url":"https://github.com/agentwares/libs/issues"},"dist":{"shasum":"2bf21a644e7e51f629a18d0269119d90e8648cc5","tarball":"https://registry.npmjs.org/@agentwares/web-bot-auth/-/web-bot-auth-0.1.0.tgz","fileCount":6,"integrity":"sha512-2rjVITzM4Jv5yPBl8v3NLI7GvQYG7Q8MzFVLP71Xs/xJ25AnIojdgeEdsmGI4vviZDVcuXCVQtpGFiN9D9+qTA==","signatures":[{"sig":"MEYCIQDGTCY5GpMnQ9ipf7ZRuXANXLgh33BFhC3mqdKB2xZRhAIhAKeETVLIEWEKqzgTcQewFJQIG8b9wHk6SWSq2DFoQw7m","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":160593},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"8f18d0223118fa06ebd3669835a9e5a9acae6a45","scripts":{"lint":"eslint src","test":"vitest run","build":"tsup","keygen":"node scripts/generate-signing-key.mjs","typecheck":"tsc --noEmit"},"_npmUser":{"name":"umerbukhari","email":"umer.bukhari@gmail.com"},"repository":{"url":"git+https://github.com/agentwares/libs.git","type":"git","directory":"packages/web-bot-auth"},"_npmVersion":"10.9.8","description":"Web Bot Auth for agents: Ed25519 keys, a signed /.well-known/http-message-signatures-directory, and RFC 9421 HTTP Message Signatures on outbound requests, so Cloudflare can verify your bot. Web Crypto only, no Node built-ins.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/web-bot-auth_0.1.0_1789104363349_0.19038524962927217","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"_id":"@agentwares/web-bot-auth@0.1.1","bugs":{"url":"https://github.com/agentwares/libs/issues"},"dist":{"shasum":"972e95fb1274740421a4de8c8babe53a1e2864d5","tarball":"https://registry.npmjs.org/@agentwares/web-bot-auth/-/web-bot-auth-0.1.1.tgz","fileCount":6,"integrity":"sha512-7rWU3Gntp+Nvw4RBnFr+Pbf7oxkOEgNw6YJ+Gj+uNqwjNZL1t7t9acwUXqZSWVVGOMP1ZNFrDKe6ROWtmsV7ow==","signatures":[{"sig":"MEYCIQCoWldukVM1e5YUT8X1Q+XQuAx1caOWEPbYuk7jl4ktGAIhAOAgj3kohG+jqm4qEQiNuEZxJefeePMW7I73o28BBuOW","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFqvG8eBf1EwzcU0Sqo6o3CGJvllSQMdjHwJxUo4iT0BAiEAtsI18waqaWrZbyJ5K3+kBuWgIJnhkyXsQDpvND/km/4="}],"unpackedSize":162142},"main":"./dist/index.js","name":"@agentwares/web-bot-auth","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"4dd9da275ae7a375abe1aa6e8eaf6dafee17a118","license":"MIT","scripts":{"lint":"eslint src","test":"vitest run","build":"tsup","keygen":"node scripts/generate-signing-key.mjs","typecheck":"tsc --noEmit"},"version":"0.1.1","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:b09aa9e3-d5d1-4e56-9a7e-2e4a6954ba28"}},"homepage":"https://github.com/agentwares/libs/tree/main/packages/web-bot-auth#readme","keywords":["web-bot-auth","http-message-signatures","rfc9421","ed25519","cloudflare","verified-bots","signature-agent","ai-agents","jwks"],"repository":{"url":"git+https://github.com/agentwares/libs.git","type":"git","directory":"packages/web-bot-auth"},"_npmVersion":"12.0.2","description":"Web Bot Auth for agents: Ed25519 keys, a signed /.well-known/http-message-signatures-directory, and RFC 9421 HTTP Message Signatures on outbound requests, so Cloudflare can verify your bot. Web Crypto only, no Node built-ins.","directories":{},"maintainers":[{"name":"umerbukhari","email":"umer.bukhari@gmail.com"}],"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/web-bot-auth_0.1.1_1789115068542_0.6134417185214427"}}},"time":{"created":"2026-09-11T05:26:03.086Z","modified":"2026-09-11T08:24:28.796Z","0.1.0":"2026-09-11T05:26:03.483Z","0.1.1":"2026-09-11T08:24:28.631Z"},"bugs":{"url":"https://github.com/agentwares/libs/issues"},"license":"MIT","homepage":"https://github.com/agentwares/libs/tree/main/packages/web-bot-auth#readme","keywords":["web-bot-auth","http-message-signatures","rfc9421","ed25519","cloudflare","verified-bots","signature-agent","ai-agents","jwks"],"repository":{"url":"git+https://github.com/agentwares/libs.git","type":"git","directory":"packages/web-bot-auth"},"description":"Web Bot Auth for agents: Ed25519 keys, a signed /.well-known/http-message-signatures-directory, and RFC 9421 HTTP Message Signatures on outbound requests, so Cloudflare can verify your bot. Web Crypto only, no Node built-ins.","maintainers":[{"name":"umerbukhari","email":"umer.bukhari@gmail.com"}],"readme":"# @agentwares/web-bot-auth\n\nSign your agent's outbound HTTP requests with Ed25519 so a verifier can tell who is calling,\nand serve the signed key directory that publishes the key. This is the mechanism behind\nCloudflare's Verified Bots programme: a fetcher that signs is identified, and a fetcher that\ndoes not is guessed at from its IP and user agent.\n\nWeb Crypto and the Fetch API only — no Node built-ins on the signing path — so it runs\nunchanged on Vercel Functions, Cloudflare Workers, Deno and Node 20+.\n\n```sh\nnpm install @agentwares/web-bot-auth\n```\n\n## Sign a request\n\n```ts\nimport { importSigningKey, signRequest, attachSignature } from \"@agentwares/web-bot-auth\";\n\nconst key = await importSigningKey(JSON.parse(process.env.WEB_BOT_AUTH_PRIVATE_JWK!));\n\nconst request = new Request(\"https://example.com/page\");\nconst headers = await signRequest(request, key, { directory: \"https://bot.example\" });\n\nconst response = await fetch(attachSignature(request, headers));\n```\n\n`headers` is the three fields a signed request carries:\n\n```http\nSignature-Agent: \"https://bot.example\"\nSignature-Input: sig1=(\"@authority\" \"signature-agent\");created=1757520000\n ;keyid=\"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U\";alg=\"ed25519\"\n ;expires=1757520060;nonce=\"…\";tag=\"web-bot-auth\"\nSignature: sig1=:…:\n```\n\nThe default window is 60 seconds, which is Cloudflare's recommended bound on replay. Cover\nmore than the authority when you can — `alsoCover: [{ name: \"@method\" }, { name: \"@path\" }]`\nnarrows a signature that would otherwise be reusable against any path on that origin until\nit expires.\n\n## Serve the key directory\n\nThe directory is a JWKS at a fixed path, and it signs itself: one signature per published\nkey, covering the `@authority` of the request that fetched it. That possession proof is what\nstops someone re-serving your key set under their domain and registering as you.\n\n```ts\nimport { directoryHandler } from \"@agentwares/web-bot-auth\";\n\nexport const GET = directoryHandler([key]); // mount at the well-known path\n```\n\nIt answers with `Content-Type: application/http-message-signatures-directory+json`, a\n`Content-Digest`, and `Signature` / `Signature-Input` tagged\n`http-message-signatures-directory`. It must be generated per request, because `@authority`\ncomes from the request.\n\n**The URL you register with Cloudflare** is the origin plus the well-known path, over HTTPS,\nwith no query and no redirect:\n\n```\nhttps://<your-host>/.well-known/http-message-signatures-directory\n```\n\n`directoryUrl(\"https://bot.example\")` builds it. The `Signature-Agent` header carries the\n_origin_ (`\"https://bot.example\"`); the verifier appends the well-known path itself.\n\n## Verify\n\n```ts\nimport { verifyRequest, verifyDirectoryResponse } from \"@agentwares/web-bot-auth\";\n\nconst result = await verifyRequest(request, { keys: publishedJwks });\nif (result.ok) console.log(result.keyid, result.signatureAgent);\nelse console.log(result.code, result.cause, result.fix);\n```\n\n`verifyRequest` never throws on hostile input — a malformed field, an unusable key or a\nreplayed signature all come back as a `code` / `cause` / `fix` failure. It rebuilds the\nsignature base from the message it actually received, so a signature only passes if every\ncovered component survived the trip byte for byte.\n\n`verifyDirectoryResponse(request, response)` checks a directory's possession proofs and\nreturns which published thumbprints proved possession.\n\n## Generate a key\n\n```sh\npnpm --filter @agentwares/web-bot-auth build\npnpm --filter @agentwares/web-bot-auth keygen\n```\n\nWrites the private JWK to `~/code/agentwares-secrets/web-bot-auth.key` at mode `0600`, the\npublic JWK and the thumbprint beside it, and prints only the thumbprint. It refuses to\noverwrite an existing key, because a key registered with Cloudflare cannot be silently\nreplaced — rotation means publishing both and retiring the old one.\n\n**The private key never goes in an environment variable, a commit, or a log.** Deployments\nneed the public JWK, which is served to the entire internet anyway.\n\n## Which spec this implements\n\nBuilt from, and tested against, the published test vectors in:\n\n- **RFC 9421**, HTTP Message Signatures (February 2024) — signature base construction,\n  `@signature-params`, structured-field serialization.\n- **draft-ietf-webbotauth-httpsig-protocol-00** (1 September 2026) — the Web Bot Auth\n  working-group draft: `Signature-Agent`, the `web-bot-auth` tag, the JWKS directory format,\n  the well-known URI, and the Appendix B directory possession proof. It is a draft; it\n  expires 5 March 2027 and the header syntax has already changed once (see below).\n- **RFC 7638** / **RFC 8037 Appendix A.3** — the JWK thumbprint used as `keyid`.\n- **Cloudflare's Web Bot Auth documentation**, which is what actually gates the Verified\n  Bots programme today.\n\nThe Ed25519 vectors from Appendix E.2 of the draft and Appendix B.2.6 of RFC 9421 are\nasserted byte for byte in the test suite: Ed25519 is deterministic, so a correct\nimplementation reproduces the published signature exactly, not merely one that verifies.\n`signRequest` reproduces the example in Cloudflare's own documentation on the nose.\n\n### Where Cloudflare and the current draft disagree\n\nCloudflare implements the older `draft-meunier-http-message-signatures-directory-03` /\n`-web-bot-auth-architecture-02`. Two things have changed since, and the defaults here follow\nCloudflare, because Cloudflare is the verifier that decides whether your bot gets through.\n\n|                            | Cloudflare today (the default)                                     | draft-ietf-webbotauth-httpsig-protocol-00                                           |\n| -------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |\n| `Signature-Agent`          | sf-string: `\"https://bot.example\"`, covered as `\"signature-agent\"` | Dictionary: `sig1=\"https://bot.example\"`, covered as `\"signature-agent\";key=\"sig1\"` |\n| Directory signature covers | `(\"@authority\";req)`                                               | `(\"@authority\";req \"content-digest\")`                                               |\n\nCloudflare's docs are explicit that it **fails** verification for the dictionary form. Pass\n`signatureAgentForm: \"dictionary\"` and `profile: \"ietf-draft\"` when you are talking to a\nverifier that has moved on. Both forms are covered by the vectors and by round-trip tests.\n\nCloudflare also rejects the `sf`, `bs`, `key` and `req` component parameters on _request_\nsignatures, and the `@query-param` and `@status` components. This library refuses `sf` and\n`bs` outright; the rest are available and simply should not be sent to Cloudflare.\n\nRe-check all of this before you deploy. It moved between this package's first and second\nparagraph of research, and it will move again.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}