@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.
- package/README.md +90 -3
- package/dist/chunk-C7Z2BRYV.js +5693 -0
- package/dist/chunk-C7Z2BRYV.js.map +1 -0
- 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/index.d.ts +70 -10
- package/dist/index.js +303 -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-DEsMmS-X.d.ts → types-oQ58UakD.d.ts} +447 -172
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/a2a/execution-fence.ts +162 -0
- package/src/a2a/handler.ts +506 -560
- 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 +468 -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 +424 -0
- package/src/dispatch-settlement.ts +139 -0
- package/src/dispatch-types.ts +84 -0
- package/src/dispatch.ts +35 -483
- package/src/index.ts +59 -1
- package/src/middleware.ts +339 -35
- 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 +188 -49
- package/src/verify.ts +240 -71
- package/dist/chunk-M7ZJAK4K.js +0 -53
- package/dist/chunk-M7ZJAK4K.js.map +0 -1
- package/dist/chunk-Q4YAIEZY.js +0 -1763
- package/dist/chunk-Q4YAIEZY.js.map +0 -1
package/src/verify.ts
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
import type { X402Config, MppConfig, ApiKeyInfo, GatewayConfig } from './types'
|
|
2
|
-
import type
|
|
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
|
+
}
|
|
3
103
|
|
|
4
104
|
/** Pure capability checks shared by discovery and every request protocol. */
|
|
5
105
|
export function isApiKeyAuthEnabled(
|
|
@@ -8,17 +108,25 @@ export function isApiKeyAuthEnabled(
|
|
|
8
108
|
return config.verifyApiKey !== undefined || config.x402.demoMode === true
|
|
9
109
|
}
|
|
10
110
|
|
|
11
|
-
/**
|
|
111
|
+
/** x402 is advertised only when the gateway can authenticate it. */
|
|
112
|
+
export function isX402AuthEnabled(
|
|
113
|
+
config: Pick<GatewayConfig, 'x402'>,
|
|
114
|
+
): boolean {
|
|
115
|
+
return config.x402.verifySigner !== undefined || config.x402.demoMode === true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** MPP is enabled only when authentication and method settlement are complete. */
|
|
12
119
|
export function isMppAuthEnabled(
|
|
13
120
|
config: Pick<GatewayConfig, 'mpp' | 'x402'>,
|
|
14
121
|
): boolean {
|
|
15
|
-
const method = config.mpp?.method ?? 'blueprintevm'
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
122
|
+
const method = (config.mpp?.method ?? 'blueprintevm').toLowerCase()
|
|
123
|
+
if (!config.mpp) return false
|
|
124
|
+
const authenticated = typeof config.mpp.authenticateCredential === 'function' ||
|
|
125
|
+
typeof config.mpp.verifySigner === 'function' ||
|
|
126
|
+
(method === 'blueprintevm' && config.x402.verifySigner !== undefined) ||
|
|
127
|
+
(method === 'blueprintevm' && config.x402.demoMode === true)
|
|
128
|
+
if (!authenticated) return false
|
|
129
|
+
return method === 'blueprintevm' || config.mpp.charge !== undefined
|
|
22
130
|
}
|
|
23
131
|
|
|
24
132
|
/**
|
|
@@ -36,6 +144,8 @@ export async function verifyX402(
|
|
|
36
144
|
spendAuthHeader: string,
|
|
37
145
|
config: X402Config,
|
|
38
146
|
nonceStore?: NonceStore,
|
|
147
|
+
minimumAmount = 1n,
|
|
148
|
+
markNonce = true,
|
|
39
149
|
): Promise<string | null> {
|
|
40
150
|
try {
|
|
41
151
|
const raw = JSON.parse(spendAuthHeader)
|
|
@@ -49,25 +159,30 @@ export async function verifyX402(
|
|
|
49
159
|
// Reject expired payments
|
|
50
160
|
if (expiry < BigInt(Math.floor(Date.now() / 1000))) return null
|
|
51
161
|
|
|
52
|
-
// Reject
|
|
53
|
-
|
|
162
|
+
// Reject payments that cannot cover the request's maximum charge. The
|
|
163
|
+
// check runs before the host verifier because that callback can reserve or
|
|
164
|
+
// settle funds as part of its production verification path.
|
|
165
|
+
if (amount <= 0n || minimumAmount < 0n || amount < minimumAmount) return null
|
|
54
166
|
|
|
55
|
-
const nonceKey = `${raw.commitment}:${nonce.toString()}`
|
|
56
|
-
if (nonceStore && await nonceStore.hasSeen(nonceKey)) return null
|
|
167
|
+
const nonceKey = `${String(raw.commitment).toLowerCase()}:${nonce.toString()}`
|
|
168
|
+
if (nonceStore?.hasSeen && await nonceStore.hasSeen(nonceKey)) return null
|
|
57
169
|
|
|
58
170
|
if (config.verifySigner) {
|
|
59
|
-
const verified = await config.verifySigner(raw
|
|
171
|
+
const verified = await config.verifySigner(raw, {
|
|
172
|
+
protocolVersion: config.paymentProtocolVersion ?? (config.paymentOperations ? 2 : 1),
|
|
173
|
+
})
|
|
60
174
|
if (!verified) return null
|
|
61
175
|
} else if (!config.demoMode) {
|
|
62
176
|
return null
|
|
63
177
|
}
|
|
64
178
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
if (nonceStore) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
await nonceStore
|
|
179
|
+
// Claim only after the signature is accepted. Otherwise invalid traffic
|
|
180
|
+
// can burn a valid payer nonce and deny the real request.
|
|
181
|
+
if (nonceStore && markNonce) {
|
|
182
|
+
const ttl = nonceTtlSeconds(expiry)
|
|
183
|
+
if (ttl === undefined) return null
|
|
184
|
+
const claimed = await claimStoredNonce(nonceStore, nonceKey, ttl)
|
|
185
|
+
if (!claimed) return null
|
|
71
186
|
}
|
|
72
187
|
|
|
73
188
|
return raw.commitment
|
|
@@ -80,92 +195,146 @@ export async function verifyX402(
|
|
|
80
195
|
* Verify MPP (Machine Payments Protocol) Authorization: Payment header.
|
|
81
196
|
*
|
|
82
197
|
* MPP uses `Authorization: Payment <method> <credential>` format. The
|
|
83
|
-
* credential is method-specific; `MppConfig.
|
|
84
|
-
* and returns the consumer identity. The built-in `blueprintevm` path can
|
|
198
|
+
* credential is method-specific; `MppConfig.authenticateCredential` owns authentication
|
|
199
|
+
* and returns the consumer plus stable payment identity. The built-in `blueprintevm` path can
|
|
85
200
|
* reuse the x402 verifier for credentials with the compatible payload shape.
|
|
86
201
|
*
|
|
87
|
-
* Returns
|
|
202
|
+
* Returns authenticated identity if valid, null otherwise.
|
|
88
203
|
* In demo mode, accepts any well-formed Payment header with an identity.
|
|
89
204
|
*/
|
|
90
|
-
export async function
|
|
205
|
+
export async function verifyMppCredential(
|
|
91
206
|
authHeader: string,
|
|
92
207
|
config: MppConfig,
|
|
93
208
|
x402Config: X402Config,
|
|
94
209
|
nonceStore?: NonceStore,
|
|
95
|
-
|
|
210
|
+
minimumAmount = 1n,
|
|
211
|
+
markNonce = true,
|
|
212
|
+
): Promise<VerifiedMppCredential | null> {
|
|
96
213
|
// MPP format: "Payment <method> <base64url-credential>"
|
|
97
214
|
const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i)
|
|
98
215
|
if (!match) return null
|
|
99
216
|
|
|
100
|
-
const [,
|
|
101
|
-
|
|
217
|
+
const [, rawMethod] = match
|
|
218
|
+
const method = rawMethod.toLowerCase()
|
|
219
|
+
if (method !== (config.method ?? 'blueprintevm').toLowerCase()) return null
|
|
102
220
|
|
|
103
221
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
222
|
+
const decodedCredential = decodeMppCredential(authHeader)
|
|
223
|
+
if (!decodedCredential) return null
|
|
224
|
+
const { credential: decoded, payload } = decodedCredential
|
|
225
|
+
|
|
226
|
+
// Validate common EVM fields before pure credential authentication.
|
|
227
|
+
// BlueprinTEVM carries x402-equivalent token amounts and must cover the
|
|
228
|
+
// same request ceiling as the X-Payment-Signature path.
|
|
229
|
+
const operator = payload.operator ?? payload.to
|
|
230
|
+
if (operator !== undefined) {
|
|
231
|
+
if (typeof operator !== 'string' || operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) {
|
|
232
|
+
return null
|
|
115
233
|
}
|
|
116
|
-
}
|
|
117
|
-
|
|
234
|
+
}
|
|
235
|
+
const paymentAmount = payload.amount ?? payload.value
|
|
236
|
+
if (paymentAmount !== undefined) {
|
|
237
|
+
const amount = BigInt(String(paymentAmount))
|
|
238
|
+
if (amount <= 0n || (method === 'blueprintevm' && amount < minimumAmount)) return null
|
|
239
|
+
} else if (method === 'blueprintevm') {
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
if (payload.nonce !== undefined) BigInt(String(payload.nonce))
|
|
243
|
+
if (payload.expiry !== undefined && BigInt(String(payload.expiry)) < BigInt(Math.floor(Date.now() / 1000))) {
|
|
244
|
+
return null
|
|
118
245
|
}
|
|
119
246
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
247
|
+
const blueprintevmKey = method === 'blueprintevm'
|
|
248
|
+
? blueprintevmNonceKey(payload)
|
|
249
|
+
: undefined
|
|
250
|
+
if (markNonce && blueprintevmKey && nonceStore?.hasSeen && await nonceStore.hasSeen(blueprintevmKey)) {
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
125
253
|
|
|
126
|
-
let
|
|
127
|
-
if (config.
|
|
128
|
-
|
|
254
|
+
let authenticated: MppAuthenticatedCredential | null = null
|
|
255
|
+
if (config.authenticateCredential) {
|
|
256
|
+
authenticated = await config.authenticateCredential(payload, { method, credential: decoded })
|
|
257
|
+
} else if (config.verifySigner) {
|
|
258
|
+
const consumerId = await config.verifySigner(payload, { method, credential: decoded })
|
|
259
|
+
authenticated = typeof consumerId === 'string' && consumerId.length > 0
|
|
260
|
+
? {
|
|
261
|
+
consumerId,
|
|
262
|
+
paymentIdentity: legacyMppPaymentIdentity(method, payload, decoded),
|
|
263
|
+
}
|
|
264
|
+
: null
|
|
129
265
|
} else if (method === 'blueprintevm' && x402Config.verifySigner && payload.commitment) {
|
|
130
|
-
const verified = await x402Config.verifySigner(payload
|
|
131
|
-
|
|
266
|
+
const verified = await x402Config.verifySigner(payload, {
|
|
267
|
+
protocolVersion: x402Config.paymentProtocolVersion ?? (x402Config.paymentOperations ? 2 : 1),
|
|
268
|
+
})
|
|
269
|
+
const paymentIdentity = blueprintevmNonceKey(payload) ?? stableJson(payload)
|
|
270
|
+
authenticated = verified
|
|
271
|
+
? { consumerId: String(payload.commitment), paymentIdentity }
|
|
272
|
+
: null
|
|
132
273
|
} else if (x402Config.demoMode) {
|
|
133
274
|
const identity = payload.commitment ?? payload.from
|
|
134
275
|
if (typeof identity !== 'string' || identity.length === 0) return null
|
|
135
|
-
|
|
276
|
+
const paymentIdentity = method === 'blueprintevm'
|
|
277
|
+
? blueprintevmNonceKey(payload) ?? stableJson(payload)
|
|
278
|
+
: ''
|
|
279
|
+
if (!paymentIdentity) return null
|
|
280
|
+
authenticated = { consumerId: identity, paymentIdentity }
|
|
136
281
|
} else {
|
|
137
282
|
return null
|
|
138
283
|
}
|
|
139
|
-
if (
|
|
284
|
+
if (
|
|
285
|
+
!authenticated ||
|
|
286
|
+
typeof authenticated.consumerId !== 'string' ||
|
|
287
|
+
authenticated.consumerId.length === 0 ||
|
|
288
|
+
typeof authenticated.paymentIdentity !== 'string' ||
|
|
289
|
+
authenticated.paymentIdentity.length === 0
|
|
290
|
+
) return null
|
|
140
291
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
if (payload.amount !== undefined && BigInt(String(payload.amount)) <= 0n) return null
|
|
150
|
-
if (payload.nonce !== undefined) BigInt(String(payload.nonce))
|
|
151
|
-
if (payload.expiry !== undefined && BigInt(String(payload.expiry)) < BigInt(Math.floor(Date.now() / 1000))) {
|
|
152
|
-
return null
|
|
153
|
-
}
|
|
292
|
+
const replayKey = method === 'blueprintevm'
|
|
293
|
+
? blueprintevmKey ??
|
|
294
|
+
await mppPaymentOperationId(method, authenticated.paymentIdentity)
|
|
295
|
+
: await mppPaymentOperationId(method, authenticated.paymentIdentity)
|
|
296
|
+
if (!replayKey) return null
|
|
297
|
+
if (markNonce && !blueprintevmKey && nonceStore?.hasSeen && await nonceStore.hasSeen(replayKey)) return null
|
|
154
298
|
|
|
155
|
-
if (nonceStore &&
|
|
299
|
+
if (nonceStore && markNonce) {
|
|
156
300
|
const expiry = payload.expiry === undefined
|
|
157
|
-
? Math.floor(Date.now() / 1000) + 3600
|
|
158
|
-
:
|
|
159
|
-
const ttl =
|
|
160
|
-
|
|
301
|
+
? BigInt(Math.floor(Date.now() / 1000) + 3600)
|
|
302
|
+
: BigInt(String(payload.expiry))
|
|
303
|
+
const ttl = nonceTtlSeconds(expiry)
|
|
304
|
+
if (ttl === undefined) return null
|
|
305
|
+
const claimed = await claimStoredNonce(nonceStore, replayKey, ttl)
|
|
306
|
+
if (!claimed) return null
|
|
161
307
|
}
|
|
162
308
|
|
|
163
|
-
return
|
|
309
|
+
return { ...authenticated, replayKey }
|
|
164
310
|
} catch {
|
|
165
311
|
return null
|
|
166
312
|
}
|
|
167
313
|
}
|
|
168
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Verify an MPP credential using the 0.7.1 public return shape.
|
|
317
|
+
* Rich durable callers use verifyMppCredential instead.
|
|
318
|
+
*/
|
|
319
|
+
export async function verifyMpp(
|
|
320
|
+
authHeader: string,
|
|
321
|
+
config: MppConfig,
|
|
322
|
+
x402Config: X402Config,
|
|
323
|
+
nonceStore?: NonceStore,
|
|
324
|
+
minimumAmount = 1n,
|
|
325
|
+
markNonce = true,
|
|
326
|
+
): Promise<string | null> {
|
|
327
|
+
const authenticated = await verifyMppCredential(
|
|
328
|
+
authHeader,
|
|
329
|
+
config,
|
|
330
|
+
x402Config,
|
|
331
|
+
nonceStore,
|
|
332
|
+
minimumAmount,
|
|
333
|
+
markNonce,
|
|
334
|
+
)
|
|
335
|
+
return authenticated?.consumerId ?? null
|
|
336
|
+
}
|
|
337
|
+
|
|
169
338
|
/**
|
|
170
339
|
* Default API key verifier — accepts any `sk_agent_*` key (demo mode).
|
|
171
340
|
* Override in GatewayConfig.verifyApiKey for production.
|
package/dist/chunk-M7ZJAK4K.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
// src/nonce-store.ts
|
|
2
|
-
var MemoryNonceStore = class {
|
|
3
|
-
seen = /* @__PURE__ */ new Map();
|
|
4
|
-
// nonce → expiresAt
|
|
5
|
-
lastEviction = Date.now();
|
|
6
|
-
async hasSeen(nonce) {
|
|
7
|
-
this.evictExpired();
|
|
8
|
-
const expiresAt = this.seen.get(nonce);
|
|
9
|
-
if (!expiresAt) return false;
|
|
10
|
-
if (expiresAt < Date.now()) {
|
|
11
|
-
this.seen.delete(nonce);
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
async markSeen(nonce, ttlSeconds) {
|
|
17
|
-
this.seen.set(nonce, Date.now() + ttlSeconds * 1e3);
|
|
18
|
-
this.evictExpired();
|
|
19
|
-
}
|
|
20
|
-
evictExpired() {
|
|
21
|
-
const now = Date.now();
|
|
22
|
-
if (now - this.lastEviction < 6e4) return;
|
|
23
|
-
this.lastEviction = now;
|
|
24
|
-
for (const [nonce, expiresAt] of this.seen) {
|
|
25
|
-
if (expiresAt < now) this.seen.delete(nonce);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
var KvNonceStore = class {
|
|
30
|
-
constructor(kv, prefix = "nonce") {
|
|
31
|
-
this.kv = kv;
|
|
32
|
-
this.prefix = prefix;
|
|
33
|
-
}
|
|
34
|
-
kv;
|
|
35
|
-
prefix;
|
|
36
|
-
async hasSeen(nonce) {
|
|
37
|
-
const value = await this.kv.get(this.key(nonce));
|
|
38
|
-
return value !== null;
|
|
39
|
-
}
|
|
40
|
-
async markSeen(nonce, ttlSeconds) {
|
|
41
|
-
const ttl = Math.max(ttlSeconds, 60);
|
|
42
|
-
await this.kv.put(this.key(nonce), "1", { expirationTtl: ttl });
|
|
43
|
-
}
|
|
44
|
-
key(nonce) {
|
|
45
|
-
return `${this.prefix}:${nonce}`;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export {
|
|
50
|
-
MemoryNonceStore,
|
|
51
|
-
KvNonceStore
|
|
52
|
-
};
|
|
53
|
-
//# sourceMappingURL=chunk-M7ZJAK4K.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/nonce-store.ts"],"sourcesContent":["/**\n * Nonce replay protection for x402/MPP payments.\n * Tracks seen nonces to prevent the same payment from being used twice.\n */\n\nexport interface NonceStore {\n /** Check if nonce has been seen. Returns true if already used (reject). */\n hasSeen(nonce: string): Promise<boolean>\n /** Mark nonce as used. TTL = how long to remember it (seconds). */\n markSeen(nonce: string, ttlSeconds: number): Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// In-memory implementation — single-worker, ephemeral\n// ---------------------------------------------------------------------------\n\n/** In-memory nonce store with automatic eviction. Use in tests or single-worker deploys. */\nexport class MemoryNonceStore implements NonceStore {\n private seen = new Map<string, number>() // nonce → expiresAt\n private lastEviction = Date.now()\n\n async hasSeen(nonce: string): Promise<boolean> {\n this.evictExpired()\n const expiresAt = this.seen.get(nonce)\n if (!expiresAt) return false\n if (expiresAt < Date.now()) {\n this.seen.delete(nonce)\n return false\n }\n return true\n }\n\n async markSeen(nonce: string, ttlSeconds: number): Promise<void> {\n this.seen.set(nonce, Date.now() + ttlSeconds * 1000)\n this.evictExpired()\n }\n\n private evictExpired() {\n const now = Date.now()\n // Evict at most every 60 seconds to avoid O(n) on every request\n if (now - this.lastEviction < 60_000) return\n this.lastEviction = now\n for (const [nonce, expiresAt] of this.seen) {\n if (expiresAt < now) this.seen.delete(nonce)\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Cloudflare KV implementation — multi-worker, distributed\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal KVNamespace shape — matches Cloudflare Workers' @cloudflare/workers-types\n * without pulling that package as a dep. Production consumers cast their KV\n * binding to this interface at the construction site.\n */\nexport interface KVNamespace {\n get(key: string, options?: { type?: 'text' | 'json' }): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/**\n * KV-backed NonceStore for distributed Cloudflare Workers deployments.\n *\n * Why this exists: MemoryNonceStore works on a single worker instance, but\n * Cloudflare routes requests across multiple isolates. Without shared state,\n * an attacker could retry a replayed nonce against a different isolate and\n * have it accepted. This implementation uses Workers KV with native TTL so\n * the nonce automatically expires at payment-expiry time.\n *\n * TTL precision: KV is eventually consistent (propagation ~60s). For x402\n * with 10-minute expiry windows this is fine — by the time KV propagates,\n * the payment itself would be expired anyway.\n *\n * Usage:\n * const nonceStore = new KvNonceStore(env.NONCE_KV, 'x402')\n * createAgentGateway({ ...config, nonceStore })\n */\nexport class KvNonceStore implements NonceStore {\n constructor(\n private readonly kv: KVNamespace,\n /** Key prefix to namespace within a shared KV (default: \"nonce\"). */\n private readonly prefix: string = 'nonce',\n ) {}\n\n async hasSeen(nonce: string): Promise<boolean> {\n const value = await this.kv.get(this.key(nonce))\n return value !== null\n }\n\n async markSeen(nonce: string, ttlSeconds: number): Promise<void> {\n // KV minimum TTL is 60 seconds\n const ttl = Math.max(ttlSeconds, 60)\n await this.kv.put(this.key(nonce), '1', { expirationTtl: ttl })\n }\n\n private key(nonce: string): string {\n return `${this.prefix}:${nonce}`\n }\n}\n"],"mappings":";AAiBO,IAAM,mBAAN,MAA6C;AAAA,EAC1C,OAAO,oBAAI,IAAoB;AAAA;AAAA,EAC/B,eAAe,KAAK,IAAI;AAAA,EAEhC,MAAM,QAAQ,OAAiC;AAC7C,SAAK,aAAa;AAClB,UAAM,YAAY,KAAK,KAAK,IAAI,KAAK;AACrC,QAAI,CAAC,UAAW,QAAO;AACvB,QAAI,YAAY,KAAK,IAAI,GAAG;AAC1B,WAAK,KAAK,OAAO,KAAK;AACtB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAS,OAAe,YAAmC;AAC/D,SAAK,KAAK,IAAI,OAAO,KAAK,IAAI,IAAI,aAAa,GAAI;AACnD,SAAK,aAAa;AAAA,EACpB;AAAA,EAEQ,eAAe;AACrB,UAAM,MAAM,KAAK,IAAI;AAErB,QAAI,MAAM,KAAK,eAAe,IAAQ;AACtC,SAAK,eAAe;AACpB,eAAW,CAAC,OAAO,SAAS,KAAK,KAAK,MAAM;AAC1C,UAAI,YAAY,IAAK,MAAK,KAAK,OAAO,KAAK;AAAA,IAC7C;AAAA,EACF;AACF;AAkCO,IAAM,eAAN,MAAyC;AAAA,EAC9C,YACmB,IAEA,SAAiB,SAClC;AAHiB;AAEA;AAAA,EAChB;AAAA,EAHgB;AAAA,EAEA;AAAA,EAGnB,MAAM,QAAQ,OAAiC;AAC7C,UAAM,QAAQ,MAAM,KAAK,GAAG,IAAI,KAAK,IAAI,KAAK,CAAC;AAC/C,WAAO,UAAU;AAAA,EACnB;AAAA,EAEA,MAAM,SAAS,OAAe,YAAmC;AAE/D,UAAM,MAAM,KAAK,IAAI,YAAY,EAAE;AACnC,UAAM,KAAK,GAAG,IAAI,KAAK,IAAI,KAAK,GAAG,KAAK,EAAE,eAAe,IAAI,CAAC;AAAA,EAChE;AAAA,EAEQ,IAAI,OAAuB;AACjC,WAAO,GAAG,KAAK,MAAM,IAAI,KAAK;AAAA,EAChC;AACF;","names":[]}
|