@tangle-network/agent-gateway 0.7.1 → 0.8.0

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.
Files changed (64) hide show
  1. package/README.md +83 -2
  2. package/dist/chunk-GITV7CPT.js +84 -0
  3. package/dist/chunk-GITV7CPT.js.map +1 -0
  4. package/dist/chunk-J5SDVHOL.js +104 -0
  5. package/dist/chunk-J5SDVHOL.js.map +1 -0
  6. package/dist/chunk-MP6IIAIA.js +5651 -0
  7. package/dist/chunk-MP6IIAIA.js.map +1 -0
  8. package/dist/index.d.ts +70 -10
  9. package/dist/index.js +303 -21
  10. package/dist/index.js.map +1 -1
  11. package/dist/middleware.d.ts +7 -2
  12. package/dist/middleware.js +3 -2
  13. package/dist/nonce-store.d.ts +47 -11
  14. package/dist/nonce-store.js +9 -3
  15. package/dist/observer-types-A0RtA8uL.d.ts +95 -0
  16. package/dist/observer.d.ts +79 -0
  17. package/dist/observer.js +11 -0
  18. package/dist/observer.js.map +1 -0
  19. package/dist/{types-DEsMmS-X.d.ts → types-BHISsm7D.d.ts} +414 -170
  20. package/dist/types.d.ts +2 -1
  21. package/package.json +1 -1
  22. package/src/a2a/execution-fence.ts +162 -0
  23. package/src/a2a/handler.ts +506 -560
  24. package/src/a2a/message-send-execution.ts +241 -0
  25. package/src/a2a/message-stream-execution.ts +392 -0
  26. package/src/a2a/payment-recovery.ts +431 -0
  27. package/src/a2a/push-config-methods.ts +158 -0
  28. package/src/a2a/push-notifications.ts +172 -22
  29. package/src/a2a/task-cancellation.ts +50 -0
  30. package/src/a2a/task-finalization.ts +451 -0
  31. package/src/a2a/task-lifecycle.ts +54 -0
  32. package/src/a2a/task-methods.ts +163 -0
  33. package/src/a2a/task-push-delivery.ts +119 -0
  34. package/src/a2a/task-recovery.ts +11 -0
  35. package/src/a2a/task-state.ts +99 -0
  36. package/src/a2a/task-store-sql.ts +222 -24
  37. package/src/a2a/task-store.ts +58 -1
  38. package/src/a2a/task-submission-recovery.ts +178 -0
  39. package/src/a2a/types.ts +1 -0
  40. package/src/dispatch-authorization.ts +437 -0
  41. package/src/dispatch-payment-recovery.ts +248 -0
  42. package/src/dispatch-payment.ts +425 -0
  43. package/src/dispatch-pricing.ts +108 -0
  44. package/src/dispatch-sandbox.ts +422 -0
  45. package/src/dispatch-settlement.ts +139 -0
  46. package/src/dispatch-types.ts +81 -0
  47. package/src/dispatch.ts +35 -483
  48. package/src/index.ts +57 -1
  49. package/src/middleware.ts +307 -26
  50. package/src/mpp-payment.ts +117 -0
  51. package/src/nonce-store.ts +122 -20
  52. package/src/observer-types.ts +63 -0
  53. package/src/observer.ts +3 -63
  54. package/src/payment-operations.ts +485 -0
  55. package/src/payment-recovery-sql.ts +108 -0
  56. package/src/payment-recovery-worker.ts +488 -0
  57. package/src/payment-recovery.ts +331 -0
  58. package/src/payment-types.ts +48 -0
  59. package/src/types.ts +144 -46
  60. package/src/verify.ts +233 -71
  61. package/dist/chunk-M7ZJAK4K.js +0 -53
  62. package/dist/chunk-M7ZJAK4K.js.map +0 -1
  63. package/dist/chunk-Q4YAIEZY.js +0 -1763
  64. package/dist/chunk-Q4YAIEZY.js.map +0 -1
package/src/verify.ts CHANGED
@@ -1,5 +1,105 @@
1
1
  import type { X402Config, MppConfig, ApiKeyInfo, GatewayConfig } from './types'
