@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/verify.ts CHANGED
@@ -1,5 +1,126 @@
1
- import type { X402Config, MppConfig, ApiKeyInfo } from './types'
2
- import type { NonceStore } from './nonce-store'
1
+ import type { X402Config, MppConfig, ApiKeyInfo, GatewayConfig } from './types'
2
+ import { claimStoredNonce, nonceTtlSeconds, type NonceStore } from './nonce-store'
3
+ import {
4
+ mppPaymentOperationId,
5
+ type MppAuthenticatedCredential,
6
+ } from './mpp-payment'
7
+
8
+ export interface VerifiedMppCredential extends MppAuthenticatedCredential {
9
+ /** Opaque replay key. BlueprinTEVM shares the x402 nonce namespace. */
10
+ replayKey: string
11
+ }
12
+
13
+ /** Return the legacy opaque nonce key used by older consumers. */
14
+ export function mppReplayNonceKey(authHeader: string): string | undefined {
15
+ const decoded = decodeMppCredential(authHeader)
16
+ return decoded ? canonicalMppNonceKey(decoded.method, decoded.payload, decoded.credential) : undefined
17
+ }
18
+
19
+ /** Return the decoded MPP payload for a durable payment claim. */
20
+ export function mppPaymentPayload(authHeader: string): Record<string, unknown> | undefined {
21
+ return decodeMppCredential(authHeader)?.payload
22
+ }
23
+
24
+ /** Return the decoded method credential for the post-guard charge lifecycle. */
25
+ export function mppPaymentCredential(authHeader: string): string | undefined {
26
+ return decodeMppCredential(authHeader)?.credential
27
+ }
28
+
29
+ interface DecodedMppCredential {
30
+ method: string
31
+ credential: string
32
+ payload: Record<string, unknown>
33
+ }
34
+
35
+ function decodeMppCredential(authHeader: string): DecodedMppCredential | undefined {
36
+ const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i)
37
+ if (!match) return undefined
38
+ const [, rawMethod, credentialB64] = match
39
+ if (!/^[A-Za-z0-9_-]+$/.test(credentialB64)) return undefined
40
+ try {
41
+ const decoded = Buffer.from(credentialB64, 'base64url').toString('utf-8')
42
+ let payload: Record<string, unknown> = {}
43
+ try {
44
+ const credential = JSON.parse(decoded) as unknown
45
+ if (credential && typeof credential === 'object' && !Array.isArray(credential)) {
46
+ const record = credential as Record<string, unknown>
47
+ const nested = record.payload
48
+ payload = nested && typeof nested === 'object' && !Array.isArray(nested)
49
+ ? nested as Record<string, unknown>
50
+ : record
51
+ }
52
+ } catch {
53
+ // Method-specific verifiers may accept a non-JSON credential format.
54
+ }
55
+ return { method: rawMethod.toLowerCase(), credential: decoded, payload }
56
+ } catch {
57
+ return undefined
58
+ }
59
+ }
60
+
61
+ function canonicalMppNonceKey(
62
+ method: string,
63
+ payload: Record<string, unknown>,
64
+ credential: string,
65
+ ): string {
66
+ if (payload.nonce === undefined) {
67
+ return `mpp:${method.toLowerCase()}:receipt:${Buffer.from(credential).toString('base64url')}`
68
+ }
69
+ const nonce = BigInt(String(payload.nonce)).toString()
70
+ const commitment = payload.commitment
71
+ if (method.toLowerCase() === 'blueprintevm' && typeof commitment === 'string' && commitment.length > 0) {
72
+ return `${commitment.toLowerCase()}:${nonce}`
73
+ }
74
+ return `mpp:${method.toLowerCase()}:${String(payload.commitment ?? payload.from ?? 'unknown').toLowerCase()}:${nonce}`
75
+ }
76
+
77
+ function blueprintevmNonceKey(payload: Record<string, unknown>): string | undefined {
78
+ if (payload.nonce === undefined) return undefined
79
+ const nonce = BigInt(String(payload.nonce)).toString()
80
+ const identity = payload.commitment ?? payload.from
81
+ if (typeof identity !== 'string' || identity.length === 0) return undefined
82
+ return `${identity.toLowerCase()}:${nonce}`
83
+ }
84
+
85
+ function stableJson(value: unknown): string {
86
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`
87
+ if (value && typeof value === 'object') {
88
+ const record = value as Record<string, unknown>
89
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(',')}}`
90
+ }
91
+ return JSON.stringify(value)
92
+ }
93
+
94
+ function legacyMppPaymentIdentity(
95
+ method: string,
96
+ payload: Record<string, unknown>,
97
+ credential: string,
98
+ ): string {
99
+ const canonicalPayload = stableJson(payload)
100
+ if (canonicalPayload !== '{}') return `legacy:${method}:${canonicalPayload}`
101
+ return `legacy:${method}:credential:${Buffer.from(credential).toString('base64url')}`
102
+ }
103
+
104
+ /** Pure capability checks shared by discovery and every request protocol. */
105
+ export function isApiKeyAuthEnabled(
106
+ config: Pick<GatewayConfig, 'verifyApiKey' | 'x402'>,
107
+ ): boolean {
108
+ return config.verifyApiKey !== undefined || config.x402.demoMode === true
109
+ }
110
+
111
+ /** MPP is enabled only when authentication and method settlement are complete. */
112
+ export function isMppAuthEnabled(
113
+ config: Pick<GatewayConfig, 'mpp' | 'x402'>,
114
+ ): boolean {
115
+ const method = (config.mpp?.method ?? 'blueprintevm').toLowerCase()
116
+ if (!config.mpp) return false
117
+ const authenticated = typeof config.mpp.authenticateCredential === 'function' ||
118
+ typeof config.mpp.verifySigner === 'function' ||
119
+ (method === 'blueprintevm' && config.x402.verifySigner !== undefined) ||
120
+ (method === 'blueprintevm' && config.x402.demoMode === true)
121
+ if (!authenticated) return false
122
+ return method === 'blueprintevm' || config.mpp.charge !== undefined
123
+ }
3
124
 
