@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
@@ -0,0 +1,431 @@
1
+ import type { AuthorizedRequest } from '../dispatch'
2
+ import type { PaymentOperations, PaymentOperation } from '../payment-operations'
3
+ import {
4
+ deserializePaymentOperation,
5
+ serializePaymentOperation,
6
+ type PaymentRecoveryConfig,
7
+ type PaymentRecoveryRecord,
8
+ type SerializedPaymentOperation,
9
+ } from '../payment-recovery'
10
+ import type { SandboxUsageReceipt } from '../types'
11
+ import type { Task } from './types'
12
+ import {
13
+ asError,
14
+ compareAndSetTask,
15
+ clearTaskMetadata,
16
+ cryptoRandomId,
17
+ withStatus,
18
+ type TaskStateStore,
19
+ } from './task-state'
20
+
21
+ const PAYMENT_RELEASE_METADATA_KEY = 'gatewayPaymentRelease'
22
+ const PAYMENT_RECOVERY_METADATA_KEY = 'gatewayPaymentRecovery'
23
+ const PAYMENT_RELEASE_LEASE_MS = 5 * 60 * 1000
24
+
25
+ interface TaskPaymentRecoveryMarker {
26
+ version: 1
27
+ id: string
28
+ }
29
+
30
+ interface PaymentReleaseRecord {
31
+ version: 1
32
+ lease: { id: string; expiresAt: number }
33
+ agentSlug: string
34
+ requestId: string
35
+ operationId: string
36
+ paymentOperation: SerializedPaymentOperation
37
+ reason: string
38
+ recoveryAttempts?: number
39
+ recoveryError?: string
40
+ }
41
+
42
+ export interface PaymentRecoveryDependencies {
43
+ taskStore: TaskStateStore
44
+ paymentOperations?: PaymentOperations
45
+ paymentRecovery?: PaymentRecoveryConfig
46
+ releasePayment: (authz: AuthorizedRequest, reason: string) => Promise<void>
47
+ releasePaymentAfterFailure: (
48
+ authz: AuthorizedRequest,
49
+ reason: string,
50
+ workObserved: boolean,
51
+ ) => Promise<void>
52
+ recoverDurablePayment: (
53
+ recoveryId: string,
54
+ options?: { force?: boolean; usage?: SandboxUsageReceipt },
55
+ ) => Promise<PaymentRecoveryRecord | undefined>
56
+ deliverPush: (task: Task) => Promise<void>
57
+ }
58
+
59
+ export async function attachPaymentRecoveryMarker(
60
+ taskStore: TaskStateStore,
61
+ task: Task,
62
+ recoveryId: string | undefined,
63
+ ): Promise<Task> {
64
+ if (!recoveryId) return task
65
+ const existing = readPaymentRecoveryMarker(task)
66
+ if (existing) {
67
+ if (existing.id !== recoveryId) {
68
+ throw new Error('A2A task already has a different payment recovery identity')
69
+ }
70
+ return task
71
+ }
72
+ const next = withPaymentRecoveryMarker(task, recoveryId)
73
+ if (await compareAndSetTask(taskStore, task, next)) return next
74
+ throw new Error('A2A task changed while payment recovery was attached')
75
+ }
76
+
77
+ /** Attach only as a retention marker. The returned task must never execute. */
78
+ export async function retainPaymentRecoveryMarker(
79
+ taskStore: TaskStateStore,
80
+ task: Task,
81
+ recoveryId: string | undefined,
82
+ ): Promise<Task> {
83
+ if (!recoveryId) return task
84
+ let current = await taskStore.get(task.id) ?? task
85
+ for (let attempt = 0; attempt < 16; attempt += 1) {
86
+ const existing = readPaymentRecoveryMarker(current)
87
+ if (existing) {
88
+ if (existing.id !== recoveryId) {
89
+ throw new Error('A2A task already has a different payment recovery identity')
90
+ }
91
+ return current
92
+ }
93
+ const next = withPaymentRecoveryMarker(current, recoveryId)
94
+ if (await compareAndSetTask(taskStore, current, next)) return next
95
+ const latest = await taskStore.get(task.id)
96
+ if (!latest) throw new Error('A2A task disappeared while payment recovery was retained')
97
+ current = latest
98
+ }
99
+ throw new Error('A2A task changed too many times while payment recovery was retained')
100
+ }
101
+
102
+ export function preservePaymentRecoveryMarker(base: Task, source: Task): Task {
103
+ const marker = readPaymentRecoveryMarker(source)
104
+ return marker ? withPaymentRecoveryMarker(base, marker.id) : base
105
+ }
106
+
107
+ export function readPaymentRecoveryMarker(task: Task): TaskPaymentRecoveryMarker | undefined {
108
+ const raw = task.metadata?.[PAYMENT_RECOVERY_METADATA_KEY]
109
+ if (!raw || typeof raw !== 'object') return undefined
110
+ const marker = raw as Partial<TaskPaymentRecoveryMarker>
111
+ if (marker.version !== 1 || typeof marker.id !== 'string' || marker.id.length === 0) {
112
+ return undefined
113
+ }
114
+ return marker as TaskPaymentRecoveryMarker
115
+ }
116
+
117
+ export function clearPaymentRecoveryMarker(task: Task): Task {
118
+ return clearTaskMetadata(task, PAYMENT_RECOVERY_METADATA_KEY)
119
+ }
120
+
121
+ export function hasPaymentReleaseRecovery(task: Task): boolean {
122
+ return task.metadata?.[PAYMENT_RELEASE_METADATA_KEY] !== undefined
123
+ }
124
+
125
+ export async function releaseTaskPayment(
126
+ authz: AuthorizedRequest,
127
+ task: Task,
128
+ deps: PaymentRecoveryDependencies,
129
+ reason: string,
130
+ workObserved: boolean,
131
+ ): Promise<Task> {
132
+ if (
133
+ !workObserved &&
134
+ !authz.paymentRecoveryId &&
135
+ authz.paymentOperation &&
136
+ deps.paymentOperations
137
+ ) {
138
+ let marked: Task
139
+ try {
140
+ marked = await beginPaymentReleaseRecovery(deps.taskStore, task, authz, reason) ?? task
141
+ } catch (error) {
142
+ console.error(
143
+ '[a2a] failed to persist payment release recovery for ' + authz.requestId + ':',
144
+ error instanceof Error ? error.message : String(error),
145
+ )
146
+ return await deps.taskStore.get(task.id) ?? task
147
+ }
148
+ const record = readPaymentReleaseRecord(marked)
149
+ if (!record) return marked
150
+ try {
151
+ await deps.releasePayment(authz, reason)
152
+ } catch (releaseError) {
153
+ const retained = await retainPaymentReleaseForRecovery(
154
+ deps.taskStore,
155
+ task.id,
156
+ record.lease.id,
157
+ asError(releaseError),
158
+ )
159
+ console.error(
160
+ '[a2a] payment release retained for ' + authz.requestId + ':',
161
+ releaseError instanceof Error ? releaseError.message : String(releaseError),
162
+ )
163
+ return retained ?? marked
164
+ }
165
+ return clearPaymentReleaseRecovery(deps.taskStore, marked, record.lease.id)
166
+ }
167
+
168
+ try {
169
+ await deps.releasePaymentAfterFailure(authz, reason, workObserved)
170
+ } catch (releaseError) {
171
+ console.error(
172
+ `[a2a] payment release failed for ${authz.requestId}:`,
173
+ releaseError instanceof Error ? releaseError.message : String(releaseError),
174
+ )
175
+ }
176
+ const current = await deps.taskStore.get(task.id) ?? task
177
+ return workObserved
178
+ ? current
179
+ : clearReconciledPaymentRecoveryMarker(current, deps)
180
+ }
181
+
182
+ async function beginPaymentReleaseRecovery(
183
+ taskStore: TaskStateStore,
184
+ task: Task,
185
+ authz: AuthorizedRequest,
186
+ reason: string,
187
+ ): Promise<Task | undefined> {
188
+ const record = buildPaymentReleaseRecord(authz, reason)
189
+ if (!record) return undefined
190
+ for (let attempt = 0; attempt < 8; attempt += 1) {
191
+ const current = await taskStore.get(task.id) ?? task
192
+ const existing = readPaymentReleaseRecord(current)
193
+ if (existing) {
194
+ if (existing.operationId !== record.operationId) {
195
+ throw new Error('A2A task already has a different payment release recovery')
196
+ }
197
+ return current
198
+ }
199
+ const next = withPaymentReleaseRecord(current, record)
200
+ if (await compareAndSetTask(taskStore, current, next)) return next
201
+ }
202
+ throw new Error('A2A task changed before payment release recovery was stored')
203
+ }
204
+
205
+ async function retainPaymentReleaseForRecovery(
206
+ taskStore: TaskStateStore,
207
+ taskId: string,
208
+ leaseId: string,
209
+ error: Error,
210
+ ): Promise<Task | undefined> {
211
+ const current = await taskStore.get(taskId)
212
+ if (!current) return undefined
213
+ const record = readPaymentReleaseRecord(current)
214
+ if (!record || record.lease.id !== leaseId) return undefined
215
+ const retry: PaymentReleaseRecord = {
216
+ ...record,
217
+ lease: { id: cryptoRandomId(), expiresAt: Date.now() + PAYMENT_RELEASE_LEASE_MS },
218
+ recoveryAttempts: (record.recoveryAttempts ?? 0) + 1,
219
+ recoveryError: error.message,
220
+ }
221
+ const next = withPaymentReleaseRecord(current, retry)
222
+ if (await compareAndSetTask(taskStore, current, next)) return next
223
+ return await taskStore.get(taskId)
224
+ }
225
+
226
+ async function clearPaymentReleaseRecovery(
227
+ taskStore: TaskStateStore,
228
+ task: Task,
229
+ leaseId: string,
230
+ ): Promise<Task> {
231
+ const current = await taskStore.get(task.id) ?? task
232
+ const record = readPaymentReleaseRecord(current)
233
+ if (!record || record.lease.id !== leaseId) return current
234
+ const cleared = clearPaymentRecoveryMarker(clearPaymentReleaseRecord(current))
235
+ if (await compareAndSetTask(taskStore, current, cleared)) return cleared
236
+ return await taskStore.get(task.id) ?? cleared
237
+ }
238
+
239
+ export async function recoverPaymentReleaseIfNeeded(
240
+ task: Task,
241
+ deps: PaymentRecoveryDependencies,
242
+ ): Promise<Task> {
243
+ const raw = task.metadata?.[PAYMENT_RELEASE_METADATA_KEY]
244
+ if (raw === undefined) return task
245
+ const record = readPaymentReleaseRecord(task)
246
+ if (!record) {
247
+ return expirePaymentRelease(
248
+ task,
249
+ deps,
250
+ new Error('A2A payment release recovery record is missing'),
251
+ )
252
+ }
253
+ if (record.lease.expiresAt > Date.now()) return task
254
+
255
+ const renewed: PaymentReleaseRecord = {
256
+ ...record,
257
+ lease: { id: cryptoRandomId(), expiresAt: Date.now() + PAYMENT_RELEASE_LEASE_MS },
258
+ }
259
+ const leasedTask = withPaymentReleaseRecord(task, renewed)
260
+ if (!await compareAndSetTask(deps.taskStore, task, leasedTask)) {
261
+ return await deps.taskStore.get(task.id) ?? task
262
+ }
263
+
264
+ try {
265
+ if (!deps.paymentOperations) {
266
+ throw new Error('A2A payment release recovery is not configured')
267
+ }
268
+ const operation = deserializePaymentOperation(renewed.paymentOperation)
269
+ await deps.paymentOperations.releasePayment(operation, renewed.reason)
270
+ if (deps.paymentRecovery) {
271
+ const recovered = await deps.recoverDurablePayment(renewed.operationId, { force: true })
272
+ if (recovered && recovered.state !== 'reconciled') {
273
+ throw new Error('durable payment release is still pending')
274
+ }
275
+ }
276
+ const recovered = clearPaymentReleaseRecord(leasedTask)
277
+ if (!await compareAndSetTask(deps.taskStore, leasedTask, recovered)) {
278
+ return await deps.taskStore.get(task.id) ?? recovered
279
+ }
280
+ return recovered
281
+ } catch (error) {
282
+ const recoveryError = asError(error)
283
+ const retained = await retainPaymentReleaseForRecovery(
284
+ deps.taskStore,
285
+ task.id,
286
+ renewed.lease.id,
287
+ recoveryError,
288
+ )
289
+ console.error(
290
+ '[a2a] payment release recovery failed for ' + task.id + ':',
291
+ recoveryError.message,
292
+ )
293
+ return retained ?? leasedTask
294
+ }
295
+ }
296
+
297
+ export async function recoverPaymentMarkerIfNeeded(
298
+ task: Task,
299
+ deps: PaymentRecoveryDependencies,
300
+ ): Promise<Task> {
301
+ const marker = readPaymentRecoveryMarker(task)
302
+ if (!marker || !deps.paymentRecovery) return task
303
+ try {
304
+ const record = await deps.recoverDurablePayment(marker.id)
305
+ if (record?.state !== 'reconciled') return task
306
+ return clearReconciledPaymentRecoveryMarker(task, deps)
307
+ } catch (error) {
308
+ console.error(
309
+ `[a2a] durable payment recovery failed for ${task.id}:`,
310
+ error instanceof Error ? error.message : String(error),
311
+ )
312
+ return task
313
+ }
314
+ }
315
+
316
+ async function clearReconciledPaymentRecoveryMarker(
317
+ task: Task,
318
+ deps: PaymentRecoveryDependencies,
319
+ ): Promise<Task> {
320
+ const marker = readPaymentRecoveryMarker(task)
321
+ if (!marker || !deps.paymentRecovery) return task
322
+ const record = await deps.paymentRecovery.store.get(marker.id)
323
+ if (record?.state !== 'reconciled') return task
324
+ const cleared = clearPaymentRecoveryMarker(task)
325
+ if (cleared.status.state === 'working' || cleared.status.state === 'submitted') {
326
+ const failed: Task = {
327
+ ...withStatus(cleared, 'failed'),
328
+ metadata: {
329
+ ...(cleared.metadata ?? {}),
330
+ gatewayExecutionRecovery: {
331
+ error: 'payment recovery completed without a task result',
332
+ },
333
+ },
334
+ }
335
+ if (await compareAndSetTask(deps.taskStore, task, failed)) return failed
336
+ return await deps.taskStore.get(task.id) ?? failed
337
+ }
338
+ if (await compareAndSetTask(deps.taskStore, task, cleared)) return cleared
339
+ return await deps.taskStore.get(task.id) ?? cleared
340
+ }
341
+
342
+ function withPaymentRecoveryMarker(task: Task, recoveryId: string): Task {
343
+ return {
344
+ ...task,
345
+ metadata: {
346
+ ...(task.metadata ?? {}),
347
+ [PAYMENT_RECOVERY_METADATA_KEY]: { version: 1, id: recoveryId },
348
+ },
349
+ }
350
+ }
351
+
352
+ function withPaymentReleaseRecord(task: Task, record: PaymentReleaseRecord): Task {
353
+ return {
354
+ ...task,
355
+ metadata: { ...(task.metadata ?? {}), [PAYMENT_RELEASE_METADATA_KEY]: record },
356
+ }
357
+ }
358
+
359
+ function clearPaymentReleaseRecord(task: Task): Task {
360
+ return clearTaskMetadata(task, PAYMENT_RELEASE_METADATA_KEY)
361
+ }
362
+
363
+ function readPaymentReleaseRecord(task: Task): PaymentReleaseRecord | undefined {
364
+ const raw = task.metadata?.[PAYMENT_RELEASE_METADATA_KEY]
365
+ if (!raw || typeof raw !== 'object') return undefined
366
+ const record = raw as Partial<PaymentReleaseRecord>
367
+ if (
368
+ record.version !== 1 ||
369
+ !record.lease ||
370
+ typeof record.lease.id !== 'string' ||
371
+ record.lease.id.length === 0 ||
372
+ typeof record.lease.expiresAt !== 'number' ||
373
+ !Number.isFinite(record.lease.expiresAt) ||
374
+ typeof record.agentSlug !== 'string' ||
375
+ record.agentSlug.length === 0 ||
376
+ typeof record.requestId !== 'string' ||
377
+ record.requestId.length === 0 ||
378
+ typeof record.operationId !== 'string' ||
379
+ record.operationId.length === 0 ||
380
+ !record.paymentOperation ||
381
+ typeof record.paymentOperation !== 'object' ||
382
+ typeof record.reason !== 'string'
383
+ ) {
384
+ return undefined
385
+ }
386
+ if (record.paymentOperation.operationId !== record.operationId) return undefined
387
+ try {
388
+ deserializePaymentOperation(record.paymentOperation)
389
+ } catch {
390
+ return undefined
391
+ }
392
+ return record as PaymentReleaseRecord
393
+ }
394
+
395
+ function buildPaymentReleaseRecord(
396
+ authz: AuthorizedRequest,
397
+ reason: string,
398
+ ): PaymentReleaseRecord | undefined {
399
+ const operation = authz.paymentOperation
400
+ if (!operation) return undefined
401
+ return {
402
+ version: 1,
403
+ lease: { id: cryptoRandomId(), expiresAt: Date.now() + PAYMENT_RELEASE_LEASE_MS },
404
+ agentSlug: authz.agent.slug,
405
+ requestId: authz.requestId,
406
+ operationId: operation.operationId,
407
+ paymentOperation: serializePaymentOperation({ ...operation, state: 'releasing' }),
408
+ reason,
409
+ }
410
+ }
411
+
412
+ async function expirePaymentRelease(
413
+ task: Task,
414
+ deps: PaymentRecoveryDependencies,
415
+ error: Error,
416
+ ): Promise<Task> {
417
+ const cleanTask = clearPaymentReleaseRecord(task)
418
+ const terminalTask = withStatus(cleanTask, 'failed')
419
+ const failed: Task = {
420
+ ...terminalTask,
421
+ metadata: {
422
+ ...(terminalTask.metadata ?? {}),
423
+ gatewayPaymentReleaseRecovery: { error: error.message },
424
+ },
425
+ }
426
+ if (await compareAndSetTask(deps.taskStore, task, failed)) {
427
+ await deps.deliverPush(failed)
428
+ return failed
429
+ }
430
+ return await deps.taskStore.get(task.id) ?? failed
431
+ }
@@ -0,0 +1,158 @@
1
+ import type { Context } from 'hono'
2
+ import {
3
+ validatePushNotificationUrl,
4
+ type PushNotificationStore,
5
+ type TaskPushNotificationConfig,
6
+ } from './push-notifications'
7
+ import type { TaskStateStore } from './task-state'
8
+ import {
9
+ A2A_ERROR_CODES,
10
+ type JSONRPCRequest,
11
+ type Task,
12
+ type TaskIdParams,
13
+ type TaskPushNotificationConfigGetParams,
14
+ } from './types'
15
+ import { fail, ok } from './jsonrpc'
16
+
17
+ export interface PushConfigMethodDependencies {
18
+ taskStore: TaskStateStore
19
+ pushStore?: PushNotificationStore
20
+ demoMode: boolean
21
+ urlValidator?: (url: URL) => boolean | Promise<boolean>
22
+ authorizeTaskAccess: (
23
+ c: Context,
24
+ req: JSONRPCRequest,
25
+ task: Task,
26
+ ) => Promise<Response | undefined>
27
+ }
28
+
29
+ export async function handlePushSet(
30
+ c: Context,
31
+ req: JSONRPCRequest,
32
+ deps: PushConfigMethodDependencies,
33
+ ): Promise<Response> {
34
+ if (!deps.pushStore) {
35
+ return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, 'push notifications not configured'))
36
+ }
37
+ const params = req.params as TaskPushNotificationConfig | undefined
38
+ if (!params || typeof params.taskId !== 'string' || !params.pushNotificationConfig?.id) {
39
+ return c.json(
40
+ fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, 'params.taskId and params.pushNotificationConfig.id required'),
41
+ )
42
+ }
43
+ if (typeof params.pushNotificationConfig.url !== 'string') {
44
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, 'pushNotificationConfig.url required'))
45
+ }
46
+ const task = await deps.taskStore.get(params.taskId)
47
+ if (!task) {
48
+ return c.json(fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.taskId}' not found`))
49
+ }
50
+ const accessError = await deps.authorizeTaskAccess(c, req, task)
51
+ if (accessError) return accessError
52
+ const pushUrl = validatePushNotificationUrl(params.pushNotificationConfig.url)
53
+ if (!pushUrl) {
54
+ return c.json(
55
+ fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, 'pushNotificationConfig.url is not a safe HTTPS destination'),
56
+ )
57
+ }
58
+ if (!deps.demoMode && !deps.urlValidator) {
59
+ return c.json(
60
+ fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, 'production push URL validation is not configured'),
61
+ )
62
+ }
63
+ let allowedByHostPolicy = true
64
+ try {
65
+ if (deps.urlValidator) allowedByHostPolicy = await deps.urlValidator(pushUrl)
66
+ } catch (error) {
67
+ allowedByHostPolicy = false
68
+ console.error(
69
+ `[a2a] push URL policy failed for task ${task.id}:`,
70
+ error instanceof Error ? error.message : String(error),
71
+ )
72
+ }
73
+ if (!allowedByHostPolicy) {
74
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, 'pushNotificationConfig.url was rejected'))
75
+ }
76
+ await deps.pushStore.set(params.taskId, params.pushNotificationConfig)
77
+ const stored = await deps.pushStore.get(params.taskId, params.pushNotificationConfig.id)
78
+ return c.json(ok(req.id, { taskId: params.taskId, pushNotificationConfig: stored }))
79
+ }
80
+
81
+ export async function handlePushGet(
82
+ c: Context,
83
+ req: JSONRPCRequest,
84
+ deps: PushConfigMethodDependencies,
85
+ ): Promise<Response> {
86
+ if (!deps.pushStore) {
87
+ return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, 'push notifications not configured'))
88
+ }
89
+ const params = req.params as TaskPushNotificationConfigGetParams | undefined
90
+ if (!params || typeof params.id !== 'string' || typeof params.pushNotificationConfigId !== 'string') {
91
+ return c.json(
92
+ fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, 'params.id and params.pushNotificationConfigId required'),
93
+ )
94
+ }
95
+ const task = await deps.taskStore.get(params.id)
96
+ if (!task) {
97
+ return c.json(fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.id}' not found`))
98
+ }
99
+ const accessError = await deps.authorizeTaskAccess(c, req, task)
100
+ if (accessError) return accessError
101
+ const cfg = await deps.pushStore.get(params.id, params.pushNotificationConfigId)
102
+ if (!cfg) {
103
+ return c.json(
104
+ fail(
105
+ req.id,
106
+ A2A_ERROR_CODES.TASK_NOT_FOUND,
107
+ `push config '${params.pushNotificationConfigId}' not found for task '${params.id}'`,
108
+ ),
109
+ )
110
+ }
111
+ return c.json(ok(req.id, { taskId: params.id, pushNotificationConfig: cfg }))
112
+ }
113
+
114
+ export async function handlePushList(
115
+ c: Context,
116
+ req: JSONRPCRequest,
117
+ deps: PushConfigMethodDependencies,
118
+ ): Promise<Response> {
119
+ if (!deps.pushStore) {
120
+ return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, 'push notifications not configured'))
121
+ }
122
+ const params = req.params as TaskIdParams | undefined
123
+ if (!params || typeof params.id !== 'string') {
124
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, 'params.id required'))
125
+ }
126
+ const task = await deps.taskStore.get(params.id)
127
+ if (!task) {
128
+ return c.json(fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.id}' not found`))
129
+ }
130
+ const accessError = await deps.authorizeTaskAccess(c, req, task)
131
+ if (accessError) return accessError
132
+ const configs = await deps.pushStore.list(params.id)
133
+ return c.json(ok(req.id, configs.map((cfg) => ({ taskId: params.id, pushNotificationConfig: cfg }))))
134
+ }
135
+
136
+ export async function handlePushDelete(
137
+ c: Context,
138
+ req: JSONRPCRequest,
139
+ deps: PushConfigMethodDependencies,
140
+ ): Promise<Response> {
141
+ if (!deps.pushStore) {
142
+ return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, 'push notifications not configured'))
143
+ }
144
+ const params = req.params as TaskPushNotificationConfigGetParams | undefined
145
+ if (!params || typeof params.id !== 'string' || typeof params.pushNotificationConfigId !== 'string') {
146
+ return c.json(
147
+ fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, 'params.id and params.pushNotificationConfigId required'),
148
+ )
149
+ }
150
+ const task = await deps.taskStore.get(params.id)
151
+ if (!task) {
152
+ return c.json(fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.id}' not found`))
153
+ }
154
+ const accessError = await deps.authorizeTaskAccess(c, req, task)
155
+ if (accessError) return accessError
156
+ await deps.pushStore.delete(params.id, params.pushNotificationConfigId)
157
+ return c.json(ok(req.id, null))
158
+ }