@tangle-network/agent-gateway 0.7.1 → 0.8.1

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 +90 -3
  2. package/dist/chunk-C7Z2BRYV.js +5693 -0
  3. package/dist/chunk-C7Z2BRYV.js.map +1 -0
  4. package/dist/chunk-GITV7CPT.js +84 -0
  5. package/dist/chunk-GITV7CPT.js.map +1 -0
  6. package/dist/chunk-J5SDVHOL.js +104 -0
  7. package/dist/chunk-J5SDVHOL.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-oQ58UakD.d.ts} +447 -172
  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 +468 -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 +424 -0
  45. package/src/dispatch-settlement.ts +139 -0
  46. package/src/dispatch-types.ts +84 -0
  47. package/src/dispatch.ts +35 -483
  48. package/src/index.ts +59 -1
  49. package/src/middleware.ts +339 -35
  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 +188 -49
  60. package/src/verify.ts +240 -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,25 @@ 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
+ /** x402 is advertised only when the gateway can authenticate it. */
112
+ export function isX402AuthEnabled(
113
+ config: Pick<GatewayConfig, 'x402'>,
114
+ ): boolean {
115
+ return config.x402.verifySigner !== undefined || config.x402.demoMode === true
116
+ }
117
+
118
+ /** MPP is enabled only when authentication and method settlement are complete. */
12
119
  export function isMppAuthEnabled(
13
120
  config: Pick<GatewayConfig, 'mpp' | 'x402'>,
14
121
  ): 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
- )
122
+ const method = (config.mpp?.method ?? 'blueprintevm').toLowerCase()
123
+ if (!config.mpp) return false
124
+ const authenticated = typeof config.mpp.authenticateCredential === 'function' ||
125
+ typeof config.mpp.verifySigner === 'function' ||
126
+ (method === 'blueprintevm' && config.x402.verifySigner !== undefined) ||
127
+ (method === 'blueprintevm' && config.x402.demoMode === true)
128
+ if (!authenticated) return false
129
+ return method === 'blueprintevm' || config.mpp.charge !== undefined
22
130
  }
23
131
 
24
132
  /**
@@ -36,6 +144,8 @@ export async function verifyX402(
36
144
  spendAuthHeader: string,
37
145
  config: X402Config,
38
146
  nonceStore?: NonceStore,
147
+ minimumAmount = 1n,
148
+ markNonce = true,
39
149
  ): Promise<string | null> {
40
150
  try {
41
151
  const raw = JSON.parse(spendAuthHeader)
@@ -49,25 +159,30 @@ export async function verifyX402(
49
159
  // Reject expired payments
50
160
  if (expiry < BigInt(Math.floor(Date.now() / 1000))) return null
51
161
 
52
- // Reject zero-amount payments
53
- if (amount <= 0n) return null
162
+ // Reject payments that cannot cover the request's maximum charge. The
163
+ // check runs before the host verifier because that callback can reserve or
164
+ // settle funds as part of its production verification path.
165
+ if (amount <= 0n || minimumAmount < 0n || amount < minimumAmount) return null
54
166
 
55
- const nonceKey = `${raw.commitment}:${nonce.toString()}`
56
- if (nonceStore && await nonceStore.hasSeen(nonceKey)) return null
167
+ const nonceKey = `${String(raw.commitment).toLowerCase()}:${nonce.toString()}`
168
+ if (nonceStore?.hasSeen && await nonceStore.hasSeen(nonceKey)) return null
57
169
 
58
170
  if (config.verifySigner) {
59
- const verified = await config.verifySigner(raw)
171
+ const verified = await config.verifySigner(raw, {
172
+ protocolVersion: config.paymentProtocolVersion ?? (config.paymentOperations ? 2 : 1),
173
+ })
60
174
  if (!verified) return null
61
175
  } else if (!config.demoMode) {
62
176
  return null
63
177
  }
64
178
 
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))
179
+ // Claim only after the signature is accepted. Otherwise invalid traffic
180
+ // can burn a valid payer nonce and deny the real request.
181
+ if (nonceStore && markNonce) {
182
+ const ttl = nonceTtlSeconds(expiry)
183
+ if (ttl === undefined) return null
184
+ const claimed = await claimStoredNonce(nonceStore, nonceKey, ttl)
185
+ if (!claimed) return null
71
186
  }
72
187
 
73
188
  return raw.commitment
@@ -80,92 +195,146 @@ export async function verifyX402(
80
195
  * Verify MPP (Machine Payments Protocol) Authorization: Payment header.
81
196
  *
82
197
  * 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
198
+ * credential is method-specific; `MppConfig.authenticateCredential` owns authentication
199
+ * and returns the consumer plus stable payment identity. The built-in `blueprintevm` path can
85
200
  * reuse the x402 verifier for credentials with the compatible payload shape.
86
201
  *
87
- * Returns the signer address if valid, null otherwise.
202
+ * Returns authenticated identity if valid, null otherwise.
88
203
  * In demo mode, accepts any well-formed Payment header with an identity.
89
204
  */
