{"_id":"@craigruks/x402-server-guard","_rev":"2-be03fabb4f1dbbb1943db3c62f2f72cf","name":"@craigruks/x402-server-guard","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@craigruks/x402-server-guard","version":"0.1.0","keywords":["x402","payments","security","middleware","hardening","replay-protection","agentic-payments","web3"],"author":{"name":"Craig Ruks"},"license":"MIT","_id":"@craigruks/x402-server-guard@0.1.0","maintainers":[{"name":"craigruks","email":"craig@craigruks.com"}],"homepage":"https://github.com/craigruks/x402-server-guard#readme","bugs":{"url":"https://github.com/craigruks/x402-server-guard/issues"},"dist":{"shasum":"6260157f74e2ffc2e00b3208b353317747a446e9","tarball":"https://registry.npmjs.org/@craigruks/x402-server-guard/-/x402-server-guard-0.1.0.tgz","fileCount":54,"integrity":"sha512-XrCz2Wgytn1jf776ulWQWgTPkCE5b7htSBRZfiS3oyz5CnrBjgnh15iwroTRFEb6MQQ3AEQlal7+UKweUkl+OA==","signatures":[{"sig":"MEUCIQCGLfoD+bXVvQrFJVLTozEN5O65CB+xQABE3eNAwJjlDAIgT1rEDIdVopY4MojpYaaRv9g/erB1RiSgumwRcLtT3G0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":112820},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./cloudflare":{"types":"./dist/cloudflare/durable-object.d.ts","default":"./dist/cloudflare/durable-object.js"},"./package.json":"./package.json"},"gitHead":"7f60c4530dcb70f9917328d083ec7b135f902b07","scripts":{"lint":"biome check .","test":"vitest run","build":"tsc -p tsconfig.build.json && tsc -p tsconfig.cloudflare.json","check":"npm run typecheck && npm run lint && npm run lint:filelength && npm run test","example":"tsx examples/secure-flow.ts","prepare":"git config core.hooksPath .githooks || true","version":"node scripts/sync-version.mjs && git add src/index.ts","lint:fix":"biome check --write .","check:pkg":"publint --strict && attw --pack . --profile esm-only","typecheck":"tsc -p tsconfig.json && tsc -p tsconfig.cloudflare.json --noEmit","smoke:dist":"npm run build && node scripts/smoke-dist.mjs","test:watch":"vitest","example:hono":"tsx examples/hono-server.ts","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build && npm run check:pkg","lint:filelength":"tsx scripts/lint-file-length.ts"},"_npmUser":{"name":"craigruks","email":"craig@craigruks.com"},"repository":{"url":"git+https://github.com/craigruks/x402-server-guard.git","type":"git"},"_npmVersion":"11.16.0","description":"Server-side hardening middleware for x402 payment endpoints. Mitigations for settlement races, replay, cross-resource substitution, and cache leakage. Not a security guarantee.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","allowScripts":{"esbuild@0.28.1":true,"workerd@1.20260708.1":true},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsx":"4.23.0","hono":"4.12.29","vitest":"4.1.10","esbuild":"0.28.1","publint":"0.3.21","miniflare":"4.20260708.1","@x402/core":"2.18.0","typescript":"7.0.2","@types/node":"26.1.1","@biomejs/biome":"2.5.3","@vitest/coverage-v8":"4.1.10","@arethetypeswrong/cli":"0.18.5","@cloudflare/workers-types":"5.20260714.1"},"_npmOperationalInternal":{"tmp":"tmp/x402-server-guard_0.1.0_1784004050343_0.9594728387867046","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@craigruks/x402-server-guard","version":"0.1.1","description":"Server-side hardening middleware for x402 payment endpoints. Mitigations for settlement races, replay, cross-resource substitution, and cache leakage. Not a security guarantee.","keywords":["x402","payments","security","middleware","hardening","replay-protection","agentic-payments","web3"],"license":"MIT","author":{"name":"Craig Ruks"},"type":"module","repository":{"type":"git","url":"git+https://github.com/craigruks/x402-server-guard.git"},"homepage":"https://craigruks.github.io/x402-server-guard/","bugs":{"url":"https://github.com/craigruks/x402-server-guard/issues"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./cloudflare":{"types":"./dist/cloudflare/durable-object.d.ts","default":"./dist/cloudflare/durable-object.js"},"./package.json":"./package.json"},"main":"./dist/index.js","types":"./dist/index.d.ts","sideEffects":false,"engines":{"node":">=22"},"publishConfig":{"access":"public","provenance":true},"scripts":{"build":"tsc -p tsconfig.build.json && tsc -p tsconfig.cloudflare.json","version":"node scripts/sync-version.mjs && git add src/index.ts","typecheck":"tsc -p tsconfig.json && tsc -p tsconfig.cloudflare.json --noEmit","lint":"biome check .","lint:fix":"biome check --write .","lint:filelength":"tsx scripts/lint-file-length.ts","test":"vitest run","test:watch":"vitest","check":"npm run typecheck && npm run lint && npm run lint:filelength && npm run test","check:pkg":"publint --strict && attw --pack . --profile esm-only","prepublishOnly":"npm run build && npm run check:pkg","prepare":"git config core.hooksPath .githooks || true","test:coverage":"vitest run --coverage","smoke:dist":"npm run build && node scripts/smoke-dist.mjs","example":"tsx examples/secure-flow.ts","example:hono":"tsx examples/hono-server.ts"},"devDependencies":{"@arethetypeswrong/cli":"0.18.5","@biomejs/biome":"2.5.4","@cloudflare/workers-types":"5.20260718.1","@types/node":"26.1.1","@vitest/coverage-v8":"4.1.10","@x402/core":"2.19.0","esbuild":"0.28.1","hono":"4.12.30","miniflare":"4.20260722.0","publint":"0.3.21","tsx":"4.23.1","typescript":"7.0.2","vitest":"4.1.10"},"allowScripts":{"esbuild@0.28.1":true,"workerd@1.20260708.1":true},"gitHead":"4b20b4e279ceaaf0b419143ec7c83eabb516cb6a","_id":"@craigruks/x402-server-guard@0.1.1","_nodeVersion":"22.23.1","_npmVersion":"12.0.1","dist":{"integrity":"sha512-VvkxlVa5lcHCX5dVUPwEIkvMgIS9QD+TYbiT10rrCyIhWb7wsl3cB35a18hDluXcy2V+00bWWcTvIK7hfWrblA==","shasum":"b65b61af163cc9eba8ff1848599b01c3b623fcd7","tarball":"https://registry.npmjs.org/@craigruks/x402-server-guard/-/x402-server-guard-0.1.1.tgz","fileCount":54,"unpackedSize":114212,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@craigruks%2fx402-server-guard@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHb/lQRuVh9qyumt+hjk9bWZuFm/Ju/VedHfh02i1LHhAiBQi+B0bzqXRLPWL7NRn0qZgSO9zYjDXNnDHSHcM3bvPg=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:83605b87-ff09-40e2-9eb9-7387f334e8a0"}},"directories":{},"maintainers":[{"name":"craigruks","email":"craig@craigruks.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/x402-server-guard_0.1.1_1784886592207_0.7425495478465012"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-14T04:40:50.187Z","modified":"2026-07-24T09:49:52.650Z","0.1.0":"2026-07-14T04:40:50.478Z","0.1.1":"2026-07-24T09:49:52.343Z"},"bugs":{"url":"https://github.com/craigruks/x402-server-guard/issues"},"author":{"name":"Craig Ruks"},"license":"MIT","homepage":"https://craigruks.github.io/x402-server-guard/","keywords":["x402","payments","security","middleware","hardening","replay-protection","agentic-payments","web3"],"repository":{"type":"git","url":"git+https://github.com/craigruks/x402-server-guard.git"},"description":"Server-side hardening middleware for x402 payment endpoints. Mitigations for settlement races, replay, cross-resource substitution, and cache leakage. Not a security guarantee.","maintainers":[{"name":"craigruks","email":"craig@craigruks.com"}],"readme":"# x402-server-guard\n\n**Server-side hardening middleware for [x402](https://github.com/coinbase/x402)\npayment endpoints.**\n\n[![CI](https://github.com/craigruks/x402-server-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/craigruks/x402-server-guard/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)\n\nDocumentation: https://craigruks.github.io/x402-server-guard/\n\nx402 lets a server charge for a resource by returning `402 Payment Required` and\nverifying a signed payment. Published research has shown that a naïve resource\nserver is exploitable in several ways: a payment can be replayed, reused against\na different resource, raced to duplicate delivery before settlement confirms, or\nleaked to unpaid clients through a shared cache. This library is the enforcement\nlayer a merchant wraps their endpoint in to close those gaps.\n\n> [!WARNING]\n> **Status: early, pre-1.0.** All four enumerated attack classes below have a\n> mitigation implemented. It is **not audited** and is **not a security\n> guarantee**; it mitigates these specific classes only and cannot make an insecure\n> endpoint safe on its own. See the mitigation table for scope.\n\n## Security disclaimer\n\nThis software is provided **\"AS IS\"**, without warranty of any kind. It is **not\naudited** and is **not a security guarantee**. It mitigates specific, enumerated\nattack classes only. It cannot make an insecure payment endpoint safe on its own.\n**The authors accept no liability for any loss of funds or damages.** See\n[SECURITY.md](./SECURITY.md) and the [LICENSE](./LICENSE).\n\n## Install\n\n```sh\nnpm install @craigruks/x402-server-guard\n```\n\nNode ≥ 22. Zero runtime dependencies. Ships ESM with type declarations; a\nTypeScript or JavaScript consumer both import the same build.\n\n## Usage\n\nReserve a payment's nonce through the guard before you grant the resource. The\nfirst request for a nonce wins; a replay or a concurrent race is denied. The\ndecision is a value, never a throw, so a stray `try/catch` cannot turn a deny\ninto an accidental grant.\n\n```ts\nimport { createGuard } from \"@craigruks/x402-server-guard\";\n\nconst guard = createGuard();\n\n// Inside your paid handler, after the facilitator verifies the payment:\nconst reservation = await guard.reserve({\n  nonce: authorization.nonce, // the EIP-3009 nonce\n  resource: request.url, // which resource this payment is for\n  expiresAt: Number(authorization.validBefore), // unix seconds\n});\n\nif (!reservation.reserved) {\n  return deny(reservation.reason.code); // e.g. \"nonce-already-reserved\"\n}\n\n// Settle before granting: a payment that fails to settle yields no resource.\nconst settled = await facilitator.settle(payload, requirements);\nif (!settled.success) {\n  return deny(\"settle failed\");\n}\n\nreturn grant(resource);\n```\n\n> [!WARNING]\n> `createGuard()` with no store uses an in-memory store that protects **one process\n> only**. On Cloudflare Workers, Vercel, AWS Lambda, or any autoscaled fleet, each\n> isolate holds its own map, so replay and race protection do not hold across\n> instances. For those deploys, pass a store backed by an atomic compare-and-set: a\n> Durable Object, Redis `SET NX`, or a database unique constraint (not a plain\n> get-then-put store like Workers KV, which reopens the race condition). Any backend that\n> implements the `NonceStore` contract works. One is built start to finish in the box, a\n> Cloudflare Durable Object adapter:\n> `import { createDurableObjectNonceStore } from \"@craigruks/x402-server-guard/cloudflare\"`\n> (see [the deployment guide](https://craigruks.github.io/x402-server-guard/deployment/cloudflare-durable-objects/)).\n> The atomic compare-and-set contract for other backends is in\n> [`docs/hardening.md`](./docs/hardening.md).\n\n### One call: `protect`\n\n`protect` runs the whole secure flow (`reserve → settle → (confirm) → deliver`) and\nreturns the cache directives on grant, releasing the reservation if the settle\nfails or finality is not reached. It has no runtime dependencies and takes plain\ncallbacks, so it drops into any framework:\n\n```ts\nimport { protect } from \"@craigruks/x402-server-guard\";\n\n// After the facilitator verifies the payment:\nconst decision = await protect(\n  guard,\n  { nonce, resource: request.url, expiresAt: Number(authorization.validBefore) },\n  {\n    // settle resolves true only when the payment actually settled.\n    settle: async () => (await facilitator.settle(payload, requirements)).success,\n    deliver: () => resource,\n    // Grant on settle success (finality rests with the facilitator and the chain).\n    // Use `finality: \"confirm\"` with a `confirm()` callback to hold for k confirmations.\n    finality: \"facilitator\",\n  },\n);\nif (!decision.granted) return deny(decision.reason.code);\nresponse.headers.set(\"Cache-Control\", decision.cacheControl);\nreturn grant(decision.resource);\n```\n\nTwo runnable examples: [`examples/secure-flow.ts`](./examples/secure-flow.ts)\n(the concurrent race, blocked) and [`examples/hono-server.ts`](./examples/hono-server.ts)\n(a Hono route protected end to end). Bind the nonce to the **served** route (the\nrequest URL), not the payload's claimed resource, which is why the binding lives at\nthe framework layer.\n\nAll four enumerated attack classes are covered; see the table below.\n\n## Design principles\n\n- **Zero runtime dependencies.** The core uses only the Web Platform `crypto` global\n  (`crypto.randomUUID`), present on Node 22+, Cloudflare Workers, and Deno, so the guard\n  runs in any modern runtime without a polyfill. Every dependency is attack surface; a\n  hardening library should have as little of it as possible. Independent signature\n  verification would need cryptographic primitives, so it stays out of the core path by\n  design; the facilitator verifies payments, and the guard hardens the flow around that.\n- **Installs clean under npm v12's hardened defaults**: no lifecycle scripts, no\n  `npm approve-scripts` step, nothing to allow.\n- **Small enough to read.** Source files are capped so the whole library can be\n  audited in an afternoon. Built with plain `tsc` so the published output maps\n  one-to-one to the source you can see.\n- **Framework-agnostic core.** `protect` takes plain callbacks, so the same guard\n  drops into Hono, Express, Next, or Fastify. A Hono binding is shown in the\n  examples; an `@x402/core`-hook convenience wrapper is a thin layer over `protect`.\n\n## Taking the dependency (or not)\n\nIf you run a payment endpoint, you are right to want as few dependencies as possible.\nThis library has none at runtime, and the core is small enough to read in one sitting. If\nyou would rather not add a dependency at all, lift the primitives you use straight into\nyour own code: the reservation, the finality hold, and the cache directives are each a\nsmall, self-contained file, and a coding agent can pull over the parts you need in a few\nminutes. That is a supported way to use this.\n\nIf you do take the dependency, we have tried to make depending on us safe. Releases\npublish through GitHub Actions using npm trusted publishing (OIDC), so no long-lived npm\ntoken exists for an attacker to steal; tokens are disallowed on the package, publishing\nrequires two-factor auth, and CI-published releases carry build provenance that ties the\npackage to the exact commit and workflow that built it.\n\n## Mitigations\n\nEach ships with a paired test proving the attack against a vanilla server and\nproving it blocked by the guard. Every class is mapped to its research, mechanism,\nand proving test in [`docs/coverage-map.md`](./docs/coverage-map.md); the rationale\nis in [`docs/hardening.md`](./docs/hardening.md). The hardest questions about scope\nand honesty (is this a strawman, does the reference actually have these gaps, what\nthis does not do) are answered in\n[`docs/objection-handling.md`](./docs/objection-handling.md).\n\n| Attack class | Status |\n| --- | --- |\n| Duplicate-settlement race | done |\n| Payment replay | done (same nonce reservation) |\n| Cross-resource substitution | done (same nonce reservation, distinct reason) |\n| Grant-before-finality (k-confirmations) | done |\n| Cache leakage of paid content | done |\n\n## Development\n\nThe local toolchain is pinned with [mise](https://mise.jdx.dev):\n\n```sh\nmise install   # installs Node (Active LTS) + just from mise.toml\nnpm ci         # installs the dev toolchain, exact-pinned\njust           # lists every repo command\njust check     # full local gate: typecheck + lint + file-length + tests\n```\n\nNode 24 (Active LTS) is used locally; the published package supports Node ≥22,\nand CI tests both. `npm run build` emits the package with plain `tsc`.\n\nDev tooling lives in `package.json` scripts (`npm run …`); the [`justfile`](./justfile)\nis the discoverable index for repo operations that aren't npm: supply-chain\nchecks, CI, release prep. Run `just` to see them all.\n\n## Acknowledgments\n\nShengchen Ling, an author of \"Free-Riding the Agentic Web: A Systematic Security\nAnalysis of x402 Payments\" ([arXiv:2605.30998](https://arxiv.org/abs/2605.30998)),\nreviewed how this library maps to the paper's flaw classes and invariants and\nconfirmed it aligns with the paper's intended interpretation. This is not an\naudit or endorsement of the implementation.\n\n## License\n\n[MIT](./LICENSE) © Craig Ruks\n","readmeFilename":"README.md"}