@craigruks/x402-server-guard 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # x402-server-guard
2
2
 
3
- **Server-side hardening middleware for [x402](https://github.com/coinbase/x402)
3
+ **Server-side hardening middleware for [x402](https://github.com/x402-foundation/x402)
4
4
  payment endpoints.**
5
5
 
6
+ [![npm](https://img.shields.io/npm/v/@craigruks/x402-server-guard)](https://www.npmjs.com/package/@craigruks/x402-server-guard)
6
7
  [![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)
7
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
8
9
 
@@ -76,7 +77,7 @@ return grant(resource);
76
77
  > isolate holds its own map, so replay and race protection do not hold across
77
78
  > instances. For those deploys, pass a store backed by an atomic compare-and-set: a
78
79
  > Durable Object, Redis `SET NX`, or a database unique constraint (not a plain
79
- > get-then-put store like Workers KV, which reopens the race). Any backend that
80
+ > get-then-put store like Workers KV, which reopens the race condition). Any backend that
80
81
  > implements the `NonceStore` contract works. One is built start to finish in the box, a
81
82
  > Cloudflare Durable Object adapter:
82
83
  > `import { createDurableObjectNonceStore } from "@craigruks/x402-server-guard/cloudflare"`
@@ -137,6 +138,21 @@ All four enumerated attack classes are covered; see the table below.
137
138
  drops into Hono, Express, Next, or Fastify. A Hono binding is shown in the
138
139
  examples; an `@x402/core`-hook convenience wrapper is a thin layer over `protect`.
139
140
 
141
+ ## Taking the dependency (or not)
142
+
143
+ If you run a payment endpoint, you are right to want as few dependencies as possible.
144
+ This library has none at runtime, and the core is small enough to read in one sitting. If
145
+ you would rather not add a dependency at all, lift the primitives you use straight into
146
+ your own code: the reservation, the finality hold, and the cache directives are each a
147
+ small, self-contained file, and a coding agent can pull over the parts you need in a few
148
+ minutes. That is a supported way to use this.
149
+
150
+ If you do take the dependency, we have tried to make depending on us safe. Releases
151
+ publish through GitHub Actions using npm trusted publishing (OIDC), so no long-lived npm
152
+ token exists for an attacker to steal; tokens are disallowed on the package, publishing
153
+ requires two-factor auth, and CI-published releases carry build provenance that ties the
154
+ package to the exact commit and workflow that built it.
155
+
140
156
  ## Mitigations
141
157
 
142
158
  Each ships with a paired test proving the attack against a vanilla server and
@@ -147,13 +163,17 @@ and honesty (is this a strawman, does the reference actually have these gaps, wh
147
163
  this does not do) are answered in
148
164
  [`docs/objection-handling.md`](./docs/objection-handling.md).
149
165
 
150
- | Attack class | Status |
151
- | --- | --- |
152
- | Duplicate-settlement race | done |
153
- | Payment replay | done (same nonce reservation) |
154
- | Cross-resource substitution | done (same nonce reservation, distinct reason) |
155
- | Grant-before-finality (k-confirmations) | done |
156
- | Cache leakage of paid content | done |
166
+ | Attack class | Status | Upstream `@x402/core` 2.29.0 |
167
+ | --- | --- | --- |
168
+ | Duplicate-settlement race | done | partly: closed by the opt-in `upfront` flow ([#3053](https://github.com/x402-foundation/x402/pull/3053)), open in the default flow |
169
+ | Payment replay | done (same nonce reservation) | partly, as for the race |
170
+ | Cross-resource substitution | done (same nonce reservation, distinct reason) | partly: `upfront` denies the reuse, nothing binds or flags the resource |
171
+ | Grant-before-finality (k-confirmations) | done | no: success is one inclusion; a hook can now fail it closed ([#3994](https://github.com/x402-foundation/x402/pull/3994)) |
172
+ | Cache leakage of paid content | done | partly: `private` on the paid 200 since 2.21.0 ([#2990](https://github.com/x402-foundation/x402/pull/2990)), no `no-store`, no `Vary` |
173
+
174
+ The upstream column is a survey, not a dependency: the guard does not call
175
+ `@x402/core`. Evidence for each entry is in
176
+ [`docs/hardening.md`](./docs/hardening.md#what-upstream-now-covers).
157
177
 
158
178
  ## Development
159
179
 
@@ -173,6 +193,14 @@ Dev tooling lives in `package.json` scripts (`npm run …`); the [`justfile`](./
173
193
  is the discoverable index for repo operations that aren't npm: supply-chain
174
194
  checks, CI, release prep. Run `just` to see them all.
175
195
 
196
+ ## Acknowledgments
197
+
198
+ Shengchen Ling, an author of "Free-Riding the Agentic Web: A Systematic Security
199
+ Analysis of x402 Payments" ([arXiv:2605.30998](https://arxiv.org/abs/2605.30998)),
200
+ reviewed how this library maps to the paper's flaw classes and invariants and
201
+ confirmed it aligns with the paper's intended interpretation. This is not an
202
+ audit or endorsement of the implementation.
203
+
176
204
  ## License
177
205
 
178
206
  [MIT](./LICENSE) © Craig Ruks
package/SECURITY.md CHANGED
@@ -47,7 +47,9 @@ published under a separate tag.
47
47
 
48
48
  Please report suspected vulnerabilities **privately** via GitHub Security
49
49
  Advisories ("Report a vulnerability" on the repository's Security tab), not
50
- through public issues.
50
+ through public issues. This covers any case where the guard fails to stop an
51
+ attack it claims to mitigate. A public issue that describes a working attack
52
+ discloses it before a fix exists.
51
53
 
52
54
  Include a description, affected version, and a reproduction if possible. Expect an
53
55
  acknowledgement within 72 hours. Please allow a reasonable window for a fix before
package/dist/cache.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Cache directives for paid responses.
3
3
  *
4
- * A shared cache (CDN or reverse proxy) keys on the URL and knows nothing about
4
+ * A shared cache (CDN or reverse proxy) is keyed on the URL and knows nothing about
5
5
  * payment, so a cacheable paid 200 can be stored and served to the next caller, paid
6
- * or not. The reference x402 adapters set no cache directive, leaving that door open.
6
+ * or not. The reference x402 adapters send `private` on the paid 200 as of
7
+ * `@x402/core` 2.21.0 (unchanged through 2.29.0), and set no directive at all before
8
+ * it; neither sets `Vary`.
7
9
  *
8
10
  * The mitigation is a response directive, not a decision about a nonce, so it lives
9
11
  * here as a small pure helper the server or adapter applies to every paid response.
@@ -20,7 +22,9 @@ export interface CacheDirectives {
20
22
  export interface CacheDirectivesOptions {
21
23
  /**
22
24
  * The request header the payment travels in, added to `Vary` so a cache that
23
- * stores despite `no-store` still keys by payer. Defaults to `X-PAYMENT`.
25
+ * stores despite `no-store` still keys by payer. Defaults to `X-PAYMENT`, the x402
26
+ * v1 header; an x402 v2 client sends `PAYMENT-SIGNATURE`, so pass that behind a v2
27
+ * server.
24
28
  */
25
29
  readonly paymentHeader?: string;
26
30
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,wFAAwF;AACxF,MAAM,WAAW,eAAe;IAC9B,6BAA6B;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,+DAA+D;AAC/D,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAGjG;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMjF"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,wFAAwF;AACxF,MAAM,WAAW,eAAe;IAC9B,6BAA6B;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,+DAA+D;AAC/D,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAGjG;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMjF"}
package/dist/cache.js CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Cache directives for paid responses.
3
3
  *
4
- * A shared cache (CDN or reverse proxy) keys on the URL and knows nothing about
4
+ * A shared cache (CDN or reverse proxy) is keyed on the URL and knows nothing about
5
5
  * payment, so a cacheable paid 200 can be stored and served to the next caller, paid
6
- * or not. The reference x402 adapters set no cache directive, leaving that door open.
6
+ * or not. The reference x402 adapters send `private` on the paid 200 as of
7
+ * `@x402/core` 2.21.0 (unchanged through 2.29.0), and set no directive at all before
8
+ * it; neither sets `Vary`.
7
9
  *
8
10
  * The mitigation is a response directive, not a decision about a nonce, so it lives
9
11
  * here as a small pure helper the server or adapter applies to every paid response.
package/dist/cache.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAkBH,+DAA+D;AAC/D,MAAM,UAAU,2BAA2B,CAAC,OAAO,GAA2B,EAAE;IAC9E,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,WAAW,CAAC;IAC3D,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;AACpE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,YAAgC;IACtE,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,KAAK,EAAE,EAAE,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,UAAU,GAAG,YAAY,CAAC,WAAW,EAAE,CAAC;IAC9C,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;AAC7E,CAAC"}
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAoBH,+DAA+D;AAC/D,MAAM,UAAU,2BAA2B,CAAC,OAAO,GAA2B,EAAE;IAC9E,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,WAAW,CAAC;IAC3D,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;AACpE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,YAAgC;IACtE,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,KAAK,EAAE,EAAE,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,UAAU,GAAG,YAAY,CAAC,WAAW,EAAE,CAAC;IAC9C,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;AAC7E,CAAC"}
package/dist/guard.js CHANGED
@@ -18,7 +18,7 @@ export function createGuard(options = {}) {
18
18
  const canonicalizeResource = options.canonicalizeResource ?? canonicalResource;
19
19
  return {
20
20
  async reserve(params) {
21
- // Fold both keys to canonical form up front; everything below keys on the
21
+ // Fold both keys to canonical form up front; everything below is keyed on the
22
22
  // canonical values (see canonical.ts for why two encodings must not split).
23
23
  const nonce = canonicalizeNonce(params.nonce);
24
24
  const resource = canonicalizeResource(params.resource);
package/dist/guard.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * race and payment replay is the first mitigation to land.
13
13
  */
14
14
  /** Current package version (semver). */
15
- export const VERSION = "0.1.0";
15
+ export const VERSION = "0.1.2";
16
16
  export { isStorableBySharedCache, paidResponseCacheDirectives, } from "./cache.js";
17
17
  export { canonicalNonce, canonicalResource } from "./canonical.js";
18
18
  export { guardError } from "./error.js";
@@ -5,10 +5,10 @@
5
5
  * are never split by an `await`. The in-memory store below is atomic by construction
6
6
  * (a synchronous body runs to completion); a distributed store must use a genuine
7
7
  * atomic compare-and-set (a Durable Object, Redis `SET .. NX`, a unique constraint).
8
- * Get-then-put stores (Workers KV, S3) reopen the race and are not safe here.
8
+ * Get-then-put stores (Workers KV, S3) reopen the race condition and are not safe here.
9
9
  *
10
10
  * `reserve` and `release` return a `Result`, so a store failure is a value the guard
11
- * turns into a fail-closed deny. Replay keys on the nonce, never the signature, which
11
+ * turns into a fail-closed deny. Replay is keyed on the nonce, never the signature, which
12
12
  * is what makes it immune to signature malleability. Full rationale: docs/hardening.md.
13
13
  * The contract itself lives in `store-types.ts` (Node-free, so non-Node adapters can
14
14
  * import it); this module owns the Node in-memory implementation.
@@ -62,7 +62,7 @@ export class MemoryNonceStore {
62
62
  return Promise.resolve(ok({ status: "already-reserved", boundResource: existing.resource }));
63
63
  }
64
64
  // Reject a fresh reservation once full rather than grow unbounded or evict a live
65
- // entry (which reopens the race). Overwriting a dead entry is exempt (no growth).
65
+ // entry (which reopens the race condition). Overwriting a dead entry is exempt (no growth).
66
66
  if (existing === undefined && this.reserved.size >= this.maxEntries) {
67
67
  return Promise.resolve(err(guardError("store-at-capacity", "nonce store at capacity")));
68
68
  }
@@ -1 +1 @@
1
- {"version":3,"file":"nonce-store.js","sourceRoot":"","sources":["../src/nonce-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAe,MAAM,aAAa,CAAC;AAiCnD,oFAAoF;AACpF,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAElC;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,SAAS,CAAC;AAEtC,MAAM,UAAU,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;AAE/D;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,gBAAgB;IACV,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAC;IAC7C,SAAS,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAC5B,GAAG,CAAe;IAClB,UAAU,CAAS;IAEpC,YAAY,OAAO,GAA4B,EAAE;QAC/C,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,UAAU,CAAC;QACrC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAC;IAC9D,CAAC;IAED,iDAAiD;IACjD,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED,OAAO,CAAC,EACN,KAAK,EACL,QAAQ,EACR,SAAS,GACK;QACd,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAErB,6EAA6E;QAC7E,oDAAoD;QACpD,IAAI,SAAS,IAAI,GAAG,EAAE,CAAC;YACrB,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;QACpD,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;YACvD,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,kBAAkB,EAAE,aAAa,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC/F,CAAC;QACD,kFAAkF;QAClF,kFAAkF;QAClF,IAAI,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpE,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,mBAAmB,EAAE,yBAAyB,CAAC,CAAC,CAAC,CAAC;QAC1F,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;QAClC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QACzD,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,OAAO,CAAC,KAAa,EAAE,KAAa;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACvC,+EAA+E;QAC/E,2EAA2E;QAC3E,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;YACjD,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5B,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;IAED,kFAAkF;IAC1E,UAAU,CAAC,GAAW;QAC5B,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,GAAG,sBAAsB,EAAE,CAAC;YAClD,OAAO;QACT,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC;QACrB,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3C,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;gBAC3B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;IACH,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAO,GAA4B,EAAE;IAC1E,OAAO,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAC;AACvC,CAAC"}
1
+ {"version":3,"file":"nonce-store.js","sourceRoot":"","sources":["../src/nonce-store.ts"],"names":[],"mappings":"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"}
@@ -34,7 +34,7 @@ export type ReserveError = StoreError | GuardError<"store-at-capacity">;
34
34
  export interface ReserveParams {
35
35
  /**
36
36
  * The payment's nonce; the x402 exact scheme uses a random 32-byte value. The guard
37
- * keys on this nonce alone (folded to a canonical form by default, see canonical.ts).
37
+ * is keyed on this nonce alone (folded to a canonical form by default, see canonical.ts).
38
38
  * It does not scope by chain or asset, so a store shared across several chains or
39
39
  * assets should namespace the nonce per (chain, asset) itself to keep two distinct
40
40
  * payments from colliding; a direct store caller must also pre-fold it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craigruks/x402-server-guard",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Server-side hardening middleware for x402 payment endpoints. Mitigations for settlement races, replay, cross-resource substitution, and cache leakage. Not a security guarantee.",
5
5
  "keywords": [
6
6
  "x402",
@@ -19,7 +19,7 @@
19
19
  "type": "git",
20
20
  "url": "git+https://github.com/craigruks/x402-server-guard.git"
21
21
  },
22
- "homepage": "https://github.com/craigruks/x402-server-guard#readme",
22
+ "homepage": "https://craigruks.github.io/x402-server-guard/",
23
23
  "bugs": {
24
24
  "url": "https://github.com/craigruks/x402-server-guard/issues"
25
25
  },
@@ -70,21 +70,21 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "@arethetypeswrong/cli": "0.18.5",
73
- "@biomejs/biome": "2.5.3",
74
- "@cloudflare/workers-types": "5.20260714.1",
75
- "@types/node": "26.1.1",
76
- "@vitest/coverage-v8": "4.1.10",
77
- "@x402/core": "2.18.0",
78
- "esbuild": "0.28.1",
79
- "hono": "4.12.29",
80
- "miniflare": "4.20260708.1",
81
- "publint": "0.3.21",
82
- "tsx": "4.23.0",
73
+ "@biomejs/biome": "2.6.4",
74
+ "@cloudflare/workers-types": "5.20261104.1",
75
+ "@types/node": "26.6.6",
76
+ "@vitest/coverage-v8": "5.1.0",
77
+ "@x402/core": "2.29.0",
78
+ "esbuild": "0.28.2",
79
+ "hono": "4.13.16",
80
+ "miniflare": "4.20260730.0",
81
+ "publint": "0.3.25",
82
+ "tsx": "4.23.15",
83
83
  "typescript": "7.0.2",
84
- "vitest": "4.1.10"
84
+ "vitest": "5.1.0"
85
85
  },
86
86
  "allowScripts": {
87
- "esbuild@0.28.1": true,
88
- "workerd@1.20260708.1": true
87
+ "esbuild@0.28.2": true,
88
+ "workerd@1.20260730.1": true
89
89
  }
90
90
  }
package/src/cache.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Cache directives for paid responses.
3
3
  *
4
- * A shared cache (CDN or reverse proxy) keys on the URL and knows nothing about
4
+ * A shared cache (CDN or reverse proxy) is keyed on the URL and knows nothing about
5
5
  * payment, so a cacheable paid 200 can be stored and served to the next caller, paid
6
- * or not. The reference x402 adapters set no cache directive, leaving that door open.
6
+ * or not. The reference x402 adapters send `private` on the paid 200 as of
7
+ * `@x402/core` 2.21.0 (unchanged through 2.29.0), and set no directive at all before
8
+ * it; neither sets `Vary`.
7
9
  *
8
10
  * The mitigation is a response directive, not a decision about a nonce, so it lives
9
11
  * here as a small pure helper the server or adapter applies to every paid response.
@@ -22,7 +24,9 @@ export interface CacheDirectives {
22
24
  export interface CacheDirectivesOptions {
23
25
  /**
24
26
  * The request header the payment travels in, added to `Vary` so a cache that
25
- * stores despite `no-store` still keys by payer. Defaults to `X-PAYMENT`.
27
+ * stores despite `no-store` still keys by payer. Defaults to `X-PAYMENT`, the x402
28
+ * v1 header; an x402 v2 client sends `PAYMENT-SIGNATURE`, so pass that behind a v2
29
+ * server.
26
30
  */
27
31
  readonly paymentHeader?: string;
28
32
  }
package/src/guard.ts CHANGED
@@ -70,7 +70,7 @@ export function createGuard(options: GuardOptions = {}): Guard {
70
70
  const canonicalizeResource = options.canonicalizeResource ?? canonicalResource;
71
71
  return {
72
72
  async reserve(params: ReserveParams): Promise<Reservation> {
73
- // Fold both keys to canonical form up front; everything below keys on the
73
+ // Fold both keys to canonical form up front; everything below is keyed on the
74
74
  // canonical values (see canonical.ts for why two encodings must not split).
75
75
  const nonce = canonicalizeNonce(params.nonce);
76
76
  const resource = canonicalizeResource(params.resource);
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  /** Current package version (semver). */
16
- export const VERSION: string = "0.1.0";
16
+ export const VERSION: string = "0.1.2";
17
17
 
18
18
  export {
19
19
  type CacheDirectives,
@@ -5,10 +5,10 @@
5
5
  * are never split by an `await`. The in-memory store below is atomic by construction
6
6
  * (a synchronous body runs to completion); a distributed store must use a genuine
7
7
  * atomic compare-and-set (a Durable Object, Redis `SET .. NX`, a unique constraint).
8
- * Get-then-put stores (Workers KV, S3) reopen the race and are not safe here.
8
+ * Get-then-put stores (Workers KV, S3) reopen the race condition and are not safe here.
9
9
  *
10
10
  * `reserve` and `release` return a `Result`, so a store failure is a value the guard
11
- * turns into a fail-closed deny. Replay keys on the nonce, never the signature, which
11
+ * turns into a fail-closed deny. Replay is keyed on the nonce, never the signature, which
12
12
  * is what makes it immune to signature malleability. Full rationale: docs/hardening.md.
13
13
  * The contract itself lives in `store-types.ts` (Node-free, so non-Node adapters can
14
14
  * import it); this module owns the Node in-memory implementation.
@@ -106,7 +106,7 @@ export class MemoryNonceStore implements NonceStore {
106
106
  return Promise.resolve(ok({ status: "already-reserved", boundResource: existing.resource }));
107
107
  }
108
108
  // Reject a fresh reservation once full rather than grow unbounded or evict a live
109
- // entry (which reopens the race). Overwriting a dead entry is exempt (no growth).
109
+ // entry (which reopens the race condition). Overwriting a dead entry is exempt (no growth).
110
110
  if (existing === undefined && this.reserved.size >= this.maxEntries) {
111
111
  return Promise.resolve(err(guardError("store-at-capacity", "nonce store at capacity")));
112
112
  }
@@ -30,7 +30,7 @@ export type ReserveError = StoreError | GuardError<"store-at-capacity">;
30
30
  export interface ReserveParams {
31
31
  /**
32
32
  * The payment's nonce; the x402 exact scheme uses a random 32-byte value. The guard
33
- * keys on this nonce alone (folded to a canonical form by default, see canonical.ts).
33
+ * is keyed on this nonce alone (folded to a canonical form by default, see canonical.ts).
34
34
  * It does not scope by chain or asset, so a store shared across several chains or
35
35
  * assets should namespace the nonce per (chain, asset) itself to keep two distinct
36
36
  * payments from colliding; a direct store caller must also pre-fold it.