@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
@@ -0,0 +1,331 @@
1
+ import type { MppChargeOperation } from './mpp-payment'
2
+ import type { PaymentOperation } from './payment-operations'
3
+ import type {
4
+ PaymentMethod,
5
+ PaymentSettlementBasis,
6
+ SandboxExecutionBudget,
7
+ SandboxUsageReceipt,
8
+ } from './payment-types'
9
+
10
+ export type { PaymentSettlementBasis } from './payment-types'
11
+
12
+ export const PAYMENT_RECOVERY_VERSION = 1 as const
13
+
14
+ export type PaymentRecoveryState =
15
+ | 'claiming'
16
+ | 'claimed'
17
+ | 'executing'
18
+ | 'retained'
19
+ | 'settling'
20
+ | 'releasing'
21
+ | 'reconciled'
22
+
23
+ export interface SerializedPaymentOperation {
24
+ protocolVersion: 2
25
+ operationId: string
26
+ acquiredByRequestId: string
27
+ executionStartedAt?: number
28
+ retentionReason?: string
29
+ nonceKey: string
30
+ authorizationId: string
31
+ reservedAmount: string
32
+ settledAmount: string
33
+ refundAmount: string
34
+ expiresAt: number
35
+ state: PaymentOperation['state']
36
+ }
37
+
38
+ export interface PaymentRecoveryAttribution {
39
+ requestId: string
40
+ agentId: string
41
+ agentSlug: string
42
+ consumerId: string
43
+ paymentMethod: PaymentMethod
44
+ startMs: number
45
+ pricePerTokenUsd: number
46
+ platformFeePercent: number
47
+ requiredAmount: string
48
+ currencyDecimals: number
49
+ maxOutputTokens: number
50
+ executionBudget: SandboxExecutionBudget
51
+ }
52
+
53
+ export type PaymentRecoveryTarget =
54
+ | {
55
+ kind: 'x402'
56
+ operationId: string
57
+ operation?: SerializedPaymentOperation
58
+ }
59
+ | {
60
+ kind: 'mpp-charge'
61
+ method: string
62
+ operationId: string
63
+ operation?: MppChargeOperation
64
+ }
65
+
66
+ /** Durable outbox row for one payment identity. Rows are never deleted here. */
67
+ export interface PaymentRecoveryRecord {
68
+ version: typeof PAYMENT_RECOVERY_VERSION
69
+ id: string
70
+ revision: number
71
+ state: PaymentRecoveryState
72
+ payment: PaymentRecoveryTarget
73
+ attribution: PaymentRecoveryAttribution
74
+ workStarted: boolean
75
+ /** Earliest time a missing receipt may settle at the quoted ceiling. */
76
+ fallbackAt?: number
77
+ usage?: SandboxUsageReceipt
78
+ usageRecorded: boolean
79
+ settlementBasis?: PaymentSettlementBasis
80
+ reason?: string
81
+ attempts: number
82
+ lastError?: string
83
+ nextAttemptAt: number
84
+ lease?: { id: string; expiresAt: number }
85
+ createdAt: number
86
+ updatedAt: number
87
+ reconciledAt?: number
88
+ }
89
+
90
+ export interface PaymentRecoveryStore {
91
+ createIfAbsent(record: PaymentRecoveryRecord): Promise<boolean>
92
+ get(id: string): Promise<PaymentRecoveryRecord | undefined>
93
+ compareAndSet(
94
+ expected: PaymentRecoveryRecord,
95
+ next: PaymentRecoveryRecord,
96
+ ): Promise<boolean>
97
+ listDue(now: number, limit: number): Promise<PaymentRecoveryRecord[]>
98
+ }
99
+
100
+ export interface PaymentRecoveryConfig {
101
+ store: PaymentRecoveryStore
102
+ /** Claimed payment with no execution becomes recoverable after this delay. */
103
+ staleRequestMs?: number
104
+ /** Work without a final receipt settles at the quoted ceiling after this delay. */
105
+ receiptTimeoutMs?: number
106
+ /** Failed provider recovery waits this long before its next attempt. */
107
+ retryDelayMs?: number
108
+ /** One recovery worker owns a row for this duration. */
109
+ leaseMs?: number
110
+ }
111
+
112
+ export const DEFAULT_STALE_REQUEST_MS = 30_000
113
+ export const DEFAULT_RECEIPT_TIMEOUT_MS = 5 * 60_000
114
+ export const DEFAULT_RECOVERY_RETRY_MS = 30_000
115
+ export const DEFAULT_RECOVERY_LEASE_MS = 30_000
116
+
117
+ /** Atomic single-process store for tests and explicit local demo mode. */
118
+ export class MemoryPaymentRecoveryStore implements PaymentRecoveryStore {
119
+ private readonly records = new Map<string, PaymentRecoveryRecord>()
120
+
121
+ async createIfAbsent(record: PaymentRecoveryRecord): Promise<boolean> {
122
+ if (this.records.has(record.id)) return false
123
+ this.records.set(record.id, clone(record))
124
+ return true
125
+ }
126
+
127
+ async get(id: string): Promise<PaymentRecoveryRecord | undefined> {
128
+ const record = this.records.get(id)
129
+ return record ? clone(record) : undefined
130
+ }
131
+
132
+ async compareAndSet(
133
+ expected: PaymentRecoveryRecord,
134
+ next: PaymentRecoveryRecord,
135
+ ): Promise<boolean> {
136
+ const current = this.records.get(expected.id)
137
+ if (!current || current.revision !== expected.revision) return false
138
+ this.records.set(expected.id, clone(next))
139
+ return true
140
+ }
141
+
142
+ async listDue(now: number, limit: number): Promise<PaymentRecoveryRecord[]> {
143
+ return [...this.records.values()]
144
+ .filter((record) => record.state !== 'reconciled' && record.nextAttemptAt <= now)
145
+ .sort((left, right) => left.nextAttemptAt - right.nextAttemptAt)
146
+ .slice(0, limit)
147
+ .map(clone)
148
+ }
149
+ }
150
+
151
+ export class PaymentRecoveryFenceError extends Error {
152
+ constructor(id: string) {
153
+ super(`payment recovery fence was lost for ${id}`)
154
+ this.name = 'PaymentRecoveryFenceError'
155
+ }
156
+ }
157
+
158
+ export class PaymentRecoveryReplayError extends Error {
159
+ constructor(id: string) {
160
+ super(`payment recovery identity was already reconciled: ${id}`)
161
+ this.name = 'PaymentRecoveryReplayError'
162
+ }
163
+ }
164
+
165
+ /** Update only while the caller still owns the durable row fence. */
166
+ export async function updateOwnedPaymentRecovery(
167
+ store: PaymentRecoveryStore,
168
+ id: string,
169
+ fenceId: string,
170
+ update: (record: PaymentRecoveryRecord) => PaymentRecoveryRecord,
171
+ now = Date.now(),
172
+ ): Promise<PaymentRecoveryRecord> {
173
+ for (let attempt = 0; attempt < 16; attempt += 1) {
174
+ const current = await store.get(id)
175
+ if (!current) throw new Error(`payment recovery record ${id} was not found`)
176
+ if (current.state === 'reconciled' || current.lease?.id !== fenceId) {
177
+ throw new PaymentRecoveryFenceError(id)
178
+ }
179
+ const candidate = update(clone(current))
180
+ assertRecoveryUpdate(current, candidate)
181
+ const next: PaymentRecoveryRecord = {
182
+ ...candidate,
183
+ id: current.id,
184
+ version: PAYMENT_RECOVERY_VERSION,
185
+ revision: current.revision + 1,
186
+ createdAt: current.createdAt,
187
+ updatedAt: now,
188
+ }
189
+ if (await store.compareAndSet(current, next)) return next
190
+ }
191
+ throw new Error(`payment recovery record ${id} changed too many times`)
192
+ }
193
+
194
+ const PAYMENT_RECOVERY_TRANSITIONS: Record<PaymentRecoveryState, ReadonlySet<PaymentRecoveryState>> = {
195
+ claiming: new Set(['claiming', 'claimed', 'releasing', 'reconciled']),
196
+ claimed: new Set(['claimed', 'executing', 'releasing', 'reconciled']),
197
+ executing: new Set(['executing', 'retained', 'settling', 'releasing', 'reconciled']),
198
+ retained: new Set(['retained', 'settling', 'reconciled']),
199
+ settling: new Set(['settling', 'reconciled']),
200
+ releasing: new Set(['releasing', 'reconciled']),
201
+ reconciled: new Set(['reconciled']),
202
+ }
203
+
204
+ function assertRecoveryUpdate(
205
+ current: PaymentRecoveryRecord,
206
+ candidate: PaymentRecoveryRecord,
207
+ ): void {
208
+ if (!PAYMENT_RECOVERY_TRANSITIONS[current.state].has(candidate.state)) {
209
+ throw new Error(`invalid payment recovery transition ${current.state} -> ${candidate.state}`)
210
+ }
211
+ if (JSON.stringify(current.attribution) !== JSON.stringify(candidate.attribution)) {
212
+ throw new Error('payment recovery attribution is immutable')
213
+ }
214
+ if (
215
+ current.payment.kind !== candidate.payment.kind ||
216
+ current.payment.operationId !== candidate.payment.operationId ||
217
+ (current.payment.kind === 'mpp-charge' &&
218
+ candidate.payment.kind === 'mpp-charge' &&
219
+ current.payment.method !== candidate.payment.method)
220
+ ) {
221
+ throw new Error('payment recovery identity is immutable')
222
+ }
223
+ if (current.workStarted && !candidate.workStarted) {
224
+ throw new Error('payment recovery cannot forget started work')
225
+ }
226
+ if (current.usageRecorded && !candidate.usageRecorded) {
227
+ throw new Error('payment recovery cannot forget usage attribution')
228
+ }
229
+ if (current.usage && (!candidate.usage || !sameRecoveryUsage(current.usage, candidate.usage))) {
230
+ throw new Error('payment recovery usage receipt is immutable')
231
+ }
232
+ if (
233
+ current.settlementBasis &&
234
+ current.settlementBasis !== candidate.settlementBasis
235
+ ) {
236
+ throw new Error('payment recovery settlement basis is immutable')
237
+ }
238
+ }
239
+
240
+ function sameRecoveryUsage(
241
+ left: SandboxUsageReceipt,
242
+ right: SandboxUsageReceipt,
243
+ ): boolean {
244
+ return left.inputTokens === right.inputTokens &&
245
+ left.outputTokens === right.outputTokens &&
246
+ left.reasoningTokens === right.reasoningTokens &&
247
+ left.toolTokens === right.toolTokens &&
248
+ left.toolCallCount === right.toolCallCount &&
249
+ left.providerCostUsd === right.providerCostUsd &&
250
+ left.budgetEnforced === right.budgetEnforced
251
+ }
252
+
253
+ export function serializePaymentOperation(
254
+ operation: PaymentOperation,
255
+ ): SerializedPaymentOperation {
256
+ return {
257
+ protocolVersion: 2,
258
+ operationId: operation.operationId,
259
+ acquiredByRequestId: operation.acquiredByRequestId,
260
+ ...(operation.executionStartedAt !== undefined
261
+ ? { executionStartedAt: operation.executionStartedAt }
262
+ : {}),
263
+ ...(operation.retentionReason ? { retentionReason: operation.retentionReason } : {}),
264
+ nonceKey: operation.nonceKey,
265
+ authorizationId: operation.authorizationId,
266
+ reservedAmount: operation.reservedAmount.toString(),
267
+ settledAmount: operation.settledAmount.toString(),
268
+ refundAmount: operation.refundAmount.toString(),
269
+ expiresAt: operation.expiresAt,
270
+ state: operation.state,
271
+ }
272
+ }
273
+
274
+ export function deserializePaymentOperation(
275
+ value: SerializedPaymentOperation,
276
+ ): PaymentOperation {
277
+ if (value.protocolVersion !== 2) throw new Error('unsupported payment operation version')
278
+ if (!value.operationId || !value.acquiredByRequestId || !value.nonceKey || !value.authorizationId) {
279
+ throw new Error('incomplete payment operation recovery record')
280
+ }
281
+ return {
282
+ protocolVersion: 2,
283
+ operationId: value.operationId,
284
+ acquiredByRequestId: value.acquiredByRequestId,
285
+ ...(value.executionStartedAt !== undefined ? { executionStartedAt: value.executionStartedAt } : {}),
286
+ ...(value.retentionReason ? { retentionReason: value.retentionReason } : {}),
287
+ nonceKey: value.nonceKey,
288
+ authorizationId: value.authorizationId,
289
+ reservedAmount: BigInt(value.reservedAmount),
290
+ settledAmount: BigInt(value.settledAmount),
291
+ refundAmount: BigInt(value.refundAmount),
292
+ expiresAt: value.expiresAt,
293
+ state: value.state,
294
+ }
295
+ }
296
+
297
+ export function recoveryTiming(config: PaymentRecoveryConfig): {
298
+ staleRequestMs: number
299
+ receiptTimeoutMs: number
300
+ retryDelayMs: number
301
+ leaseMs: number
302
+ } {
303
+ return {
304
+ staleRequestMs: config.staleRequestMs ?? DEFAULT_STALE_REQUEST_MS,
305
+ receiptTimeoutMs: config.receiptTimeoutMs ?? DEFAULT_RECEIPT_TIMEOUT_MS,
306
+ retryDelayMs: config.retryDelayMs ?? DEFAULT_RECOVERY_RETRY_MS,
307
+ leaseMs: config.leaseMs ?? DEFAULT_RECOVERY_LEASE_MS,
308
+ }
309
+ }
310
+
311
+ export function assertPaymentRecoveryConfig(config: PaymentRecoveryConfig): void {
312
+ for (const [name, value] of Object.entries(recoveryTiming(config))) {
313
+ if (!Number.isSafeInteger(value) || value <= 0) {
314
+ throw new Error(`payment recovery ${name} must be a positive safe integer`)
315
+ }
316
+ }
317
+ const store = config.store
318
+ if (
319
+ !store ||
320
+ typeof store.createIfAbsent !== 'function' ||
321
+ typeof store.get !== 'function' ||
322
+ typeof store.compareAndSet !== 'function' ||
323
+ typeof store.listDue !== 'function'
324
+ ) {
325
+ throw new Error('payment recovery store must provide atomic create, compare-and-set, and due scans')
326
+ }
327
+ }
328
+
329
+ function clone<T>(value: T): T {
330
+ return JSON.parse(JSON.stringify(value)) as T
331
+ }
@@ -0,0 +1,48 @@
1
+ export type PaymentMethod = 'x402' | 'mpp' | 'apikey' | 'none'
2
+
3
+ export interface SandboxExecutionBudget {
4
+ maxInputTokens: number
5
+ maxOutputTokens: number
6
+ maxReasoningTokens: number
7
+ maxToolTokens: number
8
+ maxToolCalls: number
9
+ maxProviderCostUsd: number
10
+ }
11
+
12
+ export interface SandboxUsageReceipt {
13
+ inputTokens: number
14
+ outputTokens: number
15
+ reasoningTokens: number
16
+ toolTokens: number
17
+ toolCallCount: number
18
+ providerCostUsd: number
19
+ /** True only when the provider/adapter enforced every supplied budget. */
20
+ budgetEnforced: boolean
21
+ }
22
+
23
+ export type PaymentSettlementBasis = 'usage-receipt' | 'quoted-ceiling'
24
+
25
+ export interface GatewayUsageEvent {
26
+ /** Correlates usage, settlement, and observer records for one request. */
27
+ requestId: string
28
+ agentId: string
29
+ agentSlug: string
30
+ consumerId: string
31
+ paymentMethod: PaymentMethod
32
+ inputTokens: number
33
+ outputTokens: number
34
+ /** Optional in 0.7.2 so 0.7.1 event constructors remain source-compatible. */
35
+ reasoningTokens?: number
36
+ /** Optional in 0.7.2 so 0.7.1 event constructors remain source-compatible. */
37
+ toolTokens?: number
38
+ /** Optional in 0.7.2 so 0.7.1 event constructors remain source-compatible. */
39
+ toolCallCount?: number
40
+ /** Optional in 0.7.2 so 0.7.1 event constructors remain source-compatible. */
41
+ providerCostUsd?: number
42
+ totalCostUsd: number
43
+ ownerEarnedUsd: number
44
+ platformFeeUsd: number
45
+ durationMs: number
46
+ /** Exact receipt in normal operation; quoted ceiling only after receipt timeout. */
47
+ settlementBasis?: PaymentSettlementBasis
48
+ }