2
- import type { NonceStore } from './nonce-store'
2
+ import { claimStoredNonce, nonceTtlSeconds, type NonceStore } from './nonce-store'
3
+ import {
4
+ mppPaymentOperationId,
5
+ type MppAuthenticatedCredential,
6
+ } from './mpp-payment'
7
+
8
+ export interface VerifiedMppCredential extends MppAuthenticatedCredential {
9
+ /** Opaque replay key. BlueprinTEVM shares the x402 nonce namespace. */
10
+ replayKey: string
11
+ }
12
+
13
+ /** Return the legacy opaque nonce key used by older consumers. */
14
+ export function mppReplayNonceKey(authHeader: string): string | undefined {
15
+ const decoded = decodeMppCredential(authHeader)
16
+ return decoded ? canonicalMppNonceKey(decoded.method, decoded.payload, decoded.credential) : undefined
17
+ }
18
+
19
+ /** Return the decoded MPP payload for a durable payment claim. */
20
+ export function mppPaymentPayload(authHeader: string): Record<string, unknown> | undefined {
21
+ return decodeMppCredential(authHeader)?.payload
22
+ }
23
+
24
+ /** Return the decoded method credential for the post-guard charge lifecycle. */
25
+ export function mppPaymentCredential(authHeader: string): string | undefined {
26
+ return decodeMppCredential(authHeader)?.credential
27
+ }
28
+
29
+ interface DecodedMppCredential {
30
+ method: string
31
+ credential: string
32
+ payload: Record<string, unknown>
33
+ }
34
+
35
+ function decodeMppCredential(authHeader: string): DecodedMppCredential | undefined {
36
+ const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i)
37
+ if (!match) return undefined
38
+ const [, rawMethod, credentialB64] = match
39
+ if (!/^[A-Za-z0-9_-]+$/.test(credentialB64)) return undefined
40
+ try {
41
+ const decoded = Buffer.from(credentialB64, 'base64url').toString('utf-8')
42
+ let payload: Record<string, unknown> = {}
43
+ try {
44
+ const credential = JSON.parse(decoded) as unknown
45
+ if (credential && typeof credential === 'object' && !Array.isArray(credential)) {
46
+ const record = credential as Record<string, unknown>
47
+ const nested = record.payload
48
+ payload = nested && typeof nested === 'object' && !Array.isArray(nested)
49
+ ? nested as Record<string, unknown>
50
+ : record
51
+ }
52
+ } catch {
53
+ // Method-specific verifiers may accept a non-JSON credential format.
54
+ }
55
+ return { method: rawMethod.toLowerCase(), credential: decoded, payload }
56
+ } catch {
57
+ return undefined
58
+ }
59
+ }
60
+
61
+ function canonicalMppNonceKey(
62
+ method: string,
63
+ payload: Record<string, unknown>,
64
+ credential: string,
65
+ ): string {
66
+ if (payload.nonce === undefined) {
67
+ return `mpp:${method.toLowerCase()}:receipt:${Buffer.from(credential).toString('base64url')}`
68
+ }
69
+ const nonce = BigInt(String(payload.nonce)).toString()
70
+ const commitment = payload.commitment
71
+ if (method.toLowerCase() === 'blueprintevm' && typeof commitment === 'string' && commitment.length > 0) {
72
+ return `${commitment.toLowerCase()}:${nonce}`
73
+ }
74
+ return `mpp:${method.toLowerCase()}:${String(payload.commitment ?? payload.from ?? 'unknown').toLowerCase()}:${nonce}`
75
+ }
76
+
77
+ function blueprintevmNonceKey(payload: Record<string, unknown>): string | undefined {
78
+ if (payload.nonce === undefined) return undefined
79
+ const nonce = BigInt(String(payload.nonce)).toString()
80
+ const identity = payload.commitment ?? payload.from
81
+ if (typeof identity !== 'string' || identity.length === 0) return undefined
82
+ return `${identity.toLowerCase()}:${nonce}`
83
+ }
84
+
85
+ function stableJson(value: unknown): string {
86
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`
87
+ if (value && typeof value === 'object') {
88
+ const record = value as Record<string, unknown>
89
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')}}`
90
+ }
91
+ return JSON.stringify(value)
92
+ }
93
+
94
+ function legacyMppPaymentIdentity(
95
+ method: string,
96
+ payload: Record<string, unknown>,
97
+ credential: string,
98
+ ): string {
99
+ const canonicalPayload = stableJson(payload)
100
+ if (canonicalPayload !== '{}') return `legacy:${method}:${canonicalPayload}`
101
+ return `legacy:${method}:credential:${Buffer.from(credential).toString('base64url')}`
102
+ }
3
103
 
