@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.
- package/README.md +108 -6
- package/dist/chunk-GITV7CPT.js +84 -0
- package/dist/chunk-GITV7CPT.js.map +1 -0
- package/dist/chunk-J5SDVHOL.js +104 -0
- package/dist/chunk-J5SDVHOL.js.map +1 -0
- package/dist/chunk-MP6IIAIA.js +5651 -0
- package/dist/chunk-MP6IIAIA.js.map +1 -0
- package/dist/index.d.ts +76 -12
- package/dist/index.js +307 -21
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +7 -2
- package/dist/middleware.js +3 -2
- package/dist/nonce-store.d.ts +47 -11
- package/dist/nonce-store.js +9 -3
- package/dist/observer-types-A0RtA8uL.d.ts +95 -0
- package/dist/observer.d.ts +79 -0
- package/dist/observer.js +11 -0
- package/dist/observer.js.map +1 -0
- package/dist/{types-CX2V06cN.d.ts → types-BHISsm7D.d.ts} +423 -166
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/a2a/agent-card.ts +4 -3
- package/src/a2a/execution-fence.ts +162 -0
- package/src/a2a/handler.ts +507 -562
- package/src/a2a/message-send-execution.ts +241 -0
- package/src/a2a/message-stream-execution.ts +392 -0
- package/src/a2a/payment-recovery.ts +431 -0
- package/src/a2a/push-config-methods.ts +158 -0
- package/src/a2a/push-notifications.ts +172 -22
- package/src/a2a/task-cancellation.ts +50 -0
- package/src/a2a/task-finalization.ts +451 -0
- package/src/a2a/task-lifecycle.ts +54 -0
- package/src/a2a/task-methods.ts +163 -0
- package/src/a2a/task-push-delivery.ts +119 -0
- package/src/a2a/task-recovery.ts +11 -0
- package/src/a2a/task-state.ts +99 -0
- package/src/a2a/task-store-sql.ts +222 -24
- package/src/a2a/task-store.ts +58 -1
- package/src/a2a/task-submission-recovery.ts +178 -0
- package/src/a2a/types.ts +1 -0
- package/src/dispatch-authorization.ts +437 -0
- package/src/dispatch-payment-recovery.ts +248 -0
- package/src/dispatch-payment.ts +425 -0
- package/src/dispatch-pricing.ts +108 -0
- package/src/dispatch-sandbox.ts +422 -0
- package/src/dispatch-settlement.ts +139 -0
- package/src/dispatch-types.ts +81 -0
- package/src/dispatch.ts +35 -462
- package/src/index.ts +64 -2
- package/src/middleware.ts +313 -32
- package/src/mpp-payment.ts +117 -0
- package/src/nonce-store.ts +122 -20
- package/src/observer-types.ts +63 -0
- package/src/observer.ts +3 -63
- package/src/payment-operations.ts +485 -0
- package/src/payment-recovery-sql.ts +108 -0
- package/src/payment-recovery-worker.ts +488 -0
- package/src/payment-recovery.ts +331 -0
- package/src/payment-types.ts +48 -0
- package/src/types.ts +153 -42
- package/src/verify.ts +265 -36
- package/dist/chunk-3IKQWFKX.js +0 -1703
- package/dist/chunk-3IKQWFKX.js.map +0 -1
- package/dist/chunk-M7ZJAK4K.js +0 -53
- 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
|
|
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
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
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
|
|
198
|
+
export async function verifyMppCredential(
|
|
69
199
|
authHeader: string,
|
|
70
|
-
|
|
200
|
+
config: MppConfig,
|
|
71
201
|
x402Config: X402Config,
|
|
72
|
-
|
|
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 [,
|
|
210
|
+
const [, rawMethod] = match
|
|
211
|
+
const method = rawMethod.toLowerCase()
|
|
212
|
+
if (method !== (config.method ?? 'blueprintevm').toLowerCase()) return null
|
|
78
213
|
|
|
79
214
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const credential =
|
|
215
|
+
const decodedCredential = decodeMppCredential(authHeader)
|
|
216
|
+
if (!decodedCredential) return null
|
|
217
|
+
const { credential: decoded, payload } = decodedCredential
|
|
83
218
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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.
|