90
- export async function verifyMpp(
205
+ export async function verifyMppCredential(
91
206
  authHeader: string,
92
207
  config: MppConfig,
93
208
  x402Config: X402Config,
94
209
  nonceStore?: NonceStore,
95
- ): Promise<string | null> {
210
+ minimumAmount = 1n,
211
+ markNonce = true,
212
+ ): Promise<VerifiedMppCredential | null> {
96
213
  // MPP format: "Payment <method> <base64url-credential>"
97
214
  const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i)
98
215
  if (!match) return null
99
216
 
100
- const [, method, credentialB64] = match
101
- if (config.method && method !== config.method) return null
217
+ const [, rawMethod] = match
218
+ const method = rawMethod.toLowerCase()
219
+ if (method !== (config.method ?? 'blueprintevm').toLowerCase()) return null
102
220
 
103
221
  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>)
222
+ const decodedCredential = decodeMppCredential(authHeader)
223
+ if (!decodedCredential) return null
224
+ const { credential: decoded, payload } = decodedCredential
225
+
226
+ // Validate common EVM fields before pure credential authentication.
227
+ // BlueprinTEVM carries x402-equivalent token amounts and must cover the
228
+ // same request ceiling as the X-Payment-Signature path.
229
+ const operator = payload.operator ?? payload.to
230
+ if (operator !== undefined) {
231
+ if (typeof operator !== 'string' || operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) {
232
+ return null
115
233
  }
116
- } catch {
117
- // Method-specific verifiers may accept a non-JSON credential format.
234
+ }
235
+ const paymentAmount = payload.amount ?? payload.value
236
+ if (paymentAmount !== undefined) {
237
+ const amount = BigInt(String(paymentAmount))
238
+ if (amount <= 0n || (method === 'blueprintevm' && amount < minimumAmount)) return null
239
+ } else if (method === 'blueprintevm') {
240
+ return null
241
+ }
242
+ if (payload.nonce !== undefined) BigInt(String(payload.nonce))
243
+ if (payload.expiry !== undefined && BigInt(String(payload.expiry)) < BigInt(Math.floor(Date.now() / 1000))) {
244
+ return null
118
245
  }
119
246
 
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
247
+ const blueprintevmKey = method === 'blueprintevm'
248
+ ? blueprintevmNonceKey(payload)
249
+ : undefined
250
+ if (markNonce && blueprintevmKey && nonceStore?.hasSeen && await nonceStore.hasSeen(blueprintevmKey)) {
251
+ return null
252
+ }
125
253
 
