@tangle-network/agent-gateway 0.7.0 → 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 (65) hide show
  1. package/README.md +108 -6
  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 +76 -12
  9. package/dist/index.js +307 -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-CX2V06cN.d.ts → types-BHISsm7D.d.ts} +423 -166
  20. package/dist/types.d.ts +2 -1
  21. package/package.json +1 -1
  22. package/src/a2a/agent-card.ts +4 -3
  23. package/src/a2a/execution-fence.ts +162 -0
  24. package/src/a2a/handler.ts +507 -562
  25. package/src/a2a/message-send-execution.ts +241 -0
  26. package/src/a2a/message-stream-execution.ts +392 -0
  27. package/src/a2a/payment-recovery.ts +431 -0
  28. package/src/a2a/push-config-methods.ts +158 -0
  29. package/src/a2a/push-notifications.ts +172 -22
  30. package/src/a2a/task-cancellation.ts +50 -0
  31. package/src/a2a/task-finalization.ts +451 -0
  32. package/src/a2a/task-lifecycle.ts +54 -0
  33. package/src/a2a/task-methods.ts +163 -0
  34. package/src/a2a/task-push-delivery.ts +119 -0
  35. package/src/a2a/task-recovery.ts +11 -0
  36. package/src/a2a/task-state.ts +99 -0
  37. package/src/a2a/task-store-sql.ts +222 -24
  38. package/src/a2a/task-store.ts +58 -1
  39. package/src/a2a/task-submission-recovery.ts +178 -0
  40. package/src/a2a/types.ts +1 -0
  41. package/src/dispatch-authorization.ts +437 -0
  42. package/src/dispatch-payment-recovery.ts +248 -0
  43. package/src/dispatch-payment.ts +425 -0
  44. package/src/dispatch-pricing.ts +108 -0
  45. package/src/dispatch-sandbox.ts +422 -0
  46. package/src/dispatch-settlement.ts +139 -0
  47. package/src/dispatch-types.ts +81 -0
  48. package/src/dispatch.ts +35 -462
  49. package/src/index.ts +64 -2
  50. package/src/middleware.ts +313 -32
  51. package/src/mpp-payment.ts +117 -0
  52. package/src/nonce-store.ts +122 -20
  53. package/src/observer-types.ts +63 -0
  54. package/src/observer.ts +3 -63
  55. package/src/payment-operations.ts +485 -0
  56. package/src/payment-recovery-sql.ts +108 -0
  57. package/src/payment-recovery-worker.ts +488 -0
  58. package/src/payment-recovery.ts +331 -0
  59. package/src/payment-types.ts +48 -0
  60. package/src/types.ts +153 -42
  61. package/src/verify.ts +265 -36
  62. package/dist/chunk-3IKQWFKX.js +0 -1703
  63. package/dist/chunk-3IKQWFKX.js.map +0 -1
  64. package/dist/chunk-M7ZJAK4K.js +0 -53
  65. package/dist/chunk-M7ZJAK4K.js.map +0 -1
package/src/middleware.ts CHANGED
@@ -6,14 +6,25 @@ import {
6
6
  type AuthorizedRequest,
7
7
  type GatewayState,
8
8
  authenticateAndGuard,
9
- dispatchSandboxStream,
10
- estimateTokens,
9
+ beginPaymentExecution,
10
+ markPaymentExecutionStarted,
11
+ renewPaymentExecution,
12
+ claimPayment,
13
+ dispatchSandboxStreamRich,
14
+ releasePayment,
15
+ releasePaymentAfterFailure,
11
16
  settleAndRecord,
12
17
  } from './dispatch'
13
- import { MemoryNonceStore } from './nonce-store'
18
+ import { isAtomicNonceStore, MemoryNonceStore } from './nonce-store'
14
19
  import { type GatewayObserver, type RequestContext, generateRequestId } from './observer'
