@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/dist/index.js CHANGED
@@ -4,28 +4,48 @@ import {
4
4
  } from "./chunk-5O75YDQP.js";
5
5
  import {
6
6
  A2A_ERROR_CODES,
7
- CompositeObserver,
8
- ConsoleObserver,
9
7
  InMemoryPushNotificationStore,
10
8
  InMemoryTaskStore,
9
+ MPP_CHARGE_PROTOCOL_VERSION,
10
+ MemoryPaymentOperations,
11
+ MemoryPaymentRecoveryStore,
12
+ PAYMENT_PROTOCOL_VERSION,
13
+ PAYMENT_RECOVERY_VERSION,
14
+ PaymentRecoveryFenceError,
11
15
  SqlPushNotificationStore,
12
16
  createAgentGateway,
13
17
  defaultVerifyApiKey,
18
+ deliverDemoPushNotifications,
14
19
  deliverPushNotifications,
15
20
  detectInjection,
16
21
  filterConsumerMessages,
17
22
  filterConsumerMessagesStrict,
18
- generateRequestId,
23
+ hasPendingPaymentRecovery,
24
+ inspectTaskExecution,
19
25
  isApiKeyAuthEnabled,
20
26
  isMppAuthEnabled,
27
+ mppPaymentCredential,
28
+ mppPaymentOperationId,
29
+ mppReplayNonceKey,
30
+ reclaimPayment,
31
+ recoverPayment,
32
+ recoverPayments,
21
33
  redactSystemPromptFromOutput,
34
+ validatePushNotificationUrl,
22
35
  verifyMpp,
36
+ verifyMppCredential,
23
37
  verifyX402
24
- } from "./chunk-Q4YAIEZY.js";
38
+ } from "./chunk-C7Z2BRYV.js";
25
39
  import {
26
40
  KvNonceStore,
27
- MemoryNonceStore
28
- } from "./chunk-M7ZJAK4K.js";
41
+ MemoryNonceStore,
42
+ isAtomicNonceStore
43
+ } from "./chunk-J5SDVHOL.js";
44
+ import {
45
+ CompositeObserver,
46
+ ConsoleObserver,
47
+ generateRequestId
48
+ } from "./chunk-GITV7CPT.js";
29
49
  import {
30
50
  createPublishRoutes
31
51
  } from "./chunk-5ZJPIIIV.js";
@@ -35,6 +55,96 @@ import {
35
55
  checkRateLimit
36
56
  } from "./chunk-XCTXHZ76.js";
37
57
 