126
- let consumerId: string | null = null
127
- if (config.verifySigner) {
128
- consumerId = await config.verifySigner(payload, { method, credential: decoded })
254
+ let authenticated: MppAuthenticatedCredential | null = null
255
+ if (config.authenticateCredential) {
256
+ authenticated = await config.authenticateCredential(payload, { method, credential: decoded })
257
+ } else if (config.verifySigner) {
258
+ const consumerId = await config.verifySigner(payload, { method, credential: decoded })
259
+ authenticated = typeof consumerId === 'string' && consumerId.length > 0
260
+ ? {
261
+ consumerId,
262
+ paymentIdentity: legacyMppPaymentIdentity(method, payload, decoded),
263
+ }
264
+ : null
129
265
  } else if (method === 'blueprintevm' && x402Config.verifySigner && payload.commitment) {
130
- const verified = await x402Config.verifySigner(payload)
131
- consumerId = verified ? String(payload.commitment) : null
266
+ const verified = await x402Config.verifySigner(payload, {
267
+ protocolVersion: x402Config.paymentProtocolVersion ?? (x402Config.paymentOperations ? 2 : 1),
268
+ })
269
+ const paymentIdentity = blueprintevmNonceKey(payload) ?? stableJson(payload)
270
+ authenticated = verified
271
+ ? { consumerId: String(payload.commitment), paymentIdentity }
272
+ : null
132
273
  } else if (x402Config.demoMode) {
133
274
  const identity = payload.commitment ?? payload.from
134
275
  if (typeof identity !== 'string' || identity.length === 0) return null
135
- consumerId = identity
276
+ const paymentIdentity = method === 'blueprintevm'
277
+ ? blueprintevmNonceKey(payload) ?? stableJson(payload)
278
+ : ''
279
+ if (!paymentIdentity) return null
280
+ authenticated = { consumerId: identity, paymentIdentity }
136
281
  } else {
137
282
  return null
138
283
  }
139
- if (!consumerId) return null
284
+ if (
285
+ !authenticated ||
286
+ typeof authenticated.consumerId !== 'string' ||
287
+ authenticated.consumerId.length === 0 ||
288
+ typeof authenticated.paymentIdentity !== 'string' ||
289
+ authenticated.paymentIdentity.length === 0
290
+ ) return null
140
291
 
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
- }
292
+ const replayKey = method === 'blueprintevm'
293
+ ? blueprintevmKey ??
294
+ await mppPaymentOperationId(method, authenticated.paymentIdentity)
295
+ : await mppPaymentOperationId(method, authenticated.paymentIdentity)
296
+ if (!replayKey) return null
297
+ if (markNonce && !blueprintevmKey && nonceStore?.hasSeen && await nonceStore.hasSeen(replayKey)) return null
154
298
 
155
- if (nonceStore && payload.nonce !== undefined) {
299
+ if (nonceStore && markNonce) {
156
300
  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))
301
+ ? BigInt(Math.floor(Date.now() / 1000) + 3600)
302
+ : BigInt(String(payload.expiry))
303
+ const ttl = nonceTtlSeconds(expiry)
304
+ if (ttl === undefined) return null
305
+ const claimed = await claimStoredNonce(nonceStore, replayKey, ttl)
306
+ if (!claimed) return null
161
307
  }
162
308
 
163
- return consumerId
309
+ return { ...authenticated, replayKey }
164
310
  } catch {
165
311
  return null
166
312
  }
167
313
  }
168
314
 
315
+ /**
316
+ * Verify an MPP credential using the 0.7.1 public return shape.
317
+ * Rich durable callers use verifyMppCredential instead.
318
+ */
319
+ export async function verifyMpp(
320
+ authHeader: string,
321
+ config: MppConfig,
322
+ x402Config: X402Config,
323
+ nonceStore?: NonceStore,
324
+ minimumAmount = 1n,
325
+ markNonce = true,
326
+ ): Promise<string | null> {
327
+ const authenticated = await verifyMppCredential(
328
+ authHeader,
329
+ config,
330
+ x402Config,
331
+ nonceStore,
332
+ minimumAmount,
333
+ markNonce,
334
+ )
335
+ return authenticated?.consumerId ?? null
336
+ }
337
+
169
338
  /**
170
339
  * Default API key verifier — accepts any `sk_agent_*` key (demo mode).
171
340
  * 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":[]}