4
125
  /**
5
126
  * Verify x402 SpendAuth signature (EIP-712).
@@ -16,6 +137,8 @@ export async function verifyX402(
16
137
  spendAuthHeader: string,
17
138
  config: X402Config,
18
139
  nonceStore?: NonceStore,
140
+ minimumAmount = 1n,
141
+ markNonce = true,
19
142
  ): Promise<string | null> {
20
143
  try {
21
144
  const raw = JSON.parse(spendAuthHeader)
@@ -29,25 +152,32 @@ export async function verifyX402(
29
152
  // Reject expired payments
30
153
  if (expiry < BigInt(Math.floor(Date.now() / 1000))) return null
31
154
 
32
- // Reject zero-amount payments
33
- if (amount <= 0n) return null
155
+ // Reject payments that cannot cover the request's maximum charge. The
156
+ // check runs before the host verifier because that callback can reserve or
157
+ // settle funds as part of its production verification path.
158
+ if (amount <= 0n || minimumAmount < 0n || amount < minimumAmount) return null
34
159
 
35
- // Reject replayed nonces
36
- const nonceKey = `${raw.commitment}:${nonce.toString()}`
37
- if (nonceStore) {
38
- if (await nonceStore.hasSeen(nonceKey)) return null
39
- // Mark seen with TTL matching the expiry window (max 1 hour)
40
- const ttl = Math.min(Number(expiry) - Math.floor(Date.now() / 1000), 3600)
41
- await nonceStore.markSeen(nonceKey, Math.max(ttl, 60))
42
- }
160
+ const nonceKey = `${String(raw.commitment).toLowerCase()}:${nonce.toString()}`
161
+ if (nonceStore?.hasSeen && await nonceStore.hasSeen(nonceKey)) return null
43
162
 
44
163
  if (config.verifySigner) {
45
- const verified = await config.verifySigner(raw)
164
+ const verified = await config.verifySigner(raw, {
165
+ protocolVersion: config.paymentProtocolVersion ?? (config.paymentOperations ? 2 : 1),
166
+ })
46
167
  if (!verified) return null
47
168
  } else if (!config.demoMode) {
48
169
  return null
49
170
  }
50
171
 
172
+ // Claim only after the signature is accepted. Otherwise invalid traffic
173
+ // can burn a valid payer nonce and deny the real request.
174
+ if (nonceStore && markNonce) {
175
+ const ttl = nonceTtlSeconds(expiry)
176
+ if (ttl === undefined) return null
177
+ const claimed = await claimStoredNonce(nonceStore, nonceKey, ttl)
178
+ if (!claimed) return null
179
+ }
180
+
51
181
  return raw.commitment
52
182
  } catch {
53
183
  return null
@@ -57,48 +187,147 @@ export async function verifyX402(
57
187
  /**
58
188
  * Verify MPP (Machine Payments Protocol) Authorization: Payment header.
59
189
  *
60
- * MPP uses `Authorization: Payment <method> <credential>` format where
61
- * the credential is a base64url-encoded JSON wrapping the same EIP-3009
62
- * payment payload that x402 uses. This means existing x402 wallets work
63
- * unchanged over the MPP wire format.
190
+ * MPP uses `Authorization: Payment <method> <credential>` format. The
191
+ * credential is method-specific; `MppConfig.authenticateCredential` owns authentication
192
+ * and returns the consumer plus stable payment identity. The built-in `blueprintevm` path can
193
+ * reuse the x402 verifier for credentials with the compatible payload shape.
64
194
  *
65
- * Returns the signer address if valid, null otherwise.
66
- * In demo mode, accepts any well-formed Payment header.
195
+ * Returns authenticated identity if valid, null otherwise.
196
+ * In demo mode, accepts any well-formed Payment header with an identity.
67
197
  */