58
+ // src/payment-recovery-sql.ts
59
+ var TABLE_DDL = (table) => `
60
+ CREATE TABLE IF NOT EXISTS ${table} (
61
+ id TEXT PRIMARY KEY,
62
+ state TEXT NOT NULL,
63
+ next_attempt_at INTEGER NOT NULL,
64
+ revision INTEGER NOT NULL,
65
+ payload TEXT NOT NULL,
66
+ updated_at INTEGER NOT NULL
67
+ )
68
+ `;
69
+ var DUE_INDEX_DDL = (table) => `
70
+ CREATE INDEX IF NOT EXISTS idx_${table}_due
71
+ ON ${table} (state, next_attempt_at)
72
+ `;
73
+ var SqlPaymentRecoveryStore = class {
74
+ constructor(db, options = {}) {
75
+ this.db = db;
76
+ this.table = options.table ?? "payment_recovery";
77
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(this.table)) {
78
+ throw new Error("payment recovery table name is invalid");
79
+ }
80
+ }
81
+ db;
82
+ table;
83
+ /** Idempotent. Run this before the gateway starts accepting traffic. */
84
+ async migrate() {
85
+ await this.db.exec(TABLE_DDL(this.table));
86
+ await this.db.exec(DUE_INDEX_DDL(this.table));
87
+ }
88
+ async createIfAbsent(record) {
89
+ try {
90
+ const result = await this.db.exec(
91
+ `INSERT INTO ${this.table} (id, state, next_attempt_at, revision, payload, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
92
+ [
93
+ record.id,
94
+ record.state,
95
+ record.nextAttemptAt,
96
+ record.revision,
97
+ JSON.stringify(record),
98
+ record.updatedAt
99
+ ]
100
+ );
101
+ return result.rowsAffected === 1;
102
+ } catch (error) {
103
+ if (await this.get(record.id)) return false;
104
+ throw error;
105
+ }
106
+ }
107
+ async get(id) {
108
+ const rows = await this.db.query(
109
+ `SELECT payload FROM ${this.table} WHERE id = ?`,
110
+ [id]
111
+ );
112
+ return rows[0] ? parseRecord(rows[0].payload) : void 0;
113
+ }
114
+ async compareAndSet(expected, next) {
115
+ const result = await this.db.exec(
116
+ `UPDATE ${this.table} SET state = ?, next_attempt_at = ?, revision = ?, payload = ?, updated_at = ? WHERE id = ? AND revision = ?`,
117
+ [
118
+ next.state,
119
+ next.nextAttemptAt,
120
+ next.revision,
121
+ JSON.stringify(next),
122
+ next.updatedAt,
123
+ expected.id,
124
+ expected.revision
125
+ ]
126
+ );
127
+ return result.rowsAffected === 1;
128
+ }
129
+ async listDue(now, limit) {
130
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
131
+ throw new Error("payment recovery scan limit must be a positive safe integer");
132
+ }
133
+ const rows = await this.db.query(
134
+ `SELECT payload FROM ${this.table} WHERE state <> ? AND next_attempt_at <= ? ORDER BY next_attempt_at ASC LIMIT ?`,
135
+ ["reconciled", now, limit]
136
+ );
137
+ return rows.map((row) => parseRecord(row.payload));
138
+ }
139
+ };
140
+ function parseRecord(payload) {
141
+ const value = JSON.parse(payload);
142
+ if (value.version !== 1 || !value.id || !Number.isSafeInteger(value.revision)) {
143
+ throw new Error("stored payment recovery record is invalid");
144
+ }
145
+ return value;
146
+ }
147
+
38
148
  // src/a2a/task-store-sql.ts
39
149
  function d1ToSqlAdapter(db) {
40
150
  return {
@@ -60,7 +170,9 @@ var TASKS_TABLE_DDL = (table) => `
60
170
  context_id TEXT NOT NULL,
61
171
  state TEXT NOT NULL,
62
172
  payload TEXT NOT NULL,
63
- updated_at INTEGER NOT NULL
173
+ updated_at INTEGER NOT NULL,
174
+ execution_request_id TEXT,
175
+ execution_lease_expires_at REAL
64
176
  )
65
177
  `;
66
178
  var CTX_INDEX_DDL = (table) => `
@@ -79,40 +191,187 @@ var SqlTaskStore = class {
79
191
  get table() {
80
192
  return this.opts.table ?? "a2a_tasks";
81
193
  }
194
+ async readRow(id) {
195
+ const rows = await this.db.query(
196
+ `SELECT payload, updated_at, execution_request_id, execution_lease_expires_at FROM ${this.table} WHERE id = ?`,
197
+ [id]
198
+ );
199
+ const row = rows[0];
200
+ return row ? {
201
+ payload: row.payload,
202
+ updatedAt: row.updated_at,
203
+ executionRequestId: row.execution_request_id ?? null,
204
+ executionLeaseExpiresAt: row.execution_lease_expires_at ?? null
205
+ } : void 0;
206
+ }
207
+ isExpired(updatedAt, task) {
208
+ return Date.now() - updatedAt > this.ttlMs && !hasPendingPaymentRecovery(task);
209
+ }
210
+ async deleteObservedRow(id, payload, updatedAt) {
211
+ const result = await this.db.exec(
212
+ `DELETE FROM ${this.table} WHERE id = ? AND payload = ? AND updated_at = ?`,
213
+ [id, payload, updatedAt]
214
+ );
215
+ return result.rowsAffected;
216
+ }
82
217
  /** Idempotent. Call once at deploy. */
83
218
  async migrate() {
84
219
  await this.db.exec(TASKS_TABLE_DDL(this.table));
220
+ for (const column of [
221
+ "execution_request_id TEXT",
222
+ "execution_lease_expires_at REAL"
223
+ ]) {
224
+ try {
225
+ await this.db.exec(`ALTER TABLE ${this.table} ADD COLUMN ${column}`);
226
+ } catch (error) {
227
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
228
+ if (!message.includes("duplicate column") && !message.includes("already exists")) throw error;
229
+ }
230
+ }
85
231
  await this.db.exec(CTX_INDEX_DDL(this.table));
86
232
  }
87
233
  async get(id) {
88
- const rows = await this.db.query(
89
- `SELECT payload, updated_at FROM ${this.table} WHERE id = ?`,
90
- [id]
91
- );
92
- const row = rows[0];
234
+ const row = await this.readRow(id);
93
235
  if (!row) return void 0;
94
- if (Date.now() - row.updated_at > this.ttlMs) {
95
- void this.db.exec(`DELETE FROM ${this.table} WHERE id = ?`, [id]);
236
+ const task = JSON.parse(row.payload);
237
+ if (this.isExpired(row.updatedAt, task)) {
238
+ await this.deleteObservedRow(id, row.payload, row.updatedAt);
96
239
  return void 0;
97
240
  }
98
- return JSON.parse(row.payload);
241
+ return task;
242
+ }
243
+ async insert(task) {
244
+ const payload = JSON.stringify(task);
245
+ const [executionRequestId, executionLeaseExpiresAt] = executionColumns(task);
246
+ const result = await this.db.exec(
247
+ `INSERT INTO ${this.table} (id, context_id, state, payload, updated_at, execution_request_id, execution_lease_expires_at)
248
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
249
+ [
250
+ task.id,
251
+ task.contextId,
252
+ task.status.state,
253
+ payload,
254
+ Date.now(),
255
+ executionRequestId,
256
+ executionLeaseExpiresAt
257
+ ]
258
+ );
259
+ return result.rowsAffected;
99
260
  }
100
261
  async put(task) {
101
262
  const payload = JSON.stringify(task);
102
263
  const updatedAt = Date.now();
264
+ const [executionRequestId, executionLeaseExpiresAt] = executionColumns(task);
103
265
  const updated = await this.db.exec(
104
- `UPDATE ${this.table} SET context_id = ?, state = ?, payload = ?, updated_at = ? WHERE id = ?`,
105
- [task.contextId, task.status.state, payload, updatedAt, task.id]
266
+ `UPDATE ${this.table}
267
+ SET context_id = ?, state = ?, payload = ?, updated_at = ?,
268
+ execution_request_id = ?, execution_lease_expires_at = ?
269
+ WHERE id = ?`,
270
+ [
271
+ task.contextId,
272
+ task.status.state,
273
+ payload,
274
+ updatedAt,
275
+ executionRequestId,
276
+ executionLeaseExpiresAt,
277
+ task.id
278
+ ]
106
279
  );
107
280
  if (updated.rowsAffected === 0) {
108
281
  await this.db.exec(
109
- `INSERT INTO ${this.table} (id, context_id, state, payload, updated_at) VALUES (?, ?, ?, ?, ?)`,
110
- [task.id, task.contextId, task.status.state, payload, updatedAt]
282
+ `INSERT INTO ${this.table} (id, context_id, state, payload, updated_at, execution_request_id, execution_lease_expires_at)
283
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
284
+ [
285
+ task.id,
286
+ task.contextId,
287
+ task.status.state,
288
+ payload,
289
+ updatedAt,
290
+ executionRequestId,
291
+ executionLeaseExpiresAt
292
+ ]
111
293
  );
112
294
  }
113
295
  }
296
+ async createIfAbsent(task) {
297
+ try {
298
+ return await this.insert(task) === 1;
299
+ } catch (error) {
300
+ const row = await this.readRow(task.id);
301
+ if (!row) throw error;
302
+ const existing = JSON.parse(row.payload);
303
+ if (!this.isExpired(row.updatedAt, existing)) return false;
304
+ await this.deleteObservedRow(task.id, row.payload, row.updatedAt);
305
+ try {
306
+ return await this.insert(task) === 1;
307
+ } catch (retryError) {
308
+ if (await this.get(task.id)) return false;
309
+ throw retryError;
310
+ }
311
+ }
312
+ }
313
+ async compareAndSet(expected, next) {
314
+ const expectedPayload = JSON.stringify(expected);
315
+ const payload = JSON.stringify(next);
316
+ const [executionRequestId, executionLeaseExpiresAt] = executionColumns(next);
317
+ const result = await this.db.exec(
318
+ `UPDATE ${this.table}
319
+ SET context_id = ?, state = ?, payload = ?, updated_at = ?,
320
+ execution_request_id = ?, execution_lease_expires_at = ?
321
+ WHERE id = ? AND payload = ?`,
322
+ [
323
+ next.contextId,
324
+ next.status.state,
325
+ payload,
326
+ Date.now(),
327
+ executionRequestId,
328
+ executionLeaseExpiresAt,
329
+ expected.id,
330
+ expectedPayload
331
+ ]
332
+ );
333
+ return result.rowsAffected === 1;
334
+ }
335
+ async compareAndSetExecution(expected, next, requestId, now) {
336
+ const expectedMarker = inspectTaskExecution(expected);
337
+ const nextMarker = inspectTaskExecution(next);
338
+ if (expectedMarker.state !== "valid" || nextMarker.state !== "valid" || expectedMarker.marker.requestId !== requestId || nextMarker.marker.requestId !== requestId || expectedMarker.marker.lease.expiresAt <= now || nextMarker.marker.lease.expiresAt <= now) return false;
339
+ const expectedPayload = JSON.stringify(expected);
340
+ const payload = JSON.stringify(next);
341
+ const updatedAt = Date.now();
342
+ const result = await this.db.exec(
343
+ `UPDATE ${this.table}
344
+ SET context_id = ?, state = ?, payload = ?, updated_at = ?,
345
+ execution_request_id = ?, execution_lease_expires_at = ?
346
+ WHERE id = ?
347
+ AND payload = ?
348
+ AND state = 'working'
349
+ AND (
350
+ (execution_request_id = ? AND execution_lease_expires_at > ?)
351
+ OR (execution_request_id IS NULL AND execution_lease_expires_at IS NULL)
352
+ )`,
353
+ [
354
+ next.contextId,
355
+ next.status.state,
356
+ payload,
357
+ updatedAt,
358
+ nextMarker.marker.requestId,
359
+ nextMarker.marker.lease.expiresAt,
360
+ expected.id,
361
+ expectedPayload,
362
+ requestId,
363
+ now
364
+ ]
365
+ );
366
+ return result.rowsAffected === 1;
367
+ }
114
368
  async delete(id) {
115
- await this.db.exec(`DELETE FROM ${this.table} WHERE id = ?`, [id]);
369
+ const task = await this.get(id);
370
+ if (!task || hasPendingPaymentRecovery(task)) return;
371
+ await this.db.exec(
372
+ `DELETE FROM ${this.table} WHERE id = ? AND payload = ?`,
373
+ [id, JSON.stringify(task)]
374
+ );
116
375
  }
117
376
  /**
118
377
  * Lookup tasks by contextId — used by `tasks/resubscribe` and the multi-turn
@@ -126,9 +385,15 @@ var SqlTaskStore = class {
126
385
  [contextId]
127
386
  );
128
387
  const now = Date.now();
129
- return rows.filter((r) => now - r.updated_at <= this.ttlMs).map((r) => JSON.parse(r.payload));
388
+ return rows.map((r) => ({ task: JSON.parse(r.payload), updatedAt: r.updated_at })).filter(
389
+ ({ task, updatedAt }) => now - updatedAt <= this.ttlMs || hasPendingPaymentRecovery(task)
390
+ ).map(({ task }) => task);
130
391
  }
131
392
  };
393
+ function executionColumns(task) {
394
+ const inspection = inspectTaskExecution(task);
395
+ return inspection.state === "valid" ? [inspection.marker.requestId, inspection.marker.lease.expiresAt] : [null, null];
396
+ }
132
397
  export {
133
398
  A2A_ERROR_CODES,
134
399
  CompositeObserver,
@@ -137,8 +402,15 @@ export {
137
402
  InMemoryTaskStore,
138
403
  KvNonceStore,
139
404
  KvRateLimitStore,
405
+ MPP_CHARGE_PROTOCOL_VERSION,
140
406
  MemoryNonceStore,
407
+ MemoryPaymentOperations,
408
+ MemoryPaymentRecoveryStore,
141
409
  MemoryRateLimitStore,
410
+ PAYMENT_PROTOCOL_VERSION,
411
+ PAYMENT_RECOVERY_VERSION,
412
+ PaymentRecoveryFenceError,
413
+ SqlPaymentRecoveryStore,
142
414
  SqlPushNotificationStore,
143
415
  SqlTaskStore,
144
416
  checkRateLimit,
@@ -147,16 +419,26 @@ export {
147
419
  createPublishRoutes,
148
420
  d1ToSqlAdapter,
149
421
  defaultVerifyApiKey,
422
+ deliverDemoPushNotifications,
150
423
  deliverPushNotifications,
151
424
  detectInjection,
152
425
  filterConsumerMessages,
153
426
  filterConsumerMessagesStrict,
154
427
  generateRequestId,
155
428
  isApiKeyAuthEnabled,
429
+ isAtomicNonceStore,
156
430
  isMppAuthEnabled,
431
+ mppPaymentCredential,
432
+ mppPaymentOperationId,
433
+ mppReplayNonceKey,
434
+ reclaimPayment,
435
+ recoverPayment,
436
+ recoverPayments,
157
437
  redactSystemPromptFromOutput,
438
+ validatePushNotificationUrl,
158
439
  verifyApiKeyFromStore,
159
440
  verifyMpp,
441
+ verifyMppCredential,
160
442
  verifyX402
161
443
  };
162
444
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/a2a/task-store-sql.ts"],"sourcesContent":["/**\n * @stable\n *\n * Durable `TaskStore` against any SQL store. Adapter-agnostic: callers wire a\n * `SqlAdapter` against their driver (D1, postgres, sqlite, libSQL, Turso) and\n * the same store survives gateway restarts so an in-flight task (and its\n * artifacts) is recoverable after a Worker recycle.\n *\n * Schema is one table: tasks keyed by id with the full JSON payload, plus a\n * secondary index on `context_id` so `tasks/resubscribe` and conversational\n * lookups by context are O(log n). TTL is enforced at read time the same way\n * `InMemoryTaskStore` does — the gateway is single-writer per task id so a\n * stale row is invisible to callers regardless of when the row is physically\n * deleted.\n *\n * Why not bake in a specific driver? Hono workers run on Cloudflare (D1),\n * Node (pg / sqlite), Bun, Deno. Burning a hard dependency on one client\n * limits the gateway's reach. The adapter indirection costs ~5 lines per\n * driver in the consumer's code and keeps the package free of native deps.\n *\n * @example D1\n * import { SqlTaskStore, d1ToSqlAdapter } from '@tangle-network/agent-gateway'\n * const store = new SqlTaskStore(d1ToSqlAdapter(env.DB))\n * await store.migrate()\n * const gw = createAgentGateway({ ..., a2a: { taskStore: store } })\n *\n * @example libSQL / Turso\n * import { createClient } from '@libsql/client'\n * const client = createClient({ url: process.env.TURSO_URL!, authToken: process.env.TURSO_TOKEN! })\n * const libsql: SqlAdapter = {\n * exec: async (sql, params = []) => {\n * const r = await client.execute({ sql, args: params as never[] })\n * return { rowsAffected: Number(r.rowsAffected ?? 0) }\n * },\n * query: async (sql, params = []) => {\n * const r = await client.execute({ sql, args: params as never[] })\n * return r.rows as unknown as Record<string, unknown>[]\n * },\n * }\n * const store = new SqlTaskStore(libsql)\n * await store.migrate()\n */\n\nimport type { TaskStore } from './task-store'\nimport type { Task } from './types'\n\n/**\n * Minimal SQL driver shape — identical to agent-runtime's `SqlAdapter` so the\n * same wrapper code works for both packages. Parameter placeholders MUST be\n * `?` (positional); driver wrappers that use `$1`, `$2`, … should rewrite at\n * the adapter boundary (see node-postgres example in the durability docs).\n */\nexport interface SqlAdapter {\n exec(sql: string, params?: readonly unknown[]): Promise<{ rowsAffected: number }>\n query<TRow = Record<string, unknown>>(\n sql: string,\n params?: readonly unknown[],\n ): Promise<TRow[]>\n}\n\n/**\n * Adapt a Cloudflare D1 binding to `SqlAdapter`. The package never imports\n * `@cloudflare/workers-types`; the binding's structural shape lines up via\n * TypeScript structural compatibility.\n */\nexport function d1ToSqlAdapter(db: D1DatabaseLike): SqlAdapter {\n return {\n async exec(sql, params = []) {\n const stmt = db.prepare(sql)\n const bound = params.length > 0 ? stmt.bind(...params) : stmt\n const result = await bound.run()\n const meta = (result as { meta?: { rows_written?: number; changes?: number } }).meta\n return { rowsAffected: meta?.rows_written ?? meta?.changes ?? 0 }\n },\n async query<TRow>(sql: string, params: readonly unknown[] = []): Promise<TRow[]> {\n const stmt = db.prepare(sql)\n const bound = params.length > 0 ? stmt.bind(...params) : stmt\n const result = await bound.all<TRow>()\n return result.results ?? []\n },\n }\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1StmtLike\n}\nexport interface D1StmtLike {\n bind(...params: unknown[]): D1StmtLike\n run(): Promise<unknown>\n all<TRow = unknown>(): Promise<{ results?: TRow[] }>\n}\n\nconst DEFAULT_TTL_MS = 60 * 60 * 1000\n\nconst TASKS_TABLE_DDL = (table: string) => `\n CREATE TABLE IF NOT EXISTS ${table} (\n id TEXT PRIMARY KEY,\n context_id TEXT NOT NULL,\n state TEXT NOT NULL,\n payload TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n )\n`\nconst CTX_INDEX_DDL = (table: string) => `\n CREATE INDEX IF NOT EXISTS idx_${table}_context ON ${table} (context_id, updated_at)\n`\n\n/**\n * SQL-backed TaskStore. Stores the full Task JSON; reads return a deep clone\n * so callers never observe shared references. TTL is enforced at read time:\n * expired rows are filtered out and (best-effort) deleted, matching the\n * in-memory store's semantics so behavior is portable across both adapters.\n */\nexport class SqlTaskStore implements TaskStore {\n constructor(\n private readonly db: SqlAdapter,\n private readonly opts: { ttlMs?: number; table?: string } = {},\n ) {}\n\n private get ttlMs(): number {\n return this.opts.ttlMs ?? DEFAULT_TTL_MS\n }\n private get table(): string {\n return this.opts.table ?? 'a2a_tasks'\n }\n\n /** Idempotent. Call once at deploy. */\n async migrate(): Promise<void> {\n await this.db.exec(TASKS_TABLE_DDL(this.table))\n await this.db.exec(CTX_INDEX_DDL(this.table))\n }\n\n async get(id: string): Promise<Task | undefined> {\n const rows = await this.db.query<{ payload: string; updated_at: number }>(\n `SELECT payload, updated_at FROM ${this.table} WHERE id = ?`,\n [id],\n )\n const row = rows[0]\n if (!row) return undefined\n if (Date.now() - row.updated_at > this.ttlMs) {\n // Lazy GC. If the delete loses a race with another reader, that reader\n // observes either the stale-then-deleted task (returning undefined here)\n // or, after this delete commits, observes undefined directly — either\n // way callers see consistent \"expired\" semantics.\n void this.db.exec(`DELETE FROM ${this.table} WHERE id = ?`, [id])\n return undefined\n }\n return JSON.parse(row.payload) as Task\n }\n\n async put(task: Task): Promise<void> {\n const payload = JSON.stringify(task)\n const updatedAt = Date.now()\n // Adapter-agnostic upsert: try update, fall back to insert if no row\n // existed. Avoids needing ON CONFLICT (postgres) vs INSERT OR REPLACE\n // (sqlite/libSQL) divergence at the SQL layer.\n const updated = await this.db.exec(\n `UPDATE ${this.table} SET context_id = ?, state = ?, payload = ?, updated_at = ? WHERE id = ?`,\n [task.contextId, task.status.state, payload, updatedAt, task.id],\n )\n if (updated.rowsAffected === 0) {\n await this.db.exec(\n `INSERT INTO ${this.table} (id, context_id, state, payload, updated_at) VALUES (?, ?, ?, ?, ?)`,\n [task.id, task.contextId, task.status.state, payload, updatedAt],\n )\n }\n }\n\n async delete(id: string): Promise<void> {\n await this.db.exec(`DELETE FROM ${this.table} WHERE id = ?`, [id])\n }\n\n /**\n * Lookup tasks by contextId — used by `tasks/resubscribe` and the multi-turn\n * dispatcher. Returns most-recent-first. Not part of the base TaskStore\n * interface since the in-memory store doesn't expose it; consumers that\n * specifically wire SqlTaskStore can use it for richer queries.\n */\n async listByContext(contextId: string): Promise<Task[]> {\n const rows = await this.db.query<{ payload: string; updated_at: number }>(\n `SELECT payload, updated_at FROM ${this.table} WHERE context_id = ? ORDER BY updated_at DESC`,\n [contextId],\n )\n const now = Date.now()\n return rows\n .filter((r) => now - r.updated_at <= this.ttlMs)\n .map((r) => JSON.parse(r.payload) as Task)\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/payment-recovery-sql.ts","../src/a2a/task-store-sql.ts"],"sourcesContent":["import type { SqlAdapter } from './a2a/task-store-sql'\nimport type {\n PaymentRecoveryRecord,\n PaymentRecoveryStore,\n} from './payment-recovery'\n\nconst TABLE_DDL = (table: string) => `\n CREATE TABLE IF NOT EXISTS ${table} (\n id TEXT PRIMARY KEY,\n state TEXT NOT NULL,\n next_attempt_at INTEGER NOT NULL,\n revision INTEGER NOT NULL,\n payload TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n )\n`\n\nconst DUE_INDEX_DDL = (table: string) => `\n CREATE INDEX IF NOT EXISTS idx_${table}_due\n ON ${table} (state, next_attempt_at)\n`\n\n/** Durable recovery outbox for D1, sqlite, libSQL, or an adapted SQL driver. */\nexport class SqlPaymentRecoveryStore implements PaymentRecoveryStore {\n private readonly table: string\n\n constructor(\n private readonly db: SqlAdapter,\n options: { table?: string } = {},\n ) {\n this.table = options.table ?? 'payment_recovery'\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(this.table)) {\n throw new Error('payment recovery table name is invalid')\n }\n }\n\n /** Idempotent. Run this before the gateway starts accepting traffic. */\n async migrate(): Promise<void> {\n await this.db.exec(TABLE_DDL(this.table))\n await this.db.exec(DUE_INDEX_DDL(this.table))\n }\n\n async createIfAbsent(record: PaymentRecoveryRecord): Promise<boolean> {\n try {\n const result = await this.db.exec(\n `INSERT INTO ${this.table} (id, state, next_attempt_at, revision, payload, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,\n [\n record.id,\n record.state,\n record.nextAttemptAt,\n record.revision,\n JSON.stringify(record),\n record.updatedAt,\n ],\n )\n return result.rowsAffected === 1\n } catch (error) {\n if (await this.get(record.id)) return false\n throw error\n }\n }\n\n async get(id: string): Promise<PaymentRecoveryRecord | undefined> {\n const rows = await this.db.query<{ payload: string }>(\n `SELECT payload FROM ${this.table} WHERE id = ?`,\n [id],\n )\n return rows[0] ? parseRecord(rows[0].payload) : undefined\n }\n\n async compareAndSet(\n expected: PaymentRecoveryRecord,\n next: PaymentRecoveryRecord,\n ): Promise<boolean> {\n const result = await this.db.exec(\n `UPDATE ${this.table} SET state = ?, next_attempt_at = ?, revision = ?, payload = ?, updated_at = ? WHERE id = ? AND revision = ?`,\n [\n next.state,\n next.nextAttemptAt,\n next.revision,\n JSON.stringify(next),\n next.updatedAt,\n expected.id,\n expected.revision,\n ],\n )\n return result.rowsAffected === 1\n }\n\n async listDue(now: number, limit: number): Promise<PaymentRecoveryRecord[]> {\n if (!Number.isSafeInteger(limit) || limit <= 0) {\n throw new Error('payment recovery scan limit must be a positive safe integer')\n }\n const rows = await this.db.query<{ payload: string }>(\n `SELECT payload FROM ${this.table} WHERE state <> ? AND next_attempt_at <= ? ORDER BY next_attempt_at ASC LIMIT ?`,\n ['reconciled', now, limit],\n )\n return rows.map((row) => parseRecord(row.payload))\n }\n}\n\nfunction parseRecord(payload: string): PaymentRecoveryRecord {\n const value = JSON.parse(payload) as PaymentRecoveryRecord\n if (value.version !== 1 || !value.id || !Number.isSafeInteger(value.revision)) {\n throw new Error('stored payment recovery record is invalid')\n }\n return value\n}\n","/**\n * @stable\n *\n * Durable `TaskStore` against any SQL store. Adapter-agnostic: callers wire a\n * `SqlAdapter` against their driver (D1, postgres, sqlite, libSQL, Turso) and\n * the same store survives gateway restarts so an in-flight task (and its\n * artifacts) is recoverable after a Worker recycle.\n *\n * Schema is one table: tasks keyed by id with the full JSON payload, plus a\n * secondary index on `context_id` so `tasks/resubscribe` and conversational\n * lookups by context are O(log n). TTL is enforced at read time the same way\n * `InMemoryTaskStore` does — `createIfAbsent` and `compareAndSet` make task\n * ownership safe when multiple gateway workers share the database.\n *\n * Why not bake in a specific driver? Hono workers run on Cloudflare (D1),\n * Node (pg / sqlite), Bun, Deno. Burning a hard dependency on one client\n * limits the gateway's reach. The adapter indirection costs ~5 lines per\n * driver in the consumer's code and keeps the package free of native deps.\n *\n * @example D1\n * import { SqlTaskStore, d1ToSqlAdapter } from '@tangle-network/agent-gateway'\n * const store = new SqlTaskStore(d1ToSqlAdapter(env.DB))\n * await store.migrate()\n * const gw = createAgentGateway({ ..., a2a: { taskStore: store } })\n *\n * @example libSQL / Turso\n * import { createClient } from '@libsql/client'\n * const client = createClient({ url: process.env.TURSO_URL!, authToken: process.env.TURSO_TOKEN! })\n * const libsql: SqlAdapter = {\n * exec: async (sql, params = []) => {\n * const r = await client.execute({ sql, args: params as never[] })\n * return { rowsAffected: Number(r.rowsAffected ?? 0) }\n * },\n * query: async (sql, params = []) => {\n * const r = await client.execute({ sql, args: params as never[] })\n * return r.rows as unknown as Record<string, unknown>[]\n * },\n * }\n * const store = new SqlTaskStore(libsql)\n * await store.migrate()\n */\n\nimport { inspectTaskExecution } from './execution-fence'\nimport { hasPendingPaymentRecovery, type TaskStore } from './task-store'\nimport type { Task } from './types'\n\n/**\n * Minimal SQL driver shape — identical to agent-runtime's `SqlAdapter` so the\n * same wrapper code works for both packages. Parameter placeholders MUST be\n * `?` (positional); driver wrappers that use `$1`, `$2`, … should rewrite at\n * the adapter boundary (see node-postgres example in the durability docs).\n */\nexport interface SqlAdapter {\n exec(sql: string, params?: readonly unknown[]): Promise<{ rowsAffected: number }>\n query<TRow = Record<string, unknown>>(\n sql: string,\n params?: readonly unknown[],\n ): Promise<TRow[]>\n}\n\n/**\n * Adapt a Cloudflare D1 binding to `SqlAdapter`. The package never imports\n * `@cloudflare/workers-types`; the binding's structural shape lines up via\n * TypeScript structural compatibility.\n */\nexport function d1ToSqlAdapter(db: D1DatabaseLike): SqlAdapter {\n return {\n async exec(sql, params = []) {\n const stmt = db.prepare(sql)\n const bound = params.length > 0 ? stmt.bind(...params) : stmt\n const result = await bound.run()\n const meta = (result as { meta?: { rows_written?: number; changes?: number } }).meta\n return { rowsAffected: meta?.rows_written ?? meta?.changes ?? 0 }\n },\n async query<TRow>(sql: string, params: readonly unknown[] = []): Promise<TRow[]> {\n const stmt = db.prepare(sql)\n const bound = params.length > 0 ? stmt.bind(...params) : stmt\n const result = await bound.all<TRow>()\n return result.results ?? []\n },\n }\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1StmtLike\n}\nexport interface D1StmtLike {\n bind(...params: unknown[]): D1StmtLike\n run(): Promise<unknown>\n all<TRow = unknown>(): Promise<{ results?: TRow[] }>\n}\n\nconst DEFAULT_TTL_MS = 60 * 60 * 1000\n\nconst TASKS_TABLE_DDL = (table: string) => `\n CREATE TABLE IF NOT EXISTS ${table} (\n id TEXT PRIMARY KEY,\n context_id TEXT NOT NULL,\n state TEXT NOT NULL,\n payload TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n execution_request_id TEXT,\n execution_lease_expires_at REAL\n )\n`\nconst CTX_INDEX_DDL = (table: string) => `\n CREATE INDEX IF NOT EXISTS idx_${table}_context ON ${table} (context_id, updated_at)\n`\n\n/**\n * SQL-backed TaskStore. Stores the full Task JSON; reads return a deep clone\n * so callers never observe shared references. TTL is enforced at read time:\n * expired rows are filtered out and (best-effort) deleted, matching the\n * in-memory store's semantics so behavior is portable across both adapters.\n */\nexport class SqlTaskStore implements TaskStore {\n constructor(\n private readonly db: SqlAdapter,\n private readonly opts: { ttlMs?: number; table?: string } = {},\n ) {}\n\n private get ttlMs(): number {\n return this.opts.ttlMs ?? DEFAULT_TTL_MS\n }\n private get table(): string {\n return this.opts.table ?? 'a2a_tasks'\n }\n\n private async readRow(id: string): Promise<{\n payload: string\n updatedAt: number\n executionRequestId: string | null\n executionLeaseExpiresAt: number | null\n } | undefined> {\n const rows = await this.db.query<{\n payload: string\n updated_at: number\n execution_request_id?: string | null\n execution_lease_expires_at?: number | null\n }>(\n `SELECT payload, updated_at, execution_request_id, execution_lease_expires_at FROM ${this.table} WHERE id = ?`,\n [id],\n )\n const row = rows[0]\n return row\n ? {\n payload: row.payload,\n updatedAt: row.updated_at,\n executionRequestId: row.execution_request_id ?? null,\n executionLeaseExpiresAt: row.execution_lease_expires_at ?? null,\n }\n : undefined\n }\n\n private isExpired(updatedAt: number, task: Task): boolean {\n return Date.now() - updatedAt > this.ttlMs && !hasPendingPaymentRecovery(task)\n }\n\n private async deleteObservedRow(\n id: string,\n payload: string,\n updatedAt: number,\n ): Promise<number> {\n const result = await this.db.exec(\n `DELETE FROM ${this.table} WHERE id = ? AND payload = ? AND updated_at = ?`,\n [id, payload, updatedAt],\n )\n return result.rowsAffected\n }\n\n /** Idempotent. Call once at deploy. */\n async migrate(): Promise<void> {\n await this.db.exec(TASKS_TABLE_DDL(this.table))\n for (const column of [\n 'execution_request_id TEXT',\n 'execution_lease_expires_at REAL',\n ]) {\n try {\n await this.db.exec(`ALTER TABLE ${this.table} ADD COLUMN ${column}`)\n } catch (error) {\n const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase()\n if (!message.includes('duplicate column') && !message.includes('already exists')) throw error\n }\n }\n await this.db.exec(CTX_INDEX_DDL(this.table))\n }\n\n async get(id: string): Promise<Task | undefined> {\n const row = await this.readRow(id)\n if (!row) return undefined\n const task = JSON.parse(row.payload) as Task\n if (this.isExpired(row.updatedAt, task)) {\n // Delete only the version that was observed as stale. A refresh can reuse\n // the same payload, so payload equality and updated_at both fence the row.\n await this.deleteObservedRow(id, row.payload, row.updatedAt)\n return undefined\n }\n return task\n }\n\n private async insert(task: Task): Promise<number> {\n const payload = JSON.stringify(task)\n const [executionRequestId, executionLeaseExpiresAt] = executionColumns(task)\n const result = await this.db.exec(\n `INSERT INTO ${this.table} (id, context_id, state, payload, updated_at, execution_request_id, execution_lease_expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n [\n task.id,\n task.contextId,\n task.status.state,\n payload,\n Date.now(),\n executionRequestId,\n executionLeaseExpiresAt,\n ],\n )\n return result.rowsAffected\n }\n\n async put(task: Task): Promise<void> {\n const payload = JSON.stringify(task)\n const updatedAt = Date.now()\n const [executionRequestId, executionLeaseExpiresAt] = executionColumns(task)\n // Adapter-agnostic upsert: try update, fall back to insert if no row\n // existed. Avoids needing ON CONFLICT (postgres) vs INSERT OR REPLACE\n // (sqlite/libSQL) divergence at the SQL layer.\n const updated = await this.db.exec(\n `UPDATE ${this.table}\n SET context_id = ?, state = ?, payload = ?, updated_at = ?,\n execution_request_id = ?, execution_lease_expires_at = ?\n WHERE id = ?`,\n [\n task.contextId,\n task.status.state,\n payload,\n updatedAt,\n executionRequestId,\n executionLeaseExpiresAt,\n task.id,\n ],\n )\n if (updated.rowsAffected === 0) {\n await this.db.exec(\n `INSERT INTO ${this.table} (id, context_id, state, payload, updated_at, execution_request_id, execution_lease_expires_at)\n VALUES (?, ?, ?, ?, ?, ?, ?)`,\n [\n task.id,\n task.contextId,\n task.status.state,\n payload,\n updatedAt,\n executionRequestId,\n executionLeaseExpiresAt,\n ],\n )\n }\n }\n\n async createIfAbsent(task: Task): Promise<boolean> {\n try {\n return (await this.insert(task)) === 1\n } catch (error) {\n // SQL dialects report duplicate primary keys as errors. Inspect the raw\n // row so an expired row can be removed and retried in the same call.\n const row = await this.readRow(task.id)\n if (!row) throw error\n const existing = JSON.parse(row.payload) as Task\n if (!this.isExpired(row.updatedAt, existing)) return false\n\n await this.deleteObservedRow(task.id, row.payload, row.updatedAt)\n try {\n return (await this.insert(task)) === 1\n } catch (retryError) {\n // Another writer may have won the retry after the stale row was\n // removed. Return the normal idempotency result in that case.\n if (await this.get(task.id)) return false\n throw retryError\n }\n }\n }\n\n async compareAndSet(expected: Task, next: Task): Promise<boolean> {\n const expectedPayload = JSON.stringify(expected)\n const payload = JSON.stringify(next)\n const [executionRequestId, executionLeaseExpiresAt] = executionColumns(next)\n const result = await this.db.exec(\n `UPDATE ${this.table}\n SET context_id = ?, state = ?, payload = ?, updated_at = ?,\n execution_request_id = ?, execution_lease_expires_at = ?\n WHERE id = ? AND payload = ?`,\n [\n next.contextId,\n next.status.state,\n payload,\n Date.now(),\n executionRequestId,\n executionLeaseExpiresAt,\n expected.id,\n expectedPayload,\n ],\n )\n return result.rowsAffected === 1\n }\n\n async compareAndSetExecution(\n expected: Task,\n next: Task,\n requestId: string,\n now: number,\n ): Promise<boolean> {\n const expectedMarker = inspectTaskExecution(expected)\n const nextMarker = inspectTaskExecution(next)\n if (\n expectedMarker.state !== 'valid' ||\n nextMarker.state !== 'valid' ||\n expectedMarker.marker.requestId !== requestId ||\n nextMarker.marker.requestId !== requestId ||\n expectedMarker.marker.lease.expiresAt <= now ||\n nextMarker.marker.lease.expiresAt <= now\n ) return false\n const expectedPayload = JSON.stringify(expected)\n const payload = JSON.stringify(next)\n const updatedAt = Date.now()\n // Both NULL columns identify a legacy row whose payload still owns the fence.\n const result = await this.db.exec(\n `UPDATE ${this.table}\n SET context_id = ?, state = ?, payload = ?, updated_at = ?,\n execution_request_id = ?, execution_lease_expires_at = ?\n WHERE id = ?\n AND payload = ?\n AND state = 'working'\n AND (\n (execution_request_id = ? AND execution_lease_expires_at > ?)\n OR (execution_request_id IS NULL AND execution_lease_expires_at IS NULL)\n )`,\n [\n next.contextId,\n next.status.state,\n payload,\n updatedAt,\n nextMarker.marker.requestId,\n nextMarker.marker.lease.expiresAt,\n expected.id,\n expectedPayload,\n requestId,\n now,\n ],\n )\n return result.rowsAffected === 1\n }\n\n async delete(id: string): Promise<void> {\n const task = await this.get(id)\n if (!task || hasPendingPaymentRecovery(task)) return\n await this.db.exec(\n `DELETE FROM ${this.table} WHERE id = ? AND payload = ?`,\n [id, JSON.stringify(task)],\n )\n }\n\n /**\n * Lookup tasks by contextId — used by `tasks/resubscribe` and the multi-turn\n * dispatcher. Returns most-recent-first. Not part of the base TaskStore\n * interface since the in-memory store doesn't expose it; consumers that\n * specifically wire SqlTaskStore can use it for richer queries.\n */\n async listByContext(contextId: string): Promise<Task[]> {\n const rows = await this.db.query<{ payload: string; updated_at: number }>(\n `SELECT payload, updated_at FROM ${this.table} WHERE context_id = ? ORDER BY updated_at DESC`,\n [contextId],\n )\n const now = Date.now()\n return rows\n .map((r) => ({ task: JSON.parse(r.payload) as Task, updatedAt: r.updated_at }))\n .filter(({ task, updatedAt }) =>\n now - updatedAt <= this.ttlMs || hasPendingPaymentRecovery(task),\n )\n .map(({ task }) => task)\n }\n}\n\nfunction executionColumns(task: Task): [string | null, number | null] {\n const inspection = inspectTaskExecution(task)\n return inspection.state === 'valid'\n ? [inspection.marker.requestId, inspection.marker.lease.expiresAt]\n : [null, null]\n}\n"],"mappings":"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","names":[]}
@@ -1,8 +1,9 @@
1
1
  import * as hono_types from 'hono/types';
2
2
  import { Hono } from 'hono';
3
- import { G as GatewayConfig } from './types-DEsMmS-X.js';
3
+ import { o as CreateAgentGatewayConfig } from './types-oQ58UakD.js';
4
4
  import './nonce-store.js';
5
5
  import './rate-limit.js';
6
+ import './observer-types-A0RtA8uL.js';
6
7
 
7
8
  /**
8
9
  * Create a Hono router that serves the agent gateway.
@@ -14,6 +15,10 @@ import './rate-limit.js';
14
15
  * GET /:slug/chat/completions — agent discovery metadata (Tangle-native shape)
15
16
  * POST /:slug/chat/completions — OpenAI-compatible chat endpoint (paid)
16
17
  */
17
- declare function createAgentGateway(config: GatewayConfig): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
18
+ declare function createAgentGateway(inputConfig: CreateAgentGatewayConfig): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
19
+ /** Public package-boundary version marker for durable payment operations. */
20
+ declare namespace createAgentGateway {
21
+ const paymentProtocolVersion: 2;
22
+ }
18
23
 
19
24
  export { createAgentGateway };
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  createAgentGateway
3
- } from "./chunk-Q4YAIEZY.js";
4
- import "./chunk-M7ZJAK4K.js";
3
+ } from "./chunk-C7Z2BRYV.js";
4
+ import "./chunk-J5SDVHOL.js";
5
+ import "./chunk-GITV7CPT.js";
5
6
  import "./chunk-XCTXHZ76.js";
6
7
  export {
7
8
  createAgentGateway
@@ -3,16 +3,33 @@
3
3
  * Tracks seen nonces to prevent the same payment from being used twice.
4
4
  */
5
5
  interface NonceStore {
6
- /** Check if nonce has been seen. Returns true if already used (reject). */
6
+ /** Check if nonce has been seen. This method never grants ownership. */
7
7
  hasSeen(nonce: string): Promise<boolean>;
8
- /** Mark nonce as used. TTL = how long to remember it (seconds). */
9
- markSeen(nonce: string, ttlSeconds: number): Promise<void>;
8
+ /**
9
+ * Atomically claim a nonce. An owner id makes a retry by the same payment
10
+ * operation idempotent. This is optional only for the 0.7.1 check-and-mark
11
+ * compatibility contract; durable owner claims require this method.
12
+ */
13
+ claim?(nonce: string, ttlSeconds: number, ownerId?: string): Promise<boolean>;
14
+ /** @deprecated Use claim() for atomic ownership in new stores. */
15
+ markSeen?(nonce: string, ttlSeconds: number): Promise<void>;
16
+ }
17
+ interface AtomicNonceStore extends NonceStore {
18
+ claim(nonce: string, ttlSeconds: number, ownerId?: string): Promise<boolean>;
10
19
  }
20
+ /**
21
+ * Return the seconds for which a signed nonce must remain stored.
22
+ *
23
+ * The signed expiry is the replay boundary. A fixed one-hour cap would allow
24
+ * a still-valid authorization to replay after the nonce entry expires.
25
+ */
26
+ declare function nonceTtlSeconds(expiry: bigint, nowSeconds?: number): number | undefined;
11
27
  /** In-memory nonce store with automatic eviction. Use in tests or single-worker deploys. */
12
28
  declare class MemoryNonceStore implements NonceStore {
13
29
  private seen;
14
30
  private lastEviction;
15
31
  hasSeen(nonce: string): Promise<boolean>;
32
+ claim(nonce: string, ttlSeconds: number, ownerId?: string): Promise<boolean>;
16
33
  markSeen(nonce: string, ttlSeconds: number): Promise<void>;
17
34
  private evictExpired;
18
35
  }
@@ -28,20 +45,31 @@ interface KVNamespace {
28
45
  put(key: string, value: string, options?: {
29
46
  expirationTtl?: number;
30
47
  }): Promise<void>;
48
+ /** Optional linearizable create-if-absent extension. Cloudflare KV does not provide it. */
49
+ putIfAbsent?(key: string, value: string, options?: {
50
+ expirationTtl?: number;
51
+ }): Promise<boolean>;
31
52
  delete(key: string): Promise<void>;
32
53
  }
54
+ /** Atomic claim supplied by D1, a Durable Object, or another linearizable store. */
55
+ type AtomicKvNonceClaim = (key: string, ttlSeconds: number, ownerId?: string) => Promise<boolean>;
56
+ interface KvNonceStoreOptions {
57
+ /**
58
+ * Claim the fully namespaced key atomically.
59
+ * The callback must make same-owner retries idempotent.
60
+ */
61
+ atomicClaim?: AtomicKvNonceClaim;
62
+ }
33
63
  /**
34
64
  * KV-backed NonceStore for distributed Cloudflare Workers deployments.
35
65
  *
36
66
  * Why this exists: MemoryNonceStore works on a single worker instance, but
37
67
  * Cloudflare routes requests across multiple isolates. Without shared state,
38
68
  * an attacker could retry a replayed nonce against a different isolate and
39
- * have it accepted. This implementation uses Workers KV with native TTL so
40
- * the nonce automatically expires at payment-expiry time.
41
- *
42
- * TTL precision: KV is eventually consistent (propagation ~60s). For x402
43
- * with 10-minute expiry windows this is fine — by the time KV propagates,
44
- * the payment itself would be expired anyway.
69
+ * have it accepted. Cloudflare KV has no conditional write, so a plain KV
70
+ * binding is not an atomic payment store. Supply `atomicClaim` from D1,
71
+ * Durable Objects, or another linearizable service before using this store
72
+ * for paid requests.
45
73
  *
46
74
  * Usage:
47
75
  * const nonceStore = new KvNonceStore(env.NONCE_KV, 'x402')
@@ -51,12 +79,20 @@ declare class KvNonceStore implements NonceStore {
51
79
  private readonly kv;
52
80
  /** Key prefix to namespace within a shared KV (default: "nonce"). */
53
81
  private readonly prefix;
82
+ private readonly atomicClaim?;
54
83
  constructor(kv: KVNamespace,
55
84
  /** Key prefix to namespace within a shared KV (default: "nonce"). */
56
- prefix?: string);
85
+ prefix?: string, options?: KvNonceStoreOptions);
57
86
  hasSeen(nonce: string): Promise<boolean>;
58
87
  markSeen(nonce: string, ttlSeconds: number): Promise<void>;
88
+ claim(nonce: string, ttlSeconds: number, ownerId?: string): Promise<boolean>;
89
+ /** Used by gateway validation to reject plain, non-atomic KV bindings. */
90
+ hasAtomicClaim(): boolean;
59
91
  private key;
60
92
  }
93
+ /** Claim through the one atomic contract used by every payment path. */
94
+ declare function claimStoredNonce(store: NonceStore, nonce: string, ttlSeconds: number, ownerId?: string): Promise<boolean>;
95
+ /** Durable payment paths must use a store with a single atomic claim operation. */
96
+ declare function isAtomicNonceStore(store: NonceStore): store is AtomicNonceStore;
61
97
 
62
- export { type KVNamespace, KvNonceStore, MemoryNonceStore, type NonceStore };
98
+ export { type AtomicKvNonceClaim, type AtomicNonceStore, type KVNamespace, KvNonceStore, type KvNonceStoreOptions, MemoryNonceStore, type NonceStore, claimStoredNonce, isAtomicNonceStore, nonceTtlSeconds };
@@ -1,9 +1,15 @@
1
1
  import {
2
2
  KvNonceStore,
3
- MemoryNonceStore
4
- } from "./chunk-M7ZJAK4K.js";
3
+ MemoryNonceStore,
4
+ claimStoredNonce,
5
+ isAtomicNonceStore,
6
+ nonceTtlSeconds
7
+ } from "./chunk-J5SDVHOL.js";
5
8
  export {
6
9
  KvNonceStore,
7
- MemoryNonceStore
10
+ MemoryNonceStore,
11
+ claimStoredNonce,
12
+ isAtomicNonceStore,
13
+ nonceTtlSeconds
8
14
  };
9
15
  //# sourceMappingURL=nonce-store.js.map