20
+ import {
21
+ MemoryPaymentRecoveryStore,
22
+ PaymentRecoveryReplayError,
23
+ assertPaymentRecoveryConfig,
24
+ } from './payment-recovery'
15
25
  import { MemoryRateLimitStore, type RateLimitStore } from './rate-limit'
16
26
  import type { ChatCompletionChunk, ChatCompletionRequest, GatewayConfig } from './types'
27
+ import { isApiKeyAuthEnabled, isMppAuthEnabled } from './verify'
17
28
 
18
29
  /**
19
30
  * Create a Hono router that serves the agent gateway.
@@ -25,7 +36,8 @@ import type { ChatCompletionChunk, ChatCompletionRequest, GatewayConfig } from '
25
36
  * GET /:slug/chat/completions — agent discovery metadata (Tangle-native shape)
26
37
  * POST /:slug/chat/completions — OpenAI-compatible chat endpoint (paid)
27
38
  */
28
- export function createAgentGateway(config: GatewayConfig) {
39
+ export function createAgentGateway(inputConfig: GatewayConfig) {
40
+ let config = inputConfig
29
41
  // Production gateways must verify x402 signatures. Tests and local
30
42
  // dev can opt into the explicit demo path.
31
43
  if (!config.x402.verifySigner && !config.x402.demoMode) {
@@ -34,6 +46,120 @@ export function createAgentGateway(config: GatewayConfig) {
34
46
  'For tests, set x402.demoMode: true explicitly.',
35
47
  )
36
48
  }
49
+ const maxOutputTokens = config.maxOutputTokens ?? 4096
50
+ const defaultOutputTokens = config.defaultOutputTokens ?? 1024
51
+ if (!Number.isSafeInteger(maxOutputTokens) || maxOutputTokens <= 0) {
52
+ throw new Error('createAgentGateway: maxOutputTokens must be a positive integer')
53
+ }
54
+ if (
55
+ !Number.isSafeInteger(defaultOutputTokens) ||
56
+ defaultOutputTokens <= 0 ||
57
+ defaultOutputTokens > maxOutputTokens
58
+ ) {
59
+ throw new Error(
60
+ 'createAgentGateway: defaultOutputTokens must be a positive integer no greater than maxOutputTokens',
61
+ )
62
+ }
63
+ if (
64
+ config.x402.currencyDecimals !== undefined &&
65
+ (!Number.isInteger(config.x402.currencyDecimals) ||
66
+ config.x402.currencyDecimals < 0 ||
67
+ config.x402.currencyDecimals > 18)
68
+ ) {
69
+ throw new Error('createAgentGateway: x402.currencyDecimals must be an integer between 0 and 18')
70
+ }
71
+ const executionBudget = config.executionBudget
72
+ for (const [name, value] of [
73
+ ['maxReasoningTokens', executionBudget?.maxReasoningTokens ?? maxOutputTokens],
74
+ ['maxToolTokens', executionBudget?.maxToolTokens ?? maxOutputTokens],
75
+ ['maxToolCalls', executionBudget?.maxToolCalls ?? 8],
76
+ ] as const) {
77
+ if (!Number.isSafeInteger(value) || value < 0) {
78
+ throw new Error(`createAgentGateway: executionBudget.${name} must be a non-negative safe integer`)
79
+ }
80
+ }
81
+ if (
82
+ executionBudget?.maxProviderCostUsd !== undefined &&
83
+ (!Number.isFinite(executionBudget.maxProviderCostUsd) || executionBudget.maxProviderCostUsd < 0)
84
+ ) {
85
+ throw new Error('createAgentGateway: executionBudget.maxProviderCostUsd must be finite and non-negative')
86
+ }
87
+ if (config.x402.paymentOperations && config.x402.paymentProtocolVersion === undefined) {
88
+ throw new Error('createAgentGateway: paymentProtocolVersion must be explicit when durable payment operations are configured')
89
+ }
90
+ if (
91
+ config.x402.authorizePayment &&
92
+ config.x402.paymentProtocolVersion !== 2 &&
93
+ !config.x402.demoMode
94
+ ) {
95
+ throw new Error(
96
+ 'createAgentGateway: production x402 version 1 cannot use authorizePayment; ' +
97
+ 'use paymentProtocolVersion: 2 with paymentOperations for durable payment ownership',
98
+ )
99
+ }
100
+ if (config.x402.paymentProtocolVersion === 2 &&
101
+ (!config.x402.paymentOperations || config.x402.paymentOperations.protocolVersion !== 2)) {
102
+ throw new Error('createAgentGateway: payment protocol version 2 requires durable payment operations')
103
+ }
104
+ if (config.x402.paymentProtocolVersion === 1 && config.x402.paymentOperations) {
105
+ throw new Error('createAgentGateway: version 1 cannot be combined with version 2 payment operations')
106
+ }
107
+ if (
108
+ config.a2a?.pushStore &&
109
+ !config.x402.demoMode &&
110
+ (!config.a2a.webhookSecret || config.a2a.webhookSecret.trim().length === 0)
111
+ ) {
112
+ throw new Error('createAgentGateway: production A2A push requires a webhookSecret')
113
+ }
114
+ const mppMethod = (config.mpp?.method ?? 'blueprintevm').toLowerCase()
115
+ if (config.mpp?.authenticateCredential !== undefined &&
116
+ typeof config.mpp.authenticateCredential !== 'function') {
117
+ throw new Error('createAgentGateway: mpp.authenticateCredential must be a function')
118
+ }
119
+ if (config.mpp?.verifySigner !== undefined && typeof config.mpp.verifySigner !== 'function') {
120
+ throw new Error('createAgentGateway: mpp.verifySigner must be a function')
121
+ }
122
+ const mppAuthenticator = typeof config.mpp?.authenticateCredential === 'function'
123
+ ? config.mpp.authenticateCredential
124
+ : typeof config.mpp?.verifySigner === 'function'
125
+ ? config.mpp.verifySigner
126
+ : undefined
127
+ if (config.mpp?.charge && config.mpp.charge.protocolVersion !== 1) {
128
+ throw new Error('createAgentGateway: unsupported MPP charge lifecycle version')
129
+ }
130
+ if (
131
+ config.mpp?.charge &&
132
+ mppMethod !== 'blueprintevm' &&
133
+ !mppAuthenticator
134
+ ) {
135
+ throw new Error('createAgentGateway: generic MPP methods require credential authentication')
136
+ }
137
+ if (
138
+ config.mpp &&
139
+ mppMethod !== 'blueprintevm' &&
140
+ mppAuthenticator &&
141
+ !config.mpp.charge
142
+ ) {
143
+ throw new Error('createAgentGateway: generic MPP methods require a charge lifecycle')
144
+ }
145
+ if (config.mpp && mppMethod !== 'blueprintevm' && !mppAuthenticator) {
146
+ throw new Error('createAgentGateway: generic MPP methods require credential authentication')
147
+ }
148
+ if (config.nonceStore && !isAtomicNonceStore(config.nonceStore)) {
149
+ throw new Error('createAgentGateway: durable payment ownership requires an atomic nonce store')
150
+ }
151
+ const needsRecovery = config.x402.paymentProtocolVersion === 2 ||
152
+ (mppMethod !== 'blueprintevm' && config.mpp?.charge !== undefined)
153
+ if (needsRecovery && !config.paymentRecovery) {
154
+ if (!config.x402.demoMode) {
155
+ throw new Error('createAgentGateway: durable payment recovery is required in production')
156
+ }
157
+ config = {
158
+ ...config,
159
+ paymentRecovery: { store: new MemoryPaymentRecoveryStore() },
160
+ }
161
+ }
162
+ if (config.paymentRecovery) assertPaymentRecoveryConfig(config.paymentRecovery)
37
163
  const gw = new Hono()
38
164
  const rateLimitStore: RateLimitStore = config.rateLimitStore ?? new MemoryRateLimitStore()
39
165
  const state: GatewayState = {
@@ -42,6 +168,12 @@ export function createAgentGateway(config: GatewayConfig) {
42
168
  globalRateLimit: config.rateLimit ?? { limit: 60, windowSeconds: 60 },
43
169
  requiredScope: config.requiredScope ?? 'chat',
44
170
  maxLen: config.maxMessageLength ?? 8000,
171
+ maxOutputTokens,
172
+ defaultOutputTokens,
173
+ maxReasoningTokens: config.executionBudget?.maxReasoningTokens ?? maxOutputTokens,
174
+ maxToolTokens: config.executionBudget?.maxToolTokens ?? maxOutputTokens,
175
+ maxToolCalls: config.executionBudget?.maxToolCalls ?? 8,
176
+ maxProviderCostUsd: config.executionBudget?.maxProviderCostUsd,
45
177
  obs: config.observer,
46
178
  }
47
179
  const obs: GatewayObserver | undefined = state.obs
@@ -51,7 +183,7 @@ export function createAgentGateway(config: GatewayConfig) {
51
183
  gw.get('/:slug/chat/completions', async (c) => {
52
184
  const slug = c.req.param('slug')
53
185
  const agent = await config.resolveAgent(slug)
54
- if (!agent) return c.json({ error: 'Agent not found or not published' }, 404)
186
+ if (!agent || !agent.enabled) return c.json({ error: 'Agent not found or not published' }, 404)
55
187
 
56
188
  const paymentMethods: Array<Record<string, unknown>> = [
57
189
  {
@@ -61,14 +193,14 @@ export function createAgentGateway(config: GatewayConfig) {
61
193
  credits_contract: config.x402.creditsAddress,
62
194
  },
63
195
  ]
64
- if (config.mpp) {
196
+ if (isMppAuthEnabled(config)) {
65
197
  paymentMethods.push({
66
198
  type: 'mpp',
67
- realm: config.mpp.realm,
68
- method: config.mpp.method ?? 'blueprintevm',
199
+ realm: config.mpp!.realm,
200
+ method: config.mpp!.method ?? 'blueprintevm',
69
201
  })
70
202
  }
71
- paymentMethods.push({ type: 'api_key', prefix: 'sk_agent_' })
203
+ if (isApiKeyAuthEnabled(config)) paymentMethods.push({ type: 'api_key', prefix: 'sk_agent_' })
72
204
 
73
205
  return c.json({
74
206
  slug: agent.slug,
@@ -124,9 +256,62 @@ export function createAgentGateway(config: GatewayConfig) {
124
256
  )
125
257
  }
126
258
 
127
- const guard = await authenticateAndGuard(c, slug, body.messages, config, state)
259
+ const guard = await authenticateAndGuard(
260
+ c,
261
+ slug,
262
+ body.messages,
263
+ config,
264
+ state,
265
+ body.max_tokens,
266
+ )
128
267
  if (guard instanceof Response) return guard
129
268
  const authz = guard
269
+ try {
270
+ await claimPayment(authz, config, state)
271
+ } catch (error) {
272
+ const replayedGenericMpp = error instanceof PaymentRecoveryReplayError &&
273
+ authz.paymentMethod === 'mpp' &&
274
+ authz.mppMethod !== 'blueprintevm'
275
+ try {
276
+ await releasePayment(authz, config, 'payment authorization failed')
277
+ } catch (releaseError) {
278
+ console.error(
279
+ `[agent-gateway] payment release failed for ${authz.requestId}:`,
280
+ releaseError instanceof Error ? releaseError.message : String(releaseError),
281
+ )
282
+ }
283
+ await obs?.onAuthFailure?.(
284
+ {
285
+ requestId: authz.requestId,
286
+ agentSlug: authz.agent.slug,
287
+ startMs: authz.startMs,
288
+ },
289
+ {
290
+ method: authz.paymentMethod,
291
+ code: replayedGenericMpp ? 'invalid_mpp_credential' : 'payment_authorization_failed',
292
+ httpStatus: replayedGenericMpp ? 401 : 402,
293
+ },
294
+ )
295
+ const status = replayedGenericMpp ? 401 : 402
296
+ return c.json(
297
+ {
298
+ error: {
299
+ message: replayedGenericMpp ? 'Invalid Payment credential' : 'Payment authorization failed',
300
+ type: replayedGenericMpp ? 'authentication_error' : 'payment_required',
301
+ code: replayedGenericMpp ? 'invalid_mpp_credential' : 'payment_authorization_failed',
302
+ },
303
+ },
304
+ replayedGenericMpp
305
+ ? {
306
+ status,
307
+ headers: {
308
+ 'WWW-Authenticate': `Payment realm="${config.mpp!.realm}", method="${config.mpp!.method ?? 'blueprintevm'}"`,
309
+ 'X-Request-Id': authz.requestId,
310
+ },
311
+ }
312
+ : { status, headers: { 'X-Payment-Required': 'spendauth', 'X-Request-Id': authz.requestId } },
313
+ )
314
+ }
130
315
 
131
316
  return streamChatCompletions(c, authz, config, obs)
132
317
  })
@@ -138,13 +323,37 @@ export function createAgentGateway(config: GatewayConfig) {
138
323
  // regardless of which protocol the caller used.
139
324
  const taskStore = config.a2a?.taskStore ?? new InMemoryTaskStore()
140
325
  const pushStore = config.a2a?.pushStore
141
- const a2a = createA2AHandlers({ config, state, taskStore, pushStore })
142
- gw.get('/:slug/.well-known/agent.json', a2a.handleAgentCard)
143
- gw.post('/:slug', a2a.handleJsonRpc)
326
+ try {
327
+ const a2a = createA2AHandlers({ config, state, taskStore, pushStore })
328
+ gw.get('/:slug/.well-known/agent.json', a2a.handleAgentCard)
329
+ gw.post('/:slug', a2a.handleJsonRpc)
330
+ } catch (error) {
331
+ // An older custom store must not take down the OpenAI surface. Keep the
332
+ // A2A surface unavailable until its owner supplies atomic methods.
333
+ console.error(
334
+ '[agent-gateway] A2A is unavailable until its task store is upgraded:',
335
+ error instanceof Error ? error.message : String(error),
336
+ )
337
+ const unavailable = (c: import('hono').Context) => c.json(
338
+ { error: 'A2A task persistence is not configured for concurrent workers' },
339
+ 503,
340
+ )
341
+ gw.get('/:slug/.well-known/agent.json', unavailable)
342
+ gw.post('/:slug', unavailable)
343
+ }
144
344
 
145
345
  return gw
146
346
  }
147
347
 
348
+ // Consumers that bind the gateway through a package boundary can fail closed
349
+ // when an old binary ignores the version 2 operation contract.
350
+ Object.assign(createAgentGateway, { paymentProtocolVersion: 2 as const })
351
+
352
+ /** Public package-boundary version marker for durable payment operations. */
353
+ export namespace createAgentGateway {
354
+ export const paymentProtocolVersion = 2 as const
355
+ }
356
+
148
357
  /**
149
358
  * Drain the sandbox stream into an OpenAI-shaped SSE response, settle the
150
359
  * payment, fire observer hooks. Identical pre-refactor behavior, just lifted
@@ -157,9 +366,23 @@ function streamChatCompletions(
157
366
  config: GatewayConfig,
158
367
  obs: GatewayObserver | undefined,
159
368
  ): Response {
160
- const { agent, consumerId, paymentMethod, requestId, userMessage, rateLimitRemaining } = authz
161
- const inputTokens = estimateTokens(userMessage)
162
- let outputTokens = 0
369
+ const {
370
+ agent,
371
+ consumerId,
372
+ paymentMethod,
373
+ requestId,
374
+ userMessage,
375
+ rateLimitRemaining,
376
+ maxOutputTokens,
377
+ } = authz
378
+ let outputText = ''
379
+ let usage: import('./types').SandboxUsageReceipt | undefined
380
+ let workObserved = false
381
+ const requestSignal = c.req.raw.signal
382
+ const abortController = new AbortController()
383
+ const abortFromRequest = () => abortController.abort()
384
+ if (requestSignal.aborted) abortFromRequest()
385
+ else requestSignal.addEventListener('abort', abortFromRequest, { once: true })
163
386
  const ctx: RequestContext = {
164
387
  requestId,
165
388
  agentSlug: agent.slug,
@@ -170,7 +393,8 @@ function streamChatCompletions(
170
393
  async start(controller) {
171
394
  const encoder = new TextEncoder()
172
395
  const sendChunk = (delta: string) => {
173
- outputTokens += estimateTokens(delta)
396
+ if (controller.desiredSize === null) return
397
+ outputText += delta
174
398
  const chunk: ChatCompletionChunk = {
175
399
  id: `chatcmpl-${Date.now()}`,
176
400
  object: 'chat.completion.chunk',
@@ -182,10 +406,41 @@ function streamChatCompletions(
182
406
  }
183
407
 
184
408
  try {
185
- for await (const delta of dispatchSandboxStream(agent, userMessage, consumerId, config)) {
186
- sendChunk(delta)
409
+ for await (const event of dispatchSandboxStreamRich(
410
+ agent,
411
+ userMessage,
412
+ consumerId,
413
+ config,
414
+ abortController.signal,
415
+ undefined,
416
+ maxOutputTokens,
417
+ () => beginPaymentExecution(authz, config),
418
+ authz.paymentOperation !== undefined || authz.mppChargeOperation !== undefined,
419
+ async () => {
420
+ if (authz.paymentRecoveryId) workObserved = true
421
+ await markPaymentExecutionStarted(authz, config)
422
+ },
423
+ authz.executionBudget.maxInputTokens,
424
+ () => renewPaymentExecution(authz, config),
425
+ )) {
426
+ if (event.kind === 'text') {
427
+ sendChunk(event.delta)
428
+ workObserved = true
429
+ }
430
+ if (event.kind === 'activity') workObserved = true
431
+ if (event.kind === 'usage') usage = event.usage
187
432
  }
188
433
 
434
+ if (!usage) throw new Error('sandbox did not provide a usage receipt')
435
+
436
+ await settleAndRecord(
437
+ agent,
438
+ authz,
439
+ usage,
440
+ config,
441
+ obs,
442
+ )
443
+
189
444
  const done: ChatCompletionChunk = {
190
445
  id: `chatcmpl-${Date.now()}`,
191
446
  object: 'chat.completion.chunk',
@@ -193,10 +448,10 @@ function streamChatCompletions(
193
448
  model: agent.slug,
194
449
  choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
195
450
  }
196
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(done)}\n\n`))
197
- controller.enqueue(encoder.encode('data: [DONE]\n\n'))
198
-
199
- await settleAndRecord(agent, authz, inputTokens, outputTokens, config, obs)
451
+ if (controller.desiredSize !== null) {
452
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(done)}\n\n`))
453
+ controller.enqueue(encoder.encode('data: [DONE]\n\n'))
454
+ }
200
455
  } catch (err) {
201
456
  const rawMessage = err instanceof Error ? err.message : String(err)
202
457
  // Never expose stack traces / absolute paths from sandbox internals.
@@ -204,16 +459,37 @@ function streamChatCompletions(
204
459
  rawMessage.includes('/') || rawMessage.includes('\\')
205
460
  ? 'Internal agent error'
206
461
  : rawMessage
207
- await obs?.onStreamError?.(ctx, { consumerId, errorMessage: rawMessage })
208
- controller.enqueue(
209
- encoder.encode(
210
- `data: ${JSON.stringify({ error: { message: safeMessage, type: 'server_error' } })}\n\n`,
211
- ),
212
- )
462
+ try {
463
+ await obs?.onStreamError?.(ctx, { consumerId, errorMessage: rawMessage })
464
+ } catch (observerError) {
465
+ console.error(
466
+ `[agent-gateway] stream observer failed for ${requestId}:`,
467
+ observerError instanceof Error ? observerError.message : String(observerError),
468
+ )
469
+ }
470
+ try {
471
+ await releasePaymentAfterFailure(authz, config, rawMessage, workObserved || usage !== undefined)
472
+ } catch (releaseError) {
473
+ console.error(
474
+ `[agent-gateway] payment release failed for ${authz.requestId}:`,
475
+ releaseError instanceof Error ? releaseError.message : String(releaseError),
476
+ )
477
+ }
478
+ if (!abortController.signal.aborted && controller.desiredSize !== null) {
479
+ controller.enqueue(
480
+ encoder.encode(
481
+ `data: ${JSON.stringify({ error: { message: safeMessage, type: 'server_error' } })}\n\n`,
482
+ ),
483
+ )
484
+ }
213
485
  } finally {
214
- controller.close()
486
+ requestSignal.removeEventListener('abort', abortFromRequest)
487
+ if (controller.desiredSize !== null) controller.close()
215
488
  }
216
489
  },
490
+ cancel() {
491
+ abortController.abort()
492
+ },
217
493
  })
218
494
 
219
495
  return new Response(stream, {
@@ -224,11 +500,16 @@ function streamChatCompletions(
224
500
  'X-Agent-Slug': agent.slug,
225
501
  'X-Agent-Hosting': agent.sandboxEndpoint ? 'sovereign' : 'centralized',
226
502
  'X-Payment-Method': paymentMethod,
227
- 'X-Payment-Settled': paymentMethod === 'x402' ? 'pending' : 'true',
503
+ 'X-Payment-Settled': paymentMethod === 'x402' || authz.paymentOperation ? 'pending' : 'true',
504
+ ...(authz.mppChargeOperation
505
+ ? { 'Payment-Receipt': authz.mppChargeOperation.receipt }
506
+ : {}),
507
+ ...(authz.paymentRecoveryId
508
+ ? { 'X-Payment-Operation-Id': authz.paymentRecoveryId }
509
+ : {}),
228
510
  ...(rateLimitRemaining !== undefined
229
511
  ? { 'X-RateLimit-Remaining': String(rateLimitRemaining) }
230
512
  : {}),
231
513
  },
232
514
  })
233
515
  }
234
-
@@ -0,0 +1,117 @@
1
+ /** Version of the gateway's method-specific MPP charge contract. */
2
+ export const MPP_CHARGE_PROTOCOL_VERSION = 1 as const
3
+
4
+ export type MppChargeOperationState = 'confirmed' | 'releasing' | 'released'
5
+
6
+ /** Pure authentication result for one method credential. */
7
+ export interface MppAuthenticatedCredential {
8
+ consumerId: string
9
+ /**
10
+ * Stable, non-secret processor identity for this payment credential.
11
+ * Return the same value for equivalent encodings of one credential.
12
+ * The gateway hashes this value before it persists or claims it.
13
+ */
14
+ paymentIdentity: string
15
+ }
16
+
17
+ /** Durable result of one immediate MPP charge. */
18
+ export interface MppChargeOperation {
19
+ protocolVersion: typeof MPP_CHARGE_PROTOCOL_VERSION
20
+ operationId: string
21
+ acquiredByRequestId: string
22
+ method: string
23
+ /** A complete Payment-Receipt header value. */
24
+ receipt: string
25
+ state: MppChargeOperationState
26
+ }
27
+
28
+ export interface MppChargeRequest {
29
+ /**
30
+ * Stable provider idempotency key. The adapter must bind every processor
31
+ * operation to this value before it attempts confirmation.
32
+ */
33
+ operationId: string
34
+ requestId: string
35
+ agentId: string
36
+ consumerId: string
37
+ method: string
38
+ /** Original decoded credential. It is available only on the live request. */
39
+ credential: string
40
+ amount: bigint
41
+ currencyDecimals: number
42
+ }
43
+
44
+ export type MppChargeRecoveryResult =
45
+ | MppChargeOperation
46
+ /** `not-found` is final and must fence this operation ID against a later charge. */
47
+ | { operationId: string; state: 'not-found' | 'pending' }
48
+
49
+ /**
50
+ * Immediate-charge lifecycle for a non-BlueprinTEVM MPP method.
51
+ *
52
+ * `confirmPayment` runs after every request denial and before a response or
53
+ * sandbox call. It must use `operationId` as its processor idempotency key,
54
+ * confirm payment, verify final success, and only then return `confirmed`.
55
+ *
56
+ * Every method must also support id-only recovery and an idempotent release.
57
+ * Recovery must inspect the existing processor operation. It must never
58
+ * create a second charge when an acknowledgement is ambiguous.
59
+ */
60
+ export interface MppChargeLifecycle {
61
+ readonly protocolVersion: typeof MPP_CHARGE_PROTOCOL_VERSION
62
+ confirmPayment(request: MppChargeRequest): Promise<MppChargeOperation>
63
+ releasePayment(operation: MppChargeOperation, reason: string): Promise<MppChargeOperation>
64
+ recoverPayment(operationId: string): Promise<MppChargeRecoveryResult>
65
+ }
66
+
67
+ /** Stable gateway identity. Neither the credential nor adapter identity is persisted. */
68
+ export async function mppPaymentOperationId(
69
+ method: string,
70
+ paymentIdentity: string,
71
+ ): Promise<string> {
72
+ const normalizedMethod = method.trim().toLowerCase()
73
+ if (!normalizedMethod || normalizedMethod.length > 128) {
74
+ throw new Error('MPP payment method is invalid')
75
+ }
76
+ if (!paymentIdentity || paymentIdentity.length > 8192) {
77
+ throw new Error('MPP payment identity is invalid')
78
+ }
79
+ const digest = await globalThis.crypto.subtle.digest(
80
+ 'SHA-256',
81
+ new TextEncoder().encode(`${normalizedMethod}\0${paymentIdentity}`),
82
+ )
83
+ const fingerprint = [...new Uint8Array(digest)]
84
+ .map((byte) => byte.toString(16).padStart(2, '0'))
85
+ .join('')
86
+ return `mpp:${normalizedMethod}:${fingerprint}`
87
+ }
88
+
89
+ export function assertMppChargeOperation(
90
+ operation: MppChargeOperation,
91
+ expected: { operationId: string; requestId: string; method: string },
92
+ allowedStates: readonly MppChargeOperationState[],
93
+ requireReceipt = true,
94
+ ): void {
95
+ if (operation.protocolVersion !== MPP_CHARGE_PROTOCOL_VERSION) {
96
+ throw new Error('MPP charge operation protocol version mismatch')
97
+ }
98
+ if (operation.operationId !== expected.operationId) {
99
+ throw new Error('MPP charge operation id mismatch')
100
+ }
101
+ if (operation.acquiredByRequestId !== expected.requestId) {
102
+ throw new Error('MPP charge operation request owner mismatch')
103
+ }
104
+ if (operation.method.toLowerCase() !== expected.method.toLowerCase()) {
105
+ throw new Error('MPP charge operation method mismatch')
106
+ }
107
+ if (!allowedStates.includes(operation.state)) {
108
+ throw new Error(`MPP charge operation is in invalid state ${operation.state}`)
109
+ }
110
+ if (requireReceipt && (
111
+ !operation.receipt ||
112
+ operation.receipt.length > 8192 ||
113
+ /[^\x20-\x7e]/.test(operation.receipt)
114
+ )) {
115
+ throw new Error('MPP charge operation has an invalid payment receipt')
116
+ }
117
+ }