68
- export async function verifyMpp(
198
+ export async function verifyMppCredential(
69
199
  authHeader: string,
70
- _config: MppConfig,
200
+ config: MppConfig,
71
201
  x402Config: X402Config,
72
- ): Promise<string | null> {
202
+ nonceStore?: NonceStore,
203
+ minimumAmount = 1n,
204
+ markNonce = true,
205
+ ): Promise<VerifiedMppCredential | null> {
73
206
  // MPP format: "Payment <method> <base64url-credential>"
74
207
  const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i)
75
208
  if (!match) return null
76
209
 
77
- const [, , credentialB64] = match
210
+ const [, rawMethod] = match
211
+ const method = rawMethod.toLowerCase()
212
+ if (method !== (config.method ?? 'blueprintevm').toLowerCase()) return null
78
213
 
79
214
  try {
80
- // Decode base64url credential → JSON with the same EIP-3009 payload
81
- const decoded = Buffer.from(credentialB64, 'base64url').toString('utf-8')
82
- const credential = JSON.parse(decoded)
215
+ const decodedCredential = decodeMppCredential(authHeader)
216
+ if (!decodedCredential) return null
217
+ const { credential: decoded, payload } = decodedCredential
83
218
 
84
- // The credential payload wraps the same fields x402 uses
85
- const payload = credential.payload ?? credential
86
- if (!payload.commitment && !payload.from) return null
87
-
88
- // Validate operator match (same as x402)
219
+ // Validate common EVM fields before pure credential authentication.
220
+ // BlueprinTEVM carries x402-equivalent token amounts and must cover the
221
+ // same request ceiling as the X-Payment-Signature path.
89
222
  const operator = payload.operator ?? payload.to
90
- if (operator && operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) return null
223
+ if (operator !== undefined) {
224
+ if (typeof operator !== 'string' || operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) {
225
+ return null
226
+ }
227
+ }
228
+ const paymentAmount = payload.amount ?? payload.value
229
+ if (paymentAmount !== undefined) {
230
+ const amount = BigInt(String(paymentAmount))
231
+ if (amount <= 0n || (method === 'blueprintevm' && amount < minimumAmount)) return null
232
+ } else if (method === 'blueprintevm') {
233
+ return null
234
+ }
235
+ if (payload.nonce !== undefined) BigInt(String(payload.nonce))
236
+ if (payload.expiry !== undefined && BigInt(String(payload.expiry)) < BigInt(Math.floor(Date.now() / 1000))) {
237
+ return null
238
+ }
91
239
 
92
- // Validate bigint fields if present
93
- if (payload.amount) BigInt(payload.amount)
94
- if (payload.nonce) BigInt(payload.nonce)
240
+ const blueprintevmKey = method === 'blueprintevm'
241
+ ? blueprintevmNonceKey(payload)
242
+ : undefined
243
+ if (markNonce && blueprintevmKey && nonceStore?.hasSeen && await nonceStore.hasSeen(blueprintevmKey)) {
244
+ return null
245
+ }
95
246
 
96
- return payload.commitment ?? payload.from ?? null
247
+ let authenticated: MppAuthenticatedCredential | null = null
248
+ if (config.authenticateCredential) {
249
+ authenticated = await config.authenticateCredential(payload, { method, credential: decoded })
250
+ } else if (config.verifySigner) {
251
+ const consumerId = await config.verifySigner(payload, { method, credential: decoded })
252
+ authenticated = typeof consumerId === 'string' && consumerId.length > 0
253
+ ? {
254
+ consumerId,
255
+ paymentIdentity: legacyMppPaymentIdentity(method, payload, decoded),
256
+ }
257
+ : null
258
+ } else if (method === 'blueprintevm' && x402Config.verifySigner && payload.commitment) {
259
+ const verified = await x402Config.verifySigner(payload, {
260
+ protocolVersion: x402Config.paymentProtocolVersion ?? (x402Config.paymentOperations ? 2 : 1),
261
+ })
262
+ const paymentIdentity = blueprintevmNonceKey(payload) ?? stableJson(payload)
263
+ authenticated = verified
264
+ ? { consumerId: String(payload.commitment), paymentIdentity }
265
+ : null
266
+ } else if (x402Config.demoMode) {
267
+ const identity = payload.commitment ?? payload.from
268
+ if (typeof identity !== 'string' || identity.length === 0) return null
269
+ const paymentIdentity = method === 'blueprintevm'
270
+ ? blueprintevmNonceKey(payload) ?? stableJson(payload)
271
+ : ''
272
+ if (!paymentIdentity) return null
273
+ authenticated = { consumerId: identity, paymentIdentity }
274
+ } else {
275
+ return null
276
+ }
277
+ if (
278
+ !authenticated ||
279
+ typeof authenticated.consumerId !== 'string' ||
280
+ authenticated.consumerId.length === 0 ||
281
+ typeof authenticated.paymentIdentity !== 'string' ||
282
+ authenticated.paymentIdentity.length === 0
283
+ ) return null
284
+
285
+ const replayKey = method === 'blueprintevm'
286
+ ? blueprintevmKey ??
287
+ await mppPaymentOperationId(method, authenticated.paymentIdentity)
288
+ : await mppPaymentOperationId(method, authenticated.paymentIdentity)
289
+ if (!replayKey) return null
290
+ if (markNonce && !blueprintevmKey && nonceStore?.hasSeen && await nonceStore.hasSeen(replayKey)) return null
291
+
292
+ if (nonceStore && markNonce) {
293
+ const expiry = payload.expiry === undefined
294
+ ? BigInt(Math.floor(Date.now() / 1000) + 3600)
295
+ : BigInt(String(payload.expiry))
296
+ const ttl = nonceTtlSeconds(expiry)
297
+ if (ttl === undefined) return null
298
+ const claimed = await claimStoredNonce(nonceStore, replayKey, ttl)
299
+ if (!claimed) return null
300
+ }
301
+
302
+ return { ...authenticated, replayKey }
97
303
  } catch {
98
304
  return null
99
305
  }
100
306
  }
101
307
 
308
+ /**
309
+ * Verify an MPP credential using the 0.7.1 public return shape.
310
+ * Rich durable callers use verifyMppCredential instead.
311
+ */
312
+ export async function verifyMpp(
313
+ authHeader: string,
314
+ config: MppConfig,
315
+ x402Config: X402Config,
316
+ nonceStore?: NonceStore,
317
+ minimumAmount = 1n,
318
+ markNonce = true,
319
+ ): Promise<string | null> {
320
+ const authenticated = await verifyMppCredential(
321
+ authHeader,
322
+ config,
323
+ x402Config,
324
+ nonceStore,
325
+ minimumAmount,
326
+ markNonce,
327
+ )
328
+ return authenticated?.consumerId ?? null
329
+ }
330
+
102
331
  /**
103
332
  * Default API key verifier — accepts any `sk_agent_*` key (demo mode).
104
333
  * Override in GatewayConfig.verifyApiKey for production.