4
104
  /** Pure capability checks shared by discovery and every request protocol. */
5
105
  export function isApiKeyAuthEnabled(
@@ -8,17 +108,18 @@ export function isApiKeyAuthEnabled(
8
108
  return config.verifyApiKey !== undefined || config.x402.demoMode === true
9
109
  }
10
110
 
11
- /** MPP is enabled only when a real verifier or explicit demo mode exists. */
111
+ /** MPP is enabled only when authentication and method settlement are complete. */
12
112
  export function isMppAuthEnabled(
13
113
  config: Pick<GatewayConfig, 'mpp' | 'x402'>,
14
114
  ): boolean {
15
- const method = config.mpp?.method ?? 'blueprintevm'
16
- return Boolean(
17
- config.mpp &&
18
- (config.mpp.verifySigner !== undefined ||
19
- (method === 'blueprintevm' && config.x402.verifySigner !== undefined) ||
20
- config.x402.demoMode === true),
21
- )
115
+ const method = (config.mpp?.method ?? 'blueprintevm').toLowerCase()
116
+ if (!config.mpp) return false
117
+ const authenticated = typeof config.mpp.authenticateCredential === 'function' ||
118
+ typeof config.mpp.verifySigner === 'function' ||
119
+ (method === 'blueprintevm' && config.x402.verifySigner !== undefined) ||
120
+ (method === 'blueprintevm' && config.x402.demoMode === true)
121
+ if (!authenticated) return false
122
+ return method === 'blueprintevm' || config.mpp.charge !== undefined
22
123
  }
23
124
 
24
125
  /**
@@ -36,6 +137,8 @@ export async function verifyX402(
36
137
  spendAuthHeader: string,
37
138
  config: X402Config,
38
139
  nonceStore?: NonceStore,
140
+ minimumAmount = 1n,
141
+ markNonce = true,
39
142
  ): Promise<string | null> {
40
143
  try {
41
144
  const raw = JSON.parse(spendAuthHeader)
@@ -49,25 +152,30 @@ export async function verifyX402(
49
152
  // Reject expired payments
50
153
  if (expiry < BigInt(Math.floor(Date.now() / 1000))) return null
51
154
 
52
- // Reject zero-amount payments
53
- if (amount <= 0n) return null
155
+ // Reject payments that cannot cover the request's maximum charge. The
156
+ // check runs before the host verifier because that callback can reserve or
157
+ // settle funds as part of its production verification path.
158
+ if (amount <= 0n || minimumAmount < 0n || amount < minimumAmount) return null
54
159
 
55
- const nonceKey = `${raw.commitment}:${nonce.toString()}`
56
- if (nonceStore && await nonceStore.hasSeen(nonceKey)) return null
160
+ const nonceKey = `${String(raw.commitment).toLowerCase()}:${nonce.toString()}`
161
+ if (nonceStore?.hasSeen && await nonceStore.hasSeen(nonceKey)) return null
57
162
 
58
163
  if (config.verifySigner) {
59
- const verified = await config.verifySigner(raw)
164
+ const verified = await config.verifySigner(raw, {
165
+ protocolVersion: config.paymentProtocolVersion ?? (config.paymentOperations ? 2 : 1),
166
+ })
60
167
  if (!verified) return null
61
168
  } else if (!config.demoMode) {
62
169
  return null
63
170
  }
64
171
 
65
- // Check and mark only after the signature is accepted. Otherwise an
66
- // invalid request can burn a valid payer nonce and deny the real request.
67
- if (nonceStore) {
68
- // Mark seen with TTL matching the expiry window (max 1 hour)
69
- const ttl = Math.min(Number(expiry) - Math.floor(Date.now() / 1000), 3600)
70
- await nonceStore.markSeen(nonceKey, Math.max(ttl, 60))
172
+ // Claim only after the signature is accepted. Otherwise invalid traffic
173
+ // can burn a valid payer nonce and deny the real request.
174
+ if (nonceStore && markNonce) {
175
+ const ttl = nonceTtlSeconds(expiry)
176
+ if (ttl === undefined) return null
177
+ const claimed = await claimStoredNonce(nonceStore, nonceKey, ttl)
178
+ if (!claimed) return null
71
179
  }
72
180
 
73
181
  return raw.commitment
@@ -80,92 +188,146 @@ export async function verifyX402(
80
188
  * Verify MPP (Machine Payments Protocol) Authorization: Payment header.
81
189
  *
82
190
  * MPP uses `Authorization: Payment <method> <credential>` format. The
83
- * credential is method-specific; `MppConfig.verifySigner` owns verification
84
- * and returns the consumer identity. The built-in `blueprintevm` path can
191
+ * credential is method-specific; `MppConfig.authenticateCredential` owns authentication
192
+ * and returns the consumer plus stable payment identity. The built-in `blueprintevm` path can
85
193
  * reuse the x402 verifier for credentials with the compatible payload shape.
86
194
  *
87
- * Returns the signer address if valid, null otherwise.
195
+ * Returns authenticated identity if valid, null otherwise.
88
196
  * In demo mode, accepts any well-formed Payment header with an identity.
89
197
  */
90
- export async function verifyMpp(
198
+ export async function verifyMppCredential(
91
199
  authHeader: string,
92
200
  config: MppConfig,
93
201
  x402Config: X402Config,
94
202
  nonceStore?: NonceStore,
95
- ): Promise<string | null> {
203
+ minimumAmount = 1n,
204
+ markNonce = true,
205
+ ): Promise<VerifiedMppCredential | null> {
96
206
  // MPP format: "Payment <method> <base64url-credential>"
97
207
  const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i)
98
208
  if (!match) return null
99
209
 
100
- const [, method, credentialB64] = match
101
- if (config.method && method !== config.method) return null
210
+ const [, rawMethod] = match
211
+ const method = rawMethod.toLowerCase()
212
+ if (method !== (config.method ?? 'blueprintevm').toLowerCase()) return null
102
213
 
103
214
  try {
104
- if (!/^[A-Za-z0-9_-]+$/.test(credentialB64)) return null
105
- const decoded = Buffer.from(credentialB64, 'base64url').toString('utf-8')
106
- let payload: Record<string, unknown> = {}
107
- try {
108
- const credential = JSON.parse(decoded) as unknown
109
- if (credential && typeof credential === 'object' && !Array.isArray(credential)) {
110
- const nested = (credential as Record<string, unknown>).payload
111
- payload =
112
- nested && typeof nested === 'object' && !Array.isArray(nested)
113
- ? (nested as Record<string, unknown>)
114
- : (credential as Record<string, unknown>)
215
+ const decodedCredential = decodeMppCredential(authHeader)
216
+ if (!decodedCredential) return null
217
+ const { credential: decoded, payload } = decodedCredential
218
+
219
+ // Validate common EVM fields before pure credential authentication.
220
+ // BlueprinTEVM carries x402-equivalent token amounts and must cover the
221
+ // same request ceiling as the X-Payment-Signature path.
222
+ const operator = payload.operator ?? payload.to
223
+ if (operator !== undefined) {
224
+ if (typeof operator !== 'string' || operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) {
225
+ return null
115
226
  }
116
- } catch {
117
- // Method-specific verifiers may accept a non-JSON credential format.
227
+ }
228
+ const paymentAmount = payload.amount ?? payload.value
229
+ if (paymentAmount !== undefined) {
230
+ const amount = BigInt(String(paymentAmount))
231
+ if (amount <= 0n || (method === 'blueprintevm' && amount < minimumAmount)) return null
232
+ } else if (method === 'blueprintevm') {
233
+ return null
234
+ }
235
+ if (payload.nonce !== undefined) BigInt(String(payload.nonce))
236
+ if (payload.expiry !== undefined && BigInt(String(payload.expiry)) < BigInt(Math.floor(Date.now() / 1000))) {
237
+ return null
118
238
  }
119
239
 
120
- const nonceKey =
121
- nonceStore && payload.nonce !== undefined
122
- ? `mpp:${method}:${String(payload.commitment ?? payload.from ?? 'unknown')}:${String(payload.nonce)}`
123
- : null
124
- if (nonceKey && await nonceStore!.hasSeen(nonceKey)) return null
240
+ const blueprintevmKey = method === 'blueprintevm'
241
+ ? blueprintevmNonceKey(payload)
242
+ : undefined
243
+ if (markNonce && blueprintevmKey && nonceStore?.hasSeen && await nonceStore.hasSeen(blueprintevmKey)) {
244
+ return null
245
+ }
125
246
 
126
- let consumerId: string | null = null
127
- if (config.verifySigner) {
128
- consumerId = await config.verifySigner(payload, { method, credential: decoded })
247
+ let authenticated: MppAuthenticatedCredential | null = null
248
+ if (config.authenticateCredential) {
249
+ authenticated = await config.authenticateCredential(payload, { method, credential: decoded })
250
+ } else if (config.verifySigner) {
251
+ const consumerId = await config.verifySigner(payload, { method, credential: decoded })
252
+ authenticated = typeof consumerId === 'string' && consumerId.length > 0
253
+ ? {
254
+ consumerId,
255
+ paymentIdentity: legacyMppPaymentIdentity(method, payload, decoded),
256
+ }
257
+ : null
129
258
  } else if (method === 'blueprintevm' && x402Config.verifySigner && payload.commitment) {
130
- const verified = await x402Config.verifySigner(payload)
131
- consumerId = verified ? String(payload.commitment) : null
259
+ const verified = await x402Config.verifySigner(payload, {
260
+ protocolVersion: x402Config.paymentProtocolVersion ?? (x402Config.paymentOperations ? 2 : 1),
261
+ })
262
+ const paymentIdentity = blueprintevmNonceKey(payload) ?? stableJson(payload)
263
+ authenticated = verified
264
+ ? { consumerId: String(payload.commitment), paymentIdentity }
265
+ : null
132
266
  } else if (x402Config.demoMode) {
133
267
  const identity = payload.commitment ?? payload.from
134
268
  if (typeof identity !== 'string' || identity.length === 0) return null
135
- consumerId = identity
269
+ const paymentIdentity = method === 'blueprintevm'
270
+ ? blueprintevmNonceKey(payload) ?? stableJson(payload)
271
+ : ''
272
+ if (!paymentIdentity) return null
273
+ authenticated = { consumerId: identity, paymentIdentity }
136
274
  } else {
137
275
  return null
138
276
  }
139
- if (!consumerId) return null
277
+ if (
278
+ !authenticated ||
279
+ typeof authenticated.consumerId !== 'string' ||
280
+ authenticated.consumerId.length === 0 ||
281
+ typeof authenticated.paymentIdentity !== 'string' ||
282
+ authenticated.paymentIdentity.length === 0
283
+ ) return null
140
284
 
141
- // Validate common EVM fields when present. Method-specific verifiers own
142
- // the complete credential contract for non-EVM methods.
143
- const operator = payload.operator ?? payload.to
144
- if (operator !== undefined) {
145
- if (typeof operator !== 'string' || operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) {
146
- return null
147
- }
148
- }
149
- if (payload.amount !== undefined && BigInt(String(payload.amount)) <= 0n) return null
150
- if (payload.nonce !== undefined) BigInt(String(payload.nonce))
151
- if (payload.expiry !== undefined && BigInt(String(payload.expiry)) < BigInt(Math.floor(Date.now() / 1000))) {
152
- return null
153
- }
285
+ const replayKey = method === 'blueprintevm'
286
+ ? blueprintevmKey ??
287
+ await mppPaymentOperationId(method, authenticated.paymentIdentity)
288
+ : await mppPaymentOperationId(method, authenticated.paymentIdentity)
289
+ if (!replayKey) return null
290
+ if (markNonce && !blueprintevmKey && nonceStore?.hasSeen && await nonceStore.hasSeen(replayKey)) return null
154
291
 
155
- if (nonceStore && payload.nonce !== undefined) {
292
+ if (nonceStore && markNonce) {
156
293
  const expiry = payload.expiry === undefined
157
- ? Math.floor(Date.now() / 1000) + 3600
158
- : Number(payload.expiry)
159
- const ttl = Math.min(expiry - Math.floor(Date.now() / 1000), 3600)
160
- await nonceStore.markSeen(nonceKey!, Math.max(ttl, 60))
294
+ ? BigInt(Math.floor(Date.now() / 1000) + 3600)
295
+ : BigInt(String(payload.expiry))
296
+ const ttl = nonceTtlSeconds(expiry)
297
+ if (ttl === undefined) return null
298
+ const claimed = await claimStoredNonce(nonceStore, replayKey, ttl)
299
+ if (!claimed) return null
161
300
  }
162
301
 
163
- return consumerId
302
+ return { ...authenticated, replayKey }
164
303
  } catch {
165
304
  return null
166
305
  }
167
306
  }
168
307
 
308
+ /**
309
+ * Verify an MPP credential using the 0.7.1 public return shape.
310
+ * Rich durable callers use verifyMppCredential instead.
311
+ */
312
+ export async function verifyMpp(
313
+ authHeader: string,
314
+ config: MppConfig,
315
+ x402Config: X402Config,
316
+ nonceStore?: NonceStore,
317
+ minimumAmount = 1n,
318
+ markNonce = true,
319
+ ): Promise<string | null> {
320
+ const authenticated = await verifyMppCredential(
321
+ authHeader,
322
+ config,
323
+ x402Config,
324
+ nonceStore,
325
+ minimumAmount,
326
+ markNonce,
327
+ )
328
+ return authenticated?.consumerId ?? null
329
+ }
330
+
169
331
  /**
170
332
  * Default API key verifier — accepts any `sk_agent_*` key (demo mode).
171
333
  * Override in GatewayConfig.verifyApiKey for production.
@@ -1,53 +0,0 @@
1
- // src/nonce-store.ts
2
- var MemoryNonceStore = class {
3
- seen = /* @__PURE__ */ new Map();
4
- // nonce → expiresAt
5
- lastEviction = Date.now();
6
- async hasSeen(nonce) {
7
- this.evictExpired();
8
- const expiresAt = this.seen.get(nonce);
9
- if (!expiresAt) return false;
10
- if (expiresAt < Date.now()) {
11
- this.seen.delete(nonce);
12
- return false;
13
- }
14
- return true;
15
- }
16
- async markSeen(nonce, ttlSeconds) {
17
- this.seen.set(nonce, Date.now() + ttlSeconds * 1e3);
18
- this.evictExpired();
19
- }
20
- evictExpired() {
21
- const now = Date.now();
22
- if (now - this.lastEviction < 6e4) return;
23
- this.lastEviction = now;
24
- for (const [nonce, expiresAt] of this.seen) {
25
- if (expiresAt < now) this.seen.delete(nonce);
26
- }
27
- }
28
- };
29
- var KvNonceStore = class {
30
- constructor(kv, prefix = "nonce") {
31
- this.kv = kv;
32
- this.prefix = prefix;
33
- }
34
- kv;
35
- prefix;
36
- async hasSeen(nonce) {
37
- const value = await this.kv.get(this.key(nonce));
38
- return value !== null;
39
- }
40
- async markSeen(nonce, ttlSeconds) {
41
- const ttl = Math.max(ttlSeconds, 60);
42
- await this.kv.put(this.key(nonce), "1", { expirationTtl: ttl });
43
- }
44
- key(nonce) {
45
- return `${this.prefix}:${nonce}`;
46
- }
47
- };
48
-
49
- export {
50
- MemoryNonceStore,
51
- KvNonceStore
52
- };
53
- //# sourceMappingURL=chunk-M7ZJAK4K.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/nonce-store.ts"],"sourcesContent":["/**\n * Nonce replay protection for x402/MPP payments.\n * Tracks seen nonces to prevent the same payment from being used twice.\n */\n\nexport interface NonceStore {\n /** Check if nonce has been seen. Returns true if already used (reject). */\n hasSeen(nonce: string): Promise<boolean>\n /** Mark nonce as used. TTL = how long to remember it (seconds). */\n markSeen(nonce: string, ttlSeconds: number): Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// In-memory implementation — single-worker, ephemeral\n// ---------------------------------------------------------------------------\n\n/** In-memory nonce store with automatic eviction. Use in tests or single-worker deploys. */\nexport class MemoryNonceStore implements NonceStore {\n private seen = new Map<string, number>() // nonce → expiresAt\n private lastEviction = Date.now()\n\n async hasSeen(nonce: string): Promise<boolean> {\n this.evictExpired()\n const expiresAt = this.seen.get(nonce)\n if (!expiresAt) return false\n if (expiresAt < Date.now()) {\n this.seen.delete(nonce)\n return false\n }\n return true\n }\n\n async markSeen(nonce: string, ttlSeconds: number): Promise<void> {\n this.seen.set(nonce, Date.now() + ttlSeconds * 1000)\n this.evictExpired()\n }\n\n private evictExpired() {\n const now = Date.now()\n // Evict at most every 60 seconds to avoid O(n) on every request\n if (now - this.lastEviction < 60_000) return\n this.lastEviction = now\n for (const [nonce, expiresAt] of this.seen) {\n if (expiresAt < now) this.seen.delete(nonce)\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Cloudflare KV implementation — multi-worker, distributed\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal KVNamespace shape — matches Cloudflare Workers' @cloudflare/workers-types\n * without pulling that package as a dep. Production consumers cast their KV\n * binding to this interface at the construction site.\n */\nexport interface KVNamespace {\n get(key: string, options?: { type?: 'text' | 'json' }): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/**\n * KV-backed NonceStore for distributed Cloudflare Workers deployments.\n *\n * Why this exists: MemoryNonceStore works on a single worker instance, but\n * Cloudflare routes requests across multiple isolates. Without shared state,\n * an attacker could retry a replayed nonce against a different isolate and\n * have it accepted. This implementation uses Workers KV with native TTL so\n * the nonce automatically expires at payment-expiry time.\n *\n * TTL precision: KV is eventually consistent (propagation ~60s). For x402\n * with 10-minute expiry windows this is fine — by the time KV propagates,\n * the payment itself would be expired anyway.\n *\n * Usage:\n * const nonceStore = new KvNonceStore(env.NONCE_KV, 'x402')\n * createAgentGateway({ ...config, nonceStore })\n */\nexport class KvNonceStore implements NonceStore {\n constructor(\n private readonly kv: KVNamespace,\n /** Key prefix to namespace within a shared KV (default: \"nonce\"). */\n private readonly prefix: string = 'nonce',\n ) {}\n\n async hasSeen(nonce: string): Promise<boolean> {\n const value = await this.kv.get(this.key(nonce))\n return value !== null\n }\n\n async markSeen(nonce: string, ttlSeconds: number): Promise<void> {\n // KV minimum TTL is 60 seconds\n const ttl = Math.max(ttlSeconds, 60)\n await this.kv.put(this.key(nonce), '1', { expirationTtl: ttl })\n }\n\n private key(nonce: string): string {\n return `${this.prefix}:${nonce}`\n }\n}\n"],"mappings":";AAiBO,IAAM,mBAAN,MAA6C;AAAA,EAC1C,OAAO,oBAAI,IAAoB;AAAA;AAAA,EAC/B,eAAe,KAAK,IAAI;AAAA,EAEhC,MAAM,QAAQ,OAAiC;AAC7C,SAAK,aAAa;AAClB,UAAM,YAAY,KAAK,KAAK,IAAI,KAAK;AACrC,QAAI,CAAC,UAAW,QAAO;AACvB,QAAI,YAAY,KAAK,IAAI,GAAG;AAC1B,WAAK,KAAK,OAAO,KAAK;AACtB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAS,OAAe,YAAmC;AAC/D,SAAK,KAAK,IAAI,OAAO,KAAK,IAAI,IAAI,aAAa,GAAI;AACnD,SAAK,aAAa;AAAA,EACpB;AAAA,EAEQ,eAAe;AACrB,UAAM,MAAM,KAAK,IAAI;AAErB,QAAI,MAAM,KAAK,eAAe,IAAQ;AACtC,SAAK,eAAe;AACpB,eAAW,CAAC,OAAO,SAAS,KAAK,KAAK,MAAM;AAC1C,UAAI,YAAY,IAAK,MAAK,KAAK,OAAO,KAAK;AAAA,IAC7C;AAAA,EACF;AACF;AAkCO,IAAM,eAAN,MAAyC;AAAA,EAC9C,YACmB,IAEA,SAAiB,SAClC;AAHiB;AAEA;AAAA,EAChB;AAAA,EAHgB;AAAA,EAEA;AAAA,EAGnB,MAAM,QAAQ,OAAiC;AAC7C,UAAM,QAAQ,MAAM,KAAK,GAAG,IAAI,KAAK,IAAI,KAAK,CAAC;AAC/C,WAAO,UAAU;AAAA,EACnB;AAAA,EAEA,MAAM,SAAS,OAAe,YAAmC;AAE/D,UAAM,MAAM,KAAK,IAAI,YAAY,EAAE;AACnC,UAAM,KAAK,GAAG,IAAI,KAAK,IAAI,KAAK,GAAG,KAAK,EAAE,eAAe,IAAI,CAAC;AAAA,EAChE;AAAA,EAEQ,IAAI,OAAuB;AACjC,WAAO,GAAG,KAAK,MAAM,IAAI,KAAK;AAAA,EAChC;AACF;","names":[]}