@tangle-network/agent-gateway 0.2.0 → 0.3.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/dist/{chunk-EMGS63QE.js → chunk-FI5HMLGY.js} +141 -20
- package/dist/chunk-FI5HMLGY.js.map +1 -0
- package/dist/{chunk-Z22ALGHW.js → chunk-M7ZJAK4K.js} +22 -2
- package/dist/chunk-M7ZJAK4K.js.map +1 -0
- package/dist/{chunk-4FULF5LW.js → chunk-XCTXHZ76.js} +27 -1
- package/dist/chunk-XCTXHZ76.js.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +13 -3
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.js +3 -3
- package/dist/nonce-store.d.ts +44 -2
- package/dist/nonce-store.js +3 -1
- package/dist/rate-limit.d.ts +35 -3
- package/dist/rate-limit.js +3 -1
- package/dist/types-14xV8J4G.d.ts +292 -0
- package/dist/types.d.ts +3 -157
- package/package.json +5 -1
- package/src/filter.ts +25 -6
- package/src/index.ts +10 -0
- package/src/middleware.ts +50 -17
- package/src/nonce-store.ts +60 -1
- package/src/observer.ts +181 -0
- package/src/rate-limit.ts +63 -2
- package/src/types.ts +8 -0
- package/dist/chunk-4FULF5LW.js.map +0 -1
- package/dist/chunk-EMGS63QE.js.map +0 -1
- package/dist/chunk-Z22ALGHW.js.map +0 -1
package/src/middleware.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { verifyX402, verifyMpp, defaultVerifyApiKey } from './verify'
|
|
|
10
10
|
import { filterConsumerMessagesStrict, redactSystemPromptFromOutput } from './filter'
|
|
11
11
|
import { checkRateLimit, MemoryRateLimitStore, type RateLimitStore } from './rate-limit'
|
|
12
12
|
import { MemoryNonceStore } from './nonce-store'
|
|
13
|
+
import { generateRequestId, type GatewayObserver, type RequestContext } from './observer'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Create a Hono router that serves the agent gateway.
|
|
@@ -28,6 +29,7 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
28
29
|
const globalRateLimit = config.rateLimit ?? { limit: 60, windowSeconds: 60 }
|
|
29
30
|
const nonceStore = config.nonceStore ?? new MemoryNonceStore()
|
|
30
31
|
const requiredScope = config.requiredScope ?? 'chat'
|
|
32
|
+
const obs: GatewayObserver | undefined = config.observer
|
|
31
33
|
|
|
32
34
|
// --- Discovery endpoint (no auth) ---
|
|
33
35
|
|
|
@@ -75,6 +77,10 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
75
77
|
gw.post('/:slug/chat/completions', async (c) => {
|
|
76
78
|
const slug = c.req.param('slug')
|
|
77
79
|
const startMs = Date.now()
|
|
80
|
+
const requestId = generateRequestId()
|
|
81
|
+
const ctx: RequestContext = { requestId, agentSlug: slug, startMs }
|
|
82
|
+
|
|
83
|
+
await obs?.onRequestStart?.(ctx)
|
|
78
84
|
|
|
79
85
|
// 1. Resolve agent
|
|
80
86
|
const agent = await config.resolveAgent(slug)
|
|
@@ -85,6 +91,7 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
85
91
|
// 2. Body size limit (before parsing — DoS prevention)
|
|
86
92
|
const contentLength = parseInt(c.req.header('Content-Length') ?? '0', 10)
|
|
87
93
|
if (contentLength > 65536) {
|
|
94
|
+
await obs?.onBodyTooLarge?.(ctx, contentLength)
|
|
88
95
|
return c.json(
|
|
89
96
|
{ error: { message: 'Request body too large (max 64KB)', type: 'invalid_request' } },
|
|
90
97
|
413,
|
|
@@ -111,9 +118,10 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
111
118
|
if (spendAuthHeader) {
|
|
112
119
|
const signer = await verifyX402(spendAuthHeader, config.x402, nonceStore)
|
|
113
120
|
if (!signer) {
|
|
121
|
+
await obs?.onAuthFailure?.(ctx, { method: 'x402', code: 'invalid_spend_auth', httpStatus: 402 })
|
|
114
122
|
return c.json(
|
|
115
123
|
{ error: { message: 'Invalid X-Payment-Signature', type: 'authentication_error', code: 'invalid_spend_auth' } },
|
|
116
|
-
{ status: 402, headers: { 'X-Payment-Required': 'spendauth' } },
|
|
124
|
+
{ status: 402, headers: { 'X-Payment-Required': 'spendauth', 'X-Request-Id': requestId } },
|
|
117
125
|
)
|
|
118
126
|
}
|
|
119
127
|
consumerId = signer
|
|
@@ -123,9 +131,10 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
123
131
|
if (!signer) {
|
|
124
132
|
const realm = config.mpp.realm
|
|
125
133
|
const method = config.mpp.method ?? 'blueprintevm'
|
|
134
|
+
await obs?.onAuthFailure?.(ctx, { method: 'mpp', code: 'invalid_mpp_credential', httpStatus: 401 })
|
|
126
135
|
return c.json(
|
|
127
136
|
{ error: { message: 'Invalid Payment credential', type: 'authentication_error', code: 'invalid_mpp_credential' } },
|
|
128
|
-
{ status: 401, headers: { 'WWW-Authenticate': `Payment realm="${realm}", method="${method}"
|
|
137
|
+
{ status: 401, headers: { 'WWW-Authenticate': `Payment realm="${realm}", method="${method}"`, 'X-Request-Id': requestId } },
|
|
129
138
|
)
|
|
130
139
|
}
|
|
131
140
|
consumerId = signer
|
|
@@ -134,14 +143,19 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
134
143
|
const verify = config.verifyApiKey ?? defaultVerifyApiKey
|
|
135
144
|
const key = await verify(authHeader)
|
|
136
145
|
if (!key) {
|
|
137
|
-
|
|
146
|
+
await obs?.onAuthFailure?.(ctx, { method: 'apikey', code: 'invalid_api_key', httpStatus: 401 })
|
|
147
|
+
return c.json(
|
|
148
|
+
{ error: { message: 'Invalid API key', type: 'authentication_error' } },
|
|
149
|
+
{ status: 401, headers: { 'X-Request-Id': requestId } },
|
|
150
|
+
)
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
// Scope enforcement — API key must include the required scope
|
|
141
154
|
if (key.scopes && key.scopes.length > 0 && !key.scopes.includes(requiredScope)) {
|
|
155
|
+
await obs?.onAuthFailure?.(ctx, { method: 'apikey', code: 'insufficient_scope', httpStatus: 403 })
|
|
142
156
|
return c.json(
|
|
143
157
|
{ error: { message: `API key missing required scope: ${requiredScope}`, type: 'forbidden', code: 'insufficient_scope' } },
|
|
144
|
-
403,
|
|
158
|
+
{ status: 403, headers: { 'X-Request-Id': requestId } },
|
|
145
159
|
)
|
|
146
160
|
}
|
|
147
161
|
|
|
@@ -150,11 +164,15 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
150
164
|
keyInfo = key
|
|
151
165
|
} else {
|
|
152
166
|
// No payment — return 402 with instructions
|
|
167
|
+
await obs?.onAuthFailure?.(ctx, { method: 'none', code: 'payment_required', httpStatus: 402 })
|
|
153
168
|
const methods: string[] = ['x402']
|
|
154
169
|
if (config.mpp) methods.push('mpp')
|
|
155
170
|
methods.push('api_key')
|
|
156
171
|
|
|
157
|
-
const headers: Record<string, string> = {
|
|
172
|
+
const headers: Record<string, string> = {
|
|
173
|
+
'X-Payment-Required': methods.join(', '),
|
|
174
|
+
'X-Request-Id': requestId,
|
|
175
|
+
}
|
|
158
176
|
if (config.mpp) {
|
|
159
177
|
headers['WWW-Authenticate'] = `Payment realm="${config.mpp.realm}", method="${config.mpp.method ?? 'blueprintevm'}"`
|
|
160
178
|
}
|
|
@@ -180,6 +198,8 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
180
198
|
}, { status: 402, headers })
|
|
181
199
|
}
|
|
182
200
|
|
|
201
|
+
await obs?.onPaymentVerified?.(ctx, { method: paymentMethod, consumerId: consumerId!, keyId: keyInfo?.keyId })
|
|
202
|
+
|
|
183
203
|
// 4. Rate limit — per-key override or global
|
|
184
204
|
const effectiveRateLimit = keyInfo?.rateLimitPerMinute
|
|
185
205
|
? { limit: keyInfo.rateLimitPerMinute, windowSeconds: 60 }
|
|
@@ -187,9 +207,10 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
187
207
|
|
|
188
208
|
const rl = await checkRateLimit(consumerId!, effectiveRateLimit, rateLimitStore)
|
|
189
209
|
if (!rl.allowed) {
|
|
210
|
+
await obs?.onRateLimited?.(ctx, { consumerId: consumerId!, retryAfterSeconds: rl.retryAfterSeconds ?? 60 })
|
|
190
211
|
return c.json(
|
|
191
212
|
{ error: { message: 'Rate limit exceeded', type: 'rate_limit_error', retry_after: rl.retryAfterSeconds } },
|
|
192
|
-
{ status: 429, headers: { 'Retry-After': String(rl.retryAfterSeconds ?? 60) } },
|
|
213
|
+
{ status: 429, headers: { 'Retry-After': String(rl.retryAfterSeconds ?? 60), 'X-Request-Id': requestId } },
|
|
193
214
|
)
|
|
194
215
|
}
|
|
195
216
|
|
|
@@ -197,13 +218,16 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
197
218
|
const { messages: filtered, injectionWarnings } = filterConsumerMessagesStrict(body.messages, maxLen)
|
|
198
219
|
|
|
199
220
|
if (injectionWarnings.length > 0) {
|
|
200
|
-
|
|
201
|
-
|
|
221
|
+
await obs?.onInjectionDetected?.(ctx, {
|
|
222
|
+
consumerId: consumerId!,
|
|
223
|
+
patterns: injectionWarnings,
|
|
224
|
+
blocked: !!config.blockInjection,
|
|
225
|
+
})
|
|
202
226
|
|
|
203
227
|
if (config.blockInjection) {
|
|
204
228
|
return c.json(
|
|
205
229
|
{ error: { message: 'Request rejected: potential prompt injection detected', type: 'content_policy_violation' } },
|
|
206
|
-
400,
|
|
230
|
+
{ status: 400, headers: { 'X-Request-Id': requestId } },
|
|
207
231
|
)
|
|
208
232
|
}
|
|
209
233
|
// In non-blocking mode, continue but the warning is logged for auditing
|
|
@@ -219,7 +243,7 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
219
243
|
}
|
|
220
244
|
|
|
221
245
|
// 6. Get sandbox and stream response with output filtering
|
|
222
|
-
|
|
246
|
+
const inputTokens = Math.ceil(userMessage.length / 4)
|
|
223
247
|
let outputTokens = 0
|
|
224
248
|
|
|
225
249
|
const stream = new ReadableStream({
|
|
@@ -272,7 +296,7 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
272
296
|
const ownerEarned = totalCost * (1 - agent.platformFeePercent)
|
|
273
297
|
const platformFee = totalCost * agent.platformFeePercent
|
|
274
298
|
|
|
275
|
-
|
|
299
|
+
const usageEvent = {
|
|
276
300
|
agentId: agent.id,
|
|
277
301
|
agentSlug: agent.slug,
|
|
278
302
|
consumerId: consumerId!,
|
|
@@ -283,18 +307,26 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
283
307
|
ownerEarnedUsd: ownerEarned,
|
|
284
308
|
platformFeeUsd: platformFee,
|
|
285
309
|
durationMs: Date.now() - startMs,
|
|
286
|
-
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await config.recordUsage(usageEvent)
|
|
313
|
+
await obs?.onRequestComplete?.(ctx, usageEvent)
|
|
287
314
|
|
|
288
315
|
if (config.settlePayment) {
|
|
289
|
-
await config.settlePayment({ method: paymentMethod, consumerId: consumerId! }, totalCost).catch(err => {
|
|
290
|
-
|
|
316
|
+
await config.settlePayment({ method: paymentMethod, consumerId: consumerId! }, totalCost).catch(async err => {
|
|
317
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
318
|
+
console.error(`[agent-gateway] settlement failed for ${consumerId}: ${msg}`)
|
|
319
|
+
await obs?.onSettlementError?.(ctx, { consumerId: consumerId!, method: paymentMethod, errorMessage: msg })
|
|
291
320
|
})
|
|
292
321
|
}
|
|
293
322
|
} catch (err) {
|
|
294
323
|
// Sanitize error — never expose stack traces or internal paths
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
324
|
+
const rawMessage = err instanceof Error ? err.message : String(err)
|
|
325
|
+
const safeMessage =
|
|
326
|
+
rawMessage.includes('/') || rawMessage.includes('\\')
|
|
327
|
+
? 'Internal agent error'
|
|
328
|
+
: rawMessage
|
|
329
|
+
await obs?.onStreamError?.(ctx, { consumerId: consumerId!, errorMessage: rawMessage })
|
|
298
330
|
controller.enqueue(
|
|
299
331
|
encoder.encode(`data: ${JSON.stringify({ error: { message: safeMessage, type: 'server_error' } })}\n\n`),
|
|
300
332
|
)
|
|
@@ -308,6 +340,7 @@ export function createAgentGateway(config: GatewayConfig) {
|
|
|
308
340
|
headers: {
|
|
309
341
|
'Content-Type': 'text/event-stream',
|
|
310
342
|
'Cache-Control': 'no-cache',
|
|
343
|
+
'X-Request-Id': requestId,
|
|
311
344
|
'X-Agent-Slug': agent.slug,
|
|
312
345
|
'X-Agent-Hosting': agent.sandboxEndpoint ? 'sovereign' : 'centralized',
|
|
313
346
|
'X-Payment-Method': paymentMethod,
|
package/src/nonce-store.ts
CHANGED
|
@@ -10,7 +10,11 @@ export interface NonceStore {
|
|
|
10
10
|
markSeen(nonce: string, ttlSeconds: number): Promise<void>
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// In-memory implementation — single-worker, ephemeral
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** In-memory nonce store with automatic eviction. Use in tests or single-worker deploys. */
|
|
14
18
|
export class MemoryNonceStore implements NonceStore {
|
|
15
19
|
private seen = new Map<string, number>() // nonce → expiresAt
|
|
16
20
|
private lastEviction = Date.now()
|
|
@@ -41,3 +45,58 @@ export class MemoryNonceStore implements NonceStore {
|
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Cloudflare KV implementation — multi-worker, distributed
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Minimal KVNamespace shape — matches Cloudflare Workers' @cloudflare/workers-types
|
|
55
|
+
* without pulling that package as a dep. Production consumers cast their KV
|
|
56
|
+
* binding to this interface at the construction site.
|
|
57
|
+
*/
|
|
58
|
+
export interface KVNamespace {
|
|
59
|
+
get(key: string, options?: { type?: 'text' | 'json' }): Promise<string | null>
|
|
60
|
+
put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>
|
|
61
|
+
delete(key: string): Promise<void>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* KV-backed NonceStore for distributed Cloudflare Workers deployments.
|
|
66
|
+
*
|
|
67
|
+
* Why this exists: MemoryNonceStore works on a single worker instance, but
|
|
68
|
+
* Cloudflare routes requests across multiple isolates. Without shared state,
|
|
69
|
+
* an attacker could retry a replayed nonce against a different isolate and
|
|
70
|
+
* have it accepted. This implementation uses Workers KV with native TTL so
|
|
71
|
+
* the nonce automatically expires at payment-expiry time.
|
|
72
|
+
*
|
|
73
|
+
* TTL precision: KV is eventually consistent (propagation ~60s). For x402
|
|
74
|
+
* with 10-minute expiry windows this is fine — by the time KV propagates,
|
|
75
|
+
* the payment itself would be expired anyway.
|
|
76
|
+
*
|
|
77
|
+
* Usage:
|
|
78
|
+
* const nonceStore = new KvNonceStore(env.NONCE_KV, 'x402')
|
|
79
|
+
* createAgentGateway({ ...config, nonceStore })
|
|
80
|
+
*/
|
|
81
|
+
export class KvNonceStore implements NonceStore {
|
|
82
|
+
constructor(
|
|
83
|
+
private readonly kv: KVNamespace,
|
|
84
|
+
/** Key prefix to namespace within a shared KV (default: "nonce"). */
|
|
85
|
+
private readonly prefix: string = 'nonce',
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
async hasSeen(nonce: string): Promise<boolean> {
|
|
89
|
+
const value = await this.kv.get(this.key(nonce))
|
|
90
|
+
return value !== null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async markSeen(nonce: string, ttlSeconds: number): Promise<void> {
|
|
94
|
+
// KV minimum TTL is 60 seconds
|
|
95
|
+
const ttl = Math.max(ttlSeconds, 60)
|
|
96
|
+
await this.kv.put(this.key(nonce), '1', { expirationTtl: ttl })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private key(nonce: string): string {
|
|
100
|
+
return `${this.prefix}:${nonce}`
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/observer.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability hook surface.
|
|
3
|
+
*
|
|
4
|
+
* Consumers implement GatewayObserver to wire the gateway into their existing
|
|
5
|
+
* telemetry stack (Langfuse, OTEL, structured logs, Prometheus, etc.) without
|
|
6
|
+
* the gateway itself depending on any of those libraries.
|
|
7
|
+
*
|
|
8
|
+
* Every event carries a requestId so downstream metrics can correlate the
|
|
9
|
+
* payment verification, sandbox execution, and settlement for one request.
|
|
10
|
+
* When no observer is configured, the gateway stays silent.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PaymentMethod, GatewayUsageEvent } from './types'
|
|
14
|
+
|
|
15
|
+
export interface RequestContext {
|
|
16
|
+
requestId: string
|
|
17
|
+
agentSlug: string
|
|
18
|
+
startMs: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthFailureReason {
|
|
22
|
+
method: 'x402' | 'mpp' | 'apikey' | 'none'
|
|
23
|
+
code: string
|
|
24
|
+
httpStatus: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GatewayObserver {
|
|
28
|
+
/** Called at the start of every chat completions POST. */
|
|
29
|
+
onRequestStart?: (ctx: RequestContext) => void | Promise<void>
|
|
30
|
+
|
|
31
|
+
/** Called when a payment method has been successfully verified. */
|
|
32
|
+
onPaymentVerified?: (ctx: RequestContext, info: {
|
|
33
|
+
method: PaymentMethod
|
|
34
|
+
consumerId: string
|
|
35
|
+
keyId?: string
|
|
36
|
+
}) => void | Promise<void>
|
|
37
|
+
|
|
38
|
+
/** Called when auth fails — every branch. */
|
|
39
|
+
onAuthFailure?: (ctx: RequestContext, reason: AuthFailureReason) => void | Promise<void>
|
|
40
|
+
|
|
41
|
+
/** Called when a consumer hits the rate limit. */
|
|
42
|
+
onRateLimited?: (ctx: RequestContext, info: {
|
|
43
|
+
consumerId: string
|
|
44
|
+
retryAfterSeconds: number
|
|
45
|
+
}) => void | Promise<void>
|
|
46
|
+
|
|
47
|
+
/** Called when the request body exceeds the 64KB limit. */
|
|
48
|
+
onBodyTooLarge?: (ctx: RequestContext, contentLength: number) => void | Promise<void>
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Called when prompt-injection patterns are detected.
|
|
52
|
+
* `blocked` is true when blockInjection config is on and the request was
|
|
53
|
+
* rejected; false when the patterns were logged but the request proceeded.
|
|
54
|
+
*/
|
|
55
|
+
onInjectionDetected?: (ctx: RequestContext, info: {
|
|
56
|
+
consumerId: string
|
|
57
|
+
patterns: string[]
|
|
58
|
+
blocked: boolean
|
|
59
|
+
}) => void | Promise<void>
|
|
60
|
+
|
|
61
|
+
/** Called after a successful stream completes and recordUsage has fired. */
|
|
62
|
+
onRequestComplete?: (ctx: RequestContext, usage: GatewayUsageEvent) => void | Promise<void>
|
|
63
|
+
|
|
64
|
+
/** Called when the sandbox throws. The error message is pre-scrubbed. */
|
|
65
|
+
onStreamError?: (ctx: RequestContext, info: {
|
|
66
|
+
consumerId: string
|
|
67
|
+
errorMessage: string
|
|
68
|
+
}) => void | Promise<void>
|
|
69
|
+
|
|
70
|
+
/** Called when settlement fails. Payment already occurred; this is async bookkeeping. */
|
|
71
|
+
onSettlementError?: (ctx: RequestContext, info: {
|
|
72
|
+
consumerId: string
|
|
73
|
+
method: PaymentMethod
|
|
74
|
+
errorMessage: string
|
|
75
|
+
}) => void | Promise<void>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Convenience implementations
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Structured-log observer. Emits one JSON line per event on the `log` function.
|
|
84
|
+
* Default sink: console.log. Production consumers usually pipe their own
|
|
85
|
+
* structured logger (pino, winston, the cf Logs binding).
|
|
86
|
+
*
|
|
87
|
+
* Usage:
|
|
88
|
+
* new ConsoleObserver(({ level, event, ...rest }) => logger.info({ event, ...rest }))
|
|
89
|
+
*/
|
|
90
|
+
export class ConsoleObserver implements GatewayObserver {
|
|
91
|
+
constructor(
|
|
92
|
+
private readonly log: (entry: Record<string, unknown>) => void = (e) => console.log(JSON.stringify(e)),
|
|
93
|
+
) {}
|
|
94
|
+
|
|
95
|
+
private emit(level: 'info' | 'warn' | 'error', event: string, ctx: RequestContext, rest: Record<string, unknown> = {}) {
|
|
96
|
+
this.log({
|
|
97
|
+
level,
|
|
98
|
+
event,
|
|
99
|
+
time: new Date().toISOString(),
|
|
100
|
+
requestId: ctx.requestId,
|
|
101
|
+
agentSlug: ctx.agentSlug,
|
|
102
|
+
durationMs: Date.now() - ctx.startMs,
|
|
103
|
+
...rest,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onRequestStart(ctx: RequestContext) { this.emit('info', 'gateway.request.start', ctx) }
|
|
108
|
+
onPaymentVerified(ctx: RequestContext, info: { method: PaymentMethod; consumerId: string; keyId?: string }) {
|
|
109
|
+
this.emit('info', 'gateway.payment.verified', ctx, info)
|
|
110
|
+
}
|
|
111
|
+
onAuthFailure(ctx: RequestContext, reason: AuthFailureReason) {
|
|
112
|
+
this.emit('warn', 'gateway.auth.failure', ctx, reason as unknown as Record<string, unknown>)
|
|
113
|
+
}
|
|
114
|
+
onRateLimited(ctx: RequestContext, info: { consumerId: string; retryAfterSeconds: number }) {
|
|
115
|
+
this.emit('warn', 'gateway.rate_limit', ctx, info)
|
|
116
|
+
}
|
|
117
|
+
onBodyTooLarge(ctx: RequestContext, contentLength: number) {
|
|
118
|
+
this.emit('warn', 'gateway.body_too_large', ctx, { contentLength })
|
|
119
|
+
}
|
|
120
|
+
onInjectionDetected(ctx: RequestContext, info: { consumerId: string; patterns: string[]; blocked: boolean }) {
|
|
121
|
+
this.emit('warn', 'gateway.injection', ctx, info)
|
|
122
|
+
}
|
|
123
|
+
onRequestComplete(ctx: RequestContext, usage: GatewayUsageEvent) {
|
|
124
|
+
this.emit('info', 'gateway.request.complete', ctx, usage as unknown as Record<string, unknown>)
|
|
125
|
+
}
|
|
126
|
+
onStreamError(ctx: RequestContext, info: { consumerId: string; errorMessage: string }) {
|
|
127
|
+
this.emit('error', 'gateway.stream.error', ctx, info)
|
|
128
|
+
}
|
|
129
|
+
onSettlementError(ctx: RequestContext, info: { consumerId: string; method: PaymentMethod; errorMessage: string }) {
|
|
130
|
+
this.emit('error', 'gateway.settlement.error', ctx, info)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Compose multiple observers into one. Errors in any individual observer
|
|
136
|
+
* don't break the others (fire-and-forget telemetry).
|
|
137
|
+
*/
|
|
138
|
+
export class CompositeObserver implements GatewayObserver {
|
|
139
|
+
constructor(private readonly observers: GatewayObserver[]) {}
|
|
140
|
+
|
|
141
|
+
private async fanOut<K extends keyof GatewayObserver>(event: K, ...args: unknown[]): Promise<void> {
|
|
142
|
+
for (const obs of this.observers) {
|
|
143
|
+
const fn = obs[event] as ((...a: unknown[]) => void | Promise<void>) | undefined
|
|
144
|
+
if (!fn) continue
|
|
145
|
+
try {
|
|
146
|
+
await fn.apply(obs, args)
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.warn(`[agent-gateway] observer ${event} threw:`, err instanceof Error ? err.message : err)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
onRequestStart = (ctx: RequestContext) => this.fanOut('onRequestStart', ctx)
|
|
154
|
+
onPaymentVerified = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onPaymentVerified']>[1]) =>
|
|
155
|
+
this.fanOut('onPaymentVerified', ctx, info)
|
|
156
|
+
onAuthFailure = (ctx: RequestContext, reason: AuthFailureReason) =>
|
|
157
|
+
this.fanOut('onAuthFailure', ctx, reason)
|
|
158
|
+
onRateLimited = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onRateLimited']>[1]) =>
|
|
159
|
+
this.fanOut('onRateLimited', ctx, info)
|
|
160
|
+
onBodyTooLarge = (ctx: RequestContext, contentLength: number) =>
|
|
161
|
+
this.fanOut('onBodyTooLarge', ctx, contentLength)
|
|
162
|
+
onInjectionDetected = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onInjectionDetected']>[1]) =>
|
|
163
|
+
this.fanOut('onInjectionDetected', ctx, info)
|
|
164
|
+
onRequestComplete = (ctx: RequestContext, usage: GatewayUsageEvent) =>
|
|
165
|
+
this.fanOut('onRequestComplete', ctx, usage)
|
|
166
|
+
onStreamError = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onStreamError']>[1]) =>
|
|
167
|
+
this.fanOut('onStreamError', ctx, info)
|
|
168
|
+
onSettlementError = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onSettlementError']>[1]) =>
|
|
169
|
+
this.fanOut('onSettlementError', ctx, info)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Generate a request-id. Crypto-random 16 bytes, hex-encoded with an `req_` prefix.
|
|
174
|
+
* Works in Workers, Node, and browsers — all have globalThis.crypto.
|
|
175
|
+
*/
|
|
176
|
+
export function generateRequestId(): string {
|
|
177
|
+
const bytes = new Uint8Array(16)
|
|
178
|
+
globalThis.crypto.getRandomValues(bytes)
|
|
179
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
180
|
+
return `req_${hex}`
|
|
181
|
+
}
|
package/src/rate-limit.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sliding
|
|
3
|
-
*
|
|
2
|
+
* Sliding-window rate limiter.
|
|
3
|
+
*
|
|
4
|
+
* Two implementations:
|
|
5
|
+
* - MemoryRateLimitStore — single-worker, ephemeral, good for tests
|
|
6
|
+
* - KvRateLimitStore — Cloudflare Workers KV, distributed
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
export interface RateLimitConfig {
|
|
@@ -24,6 +27,10 @@ export interface RateLimitStore {
|
|
|
24
27
|
set(key: string, timestamps: number[], ttlSeconds: number): Promise<void>
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// In-memory implementation
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
27
34
|
/** In-memory rate limit store with periodic eviction */
|
|
28
35
|
export class MemoryRateLimitStore implements RateLimitStore {
|
|
29
36
|
private store = new Map<string, { timestamps: number[]; expiresAt: number }>()
|
|
@@ -53,6 +60,60 @@ export class MemoryRateLimitStore implements RateLimitStore {
|
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Cloudflare KV implementation
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/** Minimal KV shape — see nonce-store.ts for rationale. */
|
|
68
|
+
export interface KVNamespace {
|
|
69
|
+
get(key: string, options?: { type?: 'text' | 'json' }): Promise<string | null>
|
|
70
|
+
put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>
|
|
71
|
+
delete(key: string): Promise<void>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* KV-backed RateLimitStore for distributed Cloudflare Workers deployments.
|
|
76
|
+
*
|
|
77
|
+
* Stores timestamp arrays per consumer. Reads are O(1); writes replace the
|
|
78
|
+
* full array (cap is already filtered by checkRateLimit before write).
|
|
79
|
+
*
|
|
80
|
+
* Consistency note: Workers KV is eventually consistent within ~60s. An
|
|
81
|
+
* attacker sitting on two isolates could technically exceed the limit by
|
|
82
|
+
* ~2x for that window. For payment rate limits this is acceptable; for
|
|
83
|
+
* abuse prevention on free endpoints consider Durable Objects instead.
|
|
84
|
+
*/
|
|
85
|
+
export class KvRateLimitStore implements RateLimitStore {
|
|
86
|
+
constructor(
|
|
87
|
+
private readonly kv: KVNamespace,
|
|
88
|
+
private readonly prefix: string = 'rl',
|
|
89
|
+
) {}
|
|
90
|
+
|
|
91
|
+
async get(key: string): Promise<number[]> {
|
|
92
|
+
const raw = await this.kv.get(this.key(key))
|
|
93
|
+
if (!raw) return []
|
|
94
|
+
try {
|
|
95
|
+
const arr = JSON.parse(raw) as unknown
|
|
96
|
+
return Array.isArray(arr) ? (arr as number[]).filter((t) => typeof t === 'number') : []
|
|
97
|
+
} catch {
|
|
98
|
+
return []
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async set(key: string, timestamps: number[], ttlSeconds: number): Promise<void> {
|
|
103
|
+
// KV minimum TTL is 60 seconds
|
|
104
|
+
const ttl = Math.max(ttlSeconds, 60)
|
|
105
|
+
await this.kv.put(this.key(key), JSON.stringify(timestamps), { expirationTtl: ttl })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private key(key: string): string {
|
|
109
|
+
return `${this.prefix}:${key}`
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Core limiter
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
56
117
|
export async function checkRateLimit(
|
|
57
118
|
consumerId: string,
|
|
58
119
|
config: RateLimitConfig,
|
package/src/types.ts
CHANGED
|
@@ -147,6 +147,14 @@ export interface GatewayConfig {
|
|
|
147
147
|
|
|
148
148
|
/** Nonce replay protection store (default: in-memory). Rejects reused x402 nonces. */
|
|
149
149
|
nonceStore?: import('./nonce-store').NonceStore
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Observability hook. When set, the gateway emits typed events for request
|
|
153
|
+
* lifecycle, auth outcomes, rate limits, injection detection, usage, errors,
|
|
154
|
+
* and settlement failures. See ./observer.ts for the interface and
|
|
155
|
+
* ConsoleObserver / CompositeObserver implementations.
|
|
156
|
+
*/
|
|
157
|
+
observer?: import('./observer').GatewayObserver
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
// --- Chat completion types (OpenAI-compatible) ---
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rate-limit.ts"],"sourcesContent":["/**\n * Sliding window rate limiter.\n * In-memory by default. Override with KV-backed store for Workers.\n */\n\nexport interface RateLimitConfig {\n /** Max requests per window (default: 60) */\n limit: number\n /** Window size in seconds (default: 60) */\n windowSeconds: number\n}\n\nexport interface RateLimitResult {\n allowed: boolean\n remaining: number\n resetAt: number\n retryAfterSeconds?: number\n}\n\nexport interface RateLimitStore {\n /** Get timestamps of recent requests for this key */\n get(key: string): Promise<number[]>\n /** Set timestamps for this key (with TTL) */\n set(key: string, timestamps: number[], ttlSeconds: number): Promise<void>\n}\n\n/** In-memory rate limit store with periodic eviction */\nexport class MemoryRateLimitStore implements RateLimitStore {\n private store = new Map<string, { timestamps: number[]; expiresAt: number }>()\n private lastEviction = Date.now()\n\n async get(key: string): Promise<number[]> {\n this.evictExpired()\n const entry = this.store.get(key)\n if (!entry || entry.expiresAt < Date.now()) {\n this.store.delete(key)\n return []\n }\n return entry.timestamps\n }\n\n async set(key: string, timestamps: number[], ttlSeconds: number): Promise<void> {\n this.store.set(key, { timestamps, expiresAt: Date.now() + ttlSeconds * 1000 })\n }\n\n private evictExpired() {\n const now = Date.now()\n if (now - this.lastEviction < 30_000) return\n this.lastEviction = now\n for (const [key, entry] of this.store) {\n if (entry.expiresAt < now) this.store.delete(key)\n }\n }\n}\n\nexport async function checkRateLimit(\n consumerId: string,\n config: RateLimitConfig,\n store: RateLimitStore,\n): Promise<RateLimitResult> {\n const now = Date.now()\n const windowMs = config.windowSeconds * 1000\n const cutoff = now - windowMs\n\n const key = `rl:${consumerId}`\n const timestamps = (await store.get(key)).filter(t => t > cutoff)\n\n if (timestamps.length >= config.limit) {\n const oldestInWindow = Math.min(...timestamps)\n const resetAt = oldestInWindow + windowMs\n return {\n allowed: false,\n remaining: 0,\n resetAt,\n retryAfterSeconds: Math.ceil((resetAt - now) / 1000),\n }\n }\n\n timestamps.push(now)\n await store.set(key, timestamps, config.windowSeconds * 2)\n\n return {\n allowed: true,\n remaining: config.limit - timestamps.length,\n resetAt: now + windowMs,\n }\n}\n"],"mappings":";AA2BO,IAAM,uBAAN,MAAqD;AAAA,EAClD,QAAQ,oBAAI,IAAyD;AAAA,EACrE,eAAe,KAAK,IAAI;AAAA,EAEhC,MAAM,IAAI,KAAgC;AACxC,SAAK,aAAa;AAClB,UAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;AAChC,QAAI,CAAC,SAAS,MAAM,YAAY,KAAK,IAAI,GAAG;AAC1C,WAAK,MAAM,OAAO,GAAG;AACrB,aAAO,CAAC;AAAA,IACV;AACA,WAAO,MAAM;AAAA,EACf;AAAA,EAEA,MAAM,IAAI,KAAa,YAAsB,YAAmC;AAC9E,SAAK,MAAM,IAAI,KAAK,EAAE,YAAY,WAAW,KAAK,IAAI,IAAI,aAAa,IAAK,CAAC;AAAA,EAC/E;AAAA,EAEQ,eAAe;AACrB,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,MAAM,KAAK,eAAe,IAAQ;AACtC,SAAK,eAAe;AACpB,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,OAAO;AACrC,UAAI,MAAM,YAAY,IAAK,MAAK,MAAM,OAAO,GAAG;AAAA,IAClD;AAAA,EACF;AACF;AAEA,eAAsB,eACpB,YACA,QACA,OAC0B;AAC1B,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,WAAW,OAAO,gBAAgB;AACxC,QAAM,SAAS,MAAM;AAErB,QAAM,MAAM,MAAM,UAAU;AAC5B,QAAM,cAAc,MAAM,MAAM,IAAI,GAAG,GAAG,OAAO,OAAK,IAAI,MAAM;AAEhE,MAAI,WAAW,UAAU,OAAO,OAAO;AACrC,UAAM,iBAAiB,KAAK,IAAI,GAAG,UAAU;AAC7C,UAAM,UAAU,iBAAiB;AACjC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,WAAW;AAAA,MACX;AAAA,MACA,mBAAmB,KAAK,MAAM,UAAU,OAAO,GAAI;AAAA,IACrD;AAAA,EACF;AAEA,aAAW,KAAK,GAAG;AACnB,QAAM,MAAM,IAAI,KAAK,YAAY,OAAO,gBAAgB,CAAC;AAEzD,SAAO;AAAA,IACL,SAAS;AAAA,IACT,WAAW,OAAO,QAAQ,WAAW;AAAA,IACrC,SAAS,MAAM;AAAA,EACjB;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware.ts","../src/verify.ts","../src/filter.ts"],"sourcesContent":["import { Hono } from 'hono'\nimport type {\n GatewayConfig,\n ChatCompletionRequest,\n ChatCompletionChunk,\n PaymentMethod,\n ApiKeyInfo,\n} from './types'\nimport { verifyX402, verifyMpp, defaultVerifyApiKey } from './verify'\nimport { filterConsumerMessagesStrict, redactSystemPromptFromOutput } from './filter'\nimport { checkRateLimit, MemoryRateLimitStore, type RateLimitStore } from './rate-limit'\nimport { MemoryNonceStore } from './nonce-store'\n\n/**\n * Create a Hono router that serves the agent gateway.\n *\n * Mount at any path:\n * app.route('/v1/agents', createAgentGateway(config))\n *\n * Exposes:\n * GET /:slug/chat/completions — agent discovery metadata\n * POST /:slug/chat/completions — OpenAI-compatible chat endpoint (paid)\n */\nexport function createAgentGateway(config: GatewayConfig) {\n const gw = new Hono()\n const maxLen = config.maxMessageLength ?? 8000\n const rateLimitStore: RateLimitStore = config.rateLimitStore ?? new MemoryRateLimitStore()\n const globalRateLimit = config.rateLimit ?? { limit: 60, windowSeconds: 60 }\n const nonceStore = config.nonceStore ?? new MemoryNonceStore()\n const requiredScope = config.requiredScope ?? 'chat'\n\n // --- Discovery endpoint (no auth) ---\n\n gw.get('/:slug/chat/completions', async (c) => {\n const slug = c.req.param('slug')\n const agent = await config.resolveAgent(slug)\n if (!agent) return c.json({ error: 'Agent not found or not published' }, 404)\n\n const paymentMethods: Array<Record<string, unknown>> = [\n {\n type: 'x402',\n operator: config.x402.operatorAddress,\n chain_id: config.x402.chainId,\n credits_contract: config.x402.creditsAddress,\n },\n ]\n if (config.mpp) {\n paymentMethods.push({\n type: 'mpp',\n realm: config.mpp.realm,\n method: config.mpp.method ?? 'blueprintevm',\n })\n }\n paymentMethods.push({ type: 'api_key', prefix: 'sk_agent_' })\n\n return c.json({\n slug: agent.slug,\n pricing: {\n per_token_usd: agent.pricePerTokenUsd,\n currency: 'USD',\n platform_fee_percent: agent.platformFeePercent,\n },\n hosting: {\n mode: agent.sandboxEndpoint ? 'sovereign' : 'centralized',\n endpoint: agent.sandboxEndpoint ?? config.baseUrl ?? 'tangle.tools',\n },\n payment_methods: paymentMethods,\n capabilities: ['chat.completions', 'streaming'],\n openai_compatible: true,\n })\n })\n\n // --- Chat completions endpoint (paid) ---\n\n gw.post('/:slug/chat/completions', async (c) => {\n const slug = c.req.param('slug')\n const startMs = Date.now()\n\n // 1. Resolve agent\n const agent = await config.resolveAgent(slug)\n if (!agent) {\n return c.json({ error: { message: 'Agent not found', type: 'not_found' } }, 404)\n }\n\n // 2. Body size limit (before parsing — DoS prevention)\n const contentLength = parseInt(c.req.header('Content-Length') ?? '0', 10)\n if (contentLength > 65536) {\n return c.json(\n { error: { message: 'Request body too large (max 64KB)', type: 'invalid_request' } },\n 413,\n )\n }\n\n let body: ChatCompletionRequest\n try {\n body = await c.req.json()\n } catch {\n return c.json({ error: { message: 'Invalid JSON', type: 'invalid_request' } }, 400)\n }\n if (!body.messages?.length) {\n return c.json({ error: { message: 'messages array required', type: 'invalid_request' } }, 400)\n }\n\n // 3. Authenticate — x402 SpendAuth, MPP, or API key\n const spendAuthHeader = c.req.header('X-Payment-Signature')\n const authHeader = c.req.header('Authorization') ?? ''\n let consumerId: string | null = null\n let paymentMethod: PaymentMethod = 'none'\n let keyInfo: ApiKeyInfo | null = null\n\n if (spendAuthHeader) {\n const signer = await verifyX402(spendAuthHeader, config.x402, nonceStore)\n if (!signer) {\n return c.json(\n { error: { message: 'Invalid X-Payment-Signature', type: 'authentication_error', code: 'invalid_spend_auth' } },\n { status: 402, headers: { 'X-Payment-Required': 'spendauth' } },\n )\n }\n consumerId = signer\n paymentMethod = 'x402'\n } else if (config.mpp && authHeader.toLowerCase().startsWith('payment ')) {\n const signer = await verifyMpp(authHeader, config.mpp, config.x402)\n if (!signer) {\n const realm = config.mpp.realm\n const method = config.mpp.method ?? 'blueprintevm'\n return c.json(\n { error: { message: 'Invalid Payment credential', type: 'authentication_error', code: 'invalid_mpp_credential' } },\n { status: 401, headers: { 'WWW-Authenticate': `Payment realm=\"${realm}\", method=\"${method}\"` } },\n )\n }\n consumerId = signer\n paymentMethod = 'mpp'\n } else if (authHeader.startsWith('Bearer ')) {\n const verify = config.verifyApiKey ?? defaultVerifyApiKey\n const key = await verify(authHeader)\n if (!key) {\n return c.json({ error: { message: 'Invalid API key', type: 'authentication_error' } }, 401)\n }\n\n // Scope enforcement — API key must include the required scope\n if (key.scopes && key.scopes.length > 0 && !key.scopes.includes(requiredScope)) {\n return c.json(\n { error: { message: `API key missing required scope: ${requiredScope}`, type: 'forbidden', code: 'insufficient_scope' } },\n 403,\n )\n }\n\n consumerId = key.consumerId\n paymentMethod = 'apikey'\n keyInfo = key\n } else {\n // No payment — return 402 with instructions\n const methods: string[] = ['x402']\n if (config.mpp) methods.push('mpp')\n methods.push('api_key')\n\n const headers: Record<string, string> = { 'X-Payment-Required': methods.join(', ') }\n if (config.mpp) {\n headers['WWW-Authenticate'] = `Payment realm=\"${config.mpp.realm}\", method=\"${config.mpp.method ?? 'blueprintevm'}\"`\n }\n\n return c.json({\n error: {\n message: 'Payment required',\n type: 'payment_required',\n payment_methods: methods,\n x402: {\n operator: config.x402.operatorAddress,\n chain_id: config.x402.chainId,\n credits_address: config.x402.creditsAddress,\n estimated_amount_per_request: '20000',\n },\n ...(config.mpp ? {\n mpp: { realm: config.mpp.realm, method: config.mpp.method ?? 'blueprintevm' },\n } : {}),\n api_key: {\n purchase_url: config.baseUrl ? `${config.baseUrl}/agents/${slug}/api-keys` : undefined,\n },\n },\n }, { status: 402, headers })\n }\n\n // 4. Rate limit — per-key override or global\n const effectiveRateLimit = keyInfo?.rateLimitPerMinute\n ? { limit: keyInfo.rateLimitPerMinute, windowSeconds: 60 }\n : globalRateLimit\n\n const rl = await checkRateLimit(consumerId!, effectiveRateLimit, rateLimitStore)\n if (!rl.allowed) {\n return c.json(\n { error: { message: 'Rate limit exceeded', type: 'rate_limit_error', retry_after: rl.retryAfterSeconds } },\n { status: 429, headers: { 'Retry-After': String(rl.retryAfterSeconds ?? 60) } },\n )\n }\n\n // 5. Filter messages — injection detection + sanitization\n const { messages: filtered, injectionWarnings } = filterConsumerMessagesStrict(body.messages, maxLen)\n\n if (injectionWarnings.length > 0) {\n // Log injection attempt\n console.warn(`[agent-gateway] injection detected from ${consumerId}: ${injectionWarnings.join(', ')}`)\n\n if (config.blockInjection) {\n return c.json(\n { error: { message: 'Request rejected: potential prompt injection detected', type: 'content_policy_violation' } },\n 400,\n )\n }\n // In non-blocking mode, continue but the warning is logged for auditing\n }\n\n const userMessage = filtered\n .filter((m) => m.role === 'user')\n .map((m) => m.content)\n .join('\\n\\n')\n\n if (!userMessage) {\n return c.json({ error: { message: 'No user message provided', type: 'invalid_request' } }, 400)\n }\n\n // 6. Get sandbox and stream response with output filtering\n let inputTokens = Math.ceil(userMessage.length / 4)\n let outputTokens = 0\n\n const stream = new ReadableStream({\n async start(controller) {\n const encoder = new TextEncoder()\n const sendChunk = (rawDelta: string) => {\n // Redact system prompt leakage from output\n const delta = redactSystemPromptFromOutput(rawDelta, agent.systemPrompt)\n outputTokens += Math.ceil(delta.length / 4)\n const chunk: ChatCompletionChunk = {\n id: `chatcmpl-${Date.now()}`,\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: agent.slug,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n }\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\\n\\n`))\n }\n\n try {\n const box = await config.getSandbox(agent)\n const promptStream = box.streamPrompt(userMessage, {\n sessionId: `consumer:${consumerId}`,\n systemPrompt: agent.systemPrompt,\n })\n\n for await (const event of promptStream) {\n if (\n event.type === 'message.part.updated' &&\n event.data?.part?.type === 'text' &&\n event.data.delta\n ) {\n sendChunk(event.data.delta)\n }\n }\n\n // Final chunk\n const done: ChatCompletionChunk = {\n id: `chatcmpl-${Date.now()}`,\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: agent.slug,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n }\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(done)}\\n\\n`))\n controller.enqueue(encoder.encode('data: [DONE]\\n\\n'))\n\n // 7. Record usage + settle payment\n const totalCost = (inputTokens + outputTokens) * agent.pricePerTokenUsd\n const ownerEarned = totalCost * (1 - agent.platformFeePercent)\n const platformFee = totalCost * agent.platformFeePercent\n\n await config.recordUsage({\n agentId: agent.id,\n agentSlug: agent.slug,\n consumerId: consumerId!,\n paymentMethod,\n inputTokens,\n outputTokens,\n totalCostUsd: totalCost,\n ownerEarnedUsd: ownerEarned,\n platformFeeUsd: platformFee,\n durationMs: Date.now() - startMs,\n })\n\n if (config.settlePayment) {\n await config.settlePayment({ method: paymentMethod, consumerId: consumerId! }, totalCost).catch(err => {\n console.error(`[agent-gateway] settlement failed for ${consumerId}: ${err instanceof Error ? err.message : err}`)\n })\n }\n } catch (err) {\n // Sanitize error — never expose stack traces or internal paths\n const safeMessage = err instanceof Error\n ? (err.message.includes('/') || err.message.includes('\\\\') ? 'Internal agent error' : err.message)\n : 'Internal agent error'\n controller.enqueue(\n encoder.encode(`data: ${JSON.stringify({ error: { message: safeMessage, type: 'server_error' } })}\\n\\n`),\n )\n } finally {\n controller.close()\n }\n },\n })\n\n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'X-Agent-Slug': agent.slug,\n 'X-Agent-Hosting': agent.sandboxEndpoint ? 'sovereign' : 'centralized',\n 'X-Payment-Method': paymentMethod,\n 'X-Payment-Settled': paymentMethod === 'x402' ? 'pending' : 'true',\n ...(rl.remaining !== undefined ? { 'X-RateLimit-Remaining': String(rl.remaining) } : {}),\n },\n })\n })\n\n return gw\n}\n","import type { X402Config, MppConfig, ApiKeyInfo } from './types'\nimport type { NonceStore } from './nonce-store'\n\n/**\n * Verify x402 SpendAuth signature (EIP-712).\n * Returns the signer address (commitment) if valid, null otherwise.\n *\n * DEMO MODE (demoMode: true): accepts any well-formed header structure.\n * PRODUCTION: requires config.verifySigner callback for on-chain verification.\n */\nexport async function verifyX402(\n spendAuthHeader: string,\n config: X402Config,\n nonceStore?: NonceStore,\n): Promise<string | null> {\n try {\n const raw = JSON.parse(spendAuthHeader)\n if (!raw.commitment || !raw.signature || !raw.amount) return null\n if (raw.operator?.toLowerCase() !== config.operatorAddress.toLowerCase()) return null\n\n const amount = BigInt(raw.amount)\n const nonce = BigInt(raw.nonce)\n const expiry = BigInt(raw.expiry)\n\n // Reject expired payments\n if (expiry < BigInt(Math.floor(Date.now() / 1000))) return null\n\n // Reject zero-amount payments\n if (amount <= 0n) return null\n\n // Reject replayed nonces\n const nonceKey = `${raw.commitment}:${nonce.toString()}`\n if (nonceStore) {\n if (await nonceStore.hasSeen(nonceKey)) return null\n // Mark seen with TTL matching the expiry window (max 1 hour)\n const ttl = Math.min(Number(expiry) - Math.floor(Date.now() / 1000), 3600)\n await nonceStore.markSeen(nonceKey, Math.max(ttl, 60))\n }\n\n // Production: delegate to on-chain verification\n if (config.verifySigner) {\n const verified = await config.verifySigner(raw)\n if (!verified) return null\n } else if (!config.demoMode) {\n console.warn('[agent-gateway] x402 verification running without verifySigner — set demoMode: true to suppress this warning')\n }\n\n return raw.commitment\n } catch {\n return null\n }\n}\n\n/**\n * Verify MPP (Machine Payments Protocol) Authorization: Payment header.\n *\n * MPP uses `Authorization: Payment <method> <credential>` format where\n * the credential is a base64url-encoded JSON wrapping the same EIP-3009\n * payment payload that x402 uses. This means existing x402 wallets work\n * unchanged over the MPP wire format.\n *\n * Returns the signer address if valid, null otherwise.\n * In demo mode, accepts any well-formed Payment header.\n */\nexport async function verifyMpp(\n authHeader: string,\n _config: MppConfig,\n x402Config: X402Config,\n): Promise<string | null> {\n // MPP format: \"Payment <method> <base64url-credential>\"\n const match = authHeader.match(/^Payment\\s+(\\S+)\\s+(\\S+)$/i)\n if (!match) return null\n\n const [, , credentialB64] = match\n\n try {\n // Decode base64url credential → JSON with the same EIP-3009 payload\n const decoded = Buffer.from(credentialB64, 'base64url').toString('utf-8')\n const credential = JSON.parse(decoded)\n\n // The credential payload wraps the same fields x402 uses\n const payload = credential.payload ?? credential\n if (!payload.commitment && !payload.from) return null\n\n // Validate operator match (same as x402)\n const operator = payload.operator ?? payload.to\n if (operator && operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) return null\n\n // Validate bigint fields if present\n if (payload.amount) BigInt(payload.amount)\n if (payload.nonce) BigInt(payload.nonce)\n\n return payload.commitment ?? payload.from ?? null\n } catch {\n return null\n }\n}\n\n/**\n * Default API key verifier — accepts any `sk_agent_*` key (demo mode).\n * Override in GatewayConfig.verifyApiKey for production.\n */\nexport async function defaultVerifyApiKey(\n authHeader: string,\n): Promise<ApiKeyInfo | null> {\n if (!authHeader.startsWith('Bearer sk_agent_')) return null\n const key = authHeader.slice(7)\n return {\n keyId: key.slice(0, 16),\n consumerId: `apikey:${key.slice(0, 16)}`,\n }\n}\n","import type { ChatMessage } from './types'\n\n// --- Injection detection patterns ---\n\nconst INJECTION_PATTERNS = [\n // Direct instruction override\n /ignore\\s+(all\\s+)?(previous|prior|above|earlier)\\s+(instructions?|prompts?|rules?|directives?)/i,\n /disregard\\s+(all\\s+)?(previous|prior|system)/i,\n /forget\\s+(everything|all|your)\\s+(previous|instructions?|training)/i,\n // Role assumption\n /you\\s+are\\s+now\\s+(a|an|the)\\s+/i,\n /pretend\\s+(you\\s+are|to\\s+be)\\s+/i,\n /act\\s+as\\s+(if\\s+you\\s+are|a|an|the)\\s+/i,\n /new\\s+instructions?:/i,\n /\\[system\\]/i,\n /\\[INST\\]/i,\n // Prompt extraction\n /what\\s+(is|are)\\s+your\\s+(system\\s+)?(prompt|instructions?|rules?|directives?)/i,\n /repeat\\s+(your|the)\\s+(system\\s+)?(prompt|instructions?)/i,\n /output\\s+(your|the)\\s+(system\\s+)?(prompt|instructions?)/i,\n /show\\s+me\\s+(your|the)\\s+(system|hidden|secret)\\s+(prompt|instructions?|message)/i,\n // Data exfiltration\n /read\\s+(the\\s+)?(vault|workspace|config|secret|\\.env)/i,\n /cat\\s+\\/home\\/agent\\/(vault|config|\\.env|secrets?)/i,\n /list\\s+(all\\s+)?(vault|workspace|secret)\\s+(files?|contents?|data)/i,\n]\n\n// Unicode normalization — collapse homoglyphs and zero-width chars\nfunction normalizeUnicode(text: string): string {\n return text\n // Remove zero-width chars (ZWJ, ZWNJ, ZWS, ZWSP)\n .replace(/[\\u200B-\\u200F\\u2028-\\u202F\\u2060\\uFEFF]/g, '')\n // Normalize to NFKC (collapses homoglyphs like а→a, е→e)\n .normalize('NFKC')\n}\n\n/**\n * Detect prompt injection attempts.\n * Returns array of matched pattern descriptions, empty if clean.\n */\nexport function detectInjection(content: string): string[] {\n const normalized = normalizeUnicode(content)\n const matches: string[] = []\n\n for (const pattern of INJECTION_PATTERNS) {\n if (pattern.test(normalized)) {\n matches.push(pattern.source.slice(0, 60))\n }\n }\n\n // Check for base64-encoded injection attempts\n const b64Matches = normalized.match(/[A-Za-z0-9+/]{40,}={0,2}/g)\n if (b64Matches) {\n for (const b64 of b64Matches) {\n try {\n const decoded = atob(b64)\n if (INJECTION_PATTERNS.some(p => p.test(decoded))) {\n matches.push('base64-encoded injection')\n }\n } catch { /* not valid b64 */ }\n }\n }\n\n return matches\n}\n\n/**\n * Security boundary — filter consumer messages before forwarding to agent.\n *\n * Defense in depth:\n * 1. Strip system messages (consumers cannot set system prompt)\n * 2. Normalize Unicode (collapse homoglyphs, remove zero-width chars)\n * 3. Detect injection patterns (instruction override, prompt extraction, data exfil)\n * 4. Redact sensitive keywords\n * 5. Cap message length\n *\n * Returns filtered messages and any injection warnings detected.\n */\nexport function filterConsumerMessages(\n messages: ChatMessage[],\n maxLength = 8000,\n): ChatMessage[] {\n return messages\n .filter((m) => m.role !== 'system')\n .map((m) => {\n const normalized = normalizeUnicode(m.content)\n const redacted = normalized\n .replace(/\\b(vault|workspace|owner|admin|secret|\\.env|config\\.json)[\\s/:][^\\s]*/gi, '[REDACTED]')\n .slice(0, maxLength)\n return { role: m.role, content: redacted }\n })\n}\n\n/**\n * Filter consumer messages with injection detection.\n * Returns { messages, injectionWarnings }.\n * If injectionWarnings is non-empty, the gateway should log and optionally reject.\n */\nexport function filterConsumerMessagesStrict(\n messages: ChatMessage[],\n maxLength = 8000,\n): { messages: ChatMessage[]; injectionWarnings: string[] } {\n const filtered = filterConsumerMessages(messages, maxLength)\n const allContent = filtered.map(m => m.content).join(' ')\n const injectionWarnings = detectInjection(allContent)\n return { messages: filtered, injectionWarnings }\n}\n\n/**\n * Redact system prompt content from agent output.\n * Prevents the agent from leaking its own instructions in responses.\n *\n * Strategy: if any chunk of the system prompt appears verbatim (>40 chars)\n * in the output, replace it with [REDACTED].\n */\nexport function redactSystemPromptFromOutput(\n output: string,\n systemPrompt: string | undefined,\n): string {\n if (!systemPrompt || systemPrompt.length < 40) return output\n\n // Split system prompt into meaningful chunks (sentences or lines)\n const chunks = systemPrompt\n .split(/[.\\n]/)\n .map(s => s.trim())\n .filter(s => s.length >= 40)\n\n let redacted = output\n for (const chunk of chunks) {\n // Case-insensitive substring match\n const idx = redacted.toLowerCase().indexOf(chunk.toLowerCase())\n if (idx >= 0) {\n redacted = redacted.slice(0, idx) + '[REDACTED — system instructions]' + redacted.slice(idx + chunk.length)\n }\n }\n\n return redacted\n}\n"],"mappings":";;;;;;;;;AAAA,SAAS,YAAY;;;ACUrB,eAAsB,WACpB,iBACA,QACA,YACwB;AACxB,MAAI;AACF,UAAM,MAAM,KAAK,MAAM,eAAe;AACtC,QAAI,CAAC,IAAI,cAAc,CAAC,IAAI,aAAa,CAAC,IAAI,OAAQ,QAAO;AAC7D,QAAI,IAAI,UAAU,YAAY,MAAM,OAAO,gBAAgB,YAAY,EAAG,QAAO;AAEjF,UAAM,SAAS,OAAO,IAAI,MAAM;AAChC,UAAM,QAAQ,OAAO,IAAI,KAAK;AAC9B,UAAM,SAAS,OAAO,IAAI,MAAM;AAGhC,QAAI,SAAS,OAAO,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,CAAC,EAAG,QAAO;AAG3D,QAAI,UAAU,GAAI,QAAO;AAGzB,UAAM,WAAW,GAAG,IAAI,UAAU,IAAI,MAAM,SAAS,CAAC;AACtD,QAAI,YAAY;AACd,UAAI,MAAM,WAAW,QAAQ,QAAQ,EAAG,QAAO;AAE/C,YAAM,MAAM,KAAK,IAAI,OAAO,MAAM,IAAI,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GAAG,IAAI;AACzE,YAAM,WAAW,SAAS,UAAU,KAAK,IAAI,KAAK,EAAE,CAAC;AAAA,IACvD;AAGA,QAAI,OAAO,cAAc;AACvB,YAAM,WAAW,MAAM,OAAO,aAAa,GAAG;AAC9C,UAAI,CAAC,SAAU,QAAO;AAAA,IACxB,WAAW,CAAC,OAAO,UAAU;AAC3B,cAAQ,KAAK,mHAA8G;AAAA,IAC7H;AAEA,WAAO,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAaA,eAAsB,UACpB,YACA,SACA,YACwB;AAExB,QAAM,QAAQ,WAAW,MAAM,4BAA4B;AAC3D,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,CAAC,EAAE,EAAE,aAAa,IAAI;AAE5B,MAAI;AAEF,UAAM,UAAU,OAAO,KAAK,eAAe,WAAW,EAAE,SAAS,OAAO;AACxE,UAAM,aAAa,KAAK,MAAM,OAAO;AAGrC,UAAM,UAAU,WAAW,WAAW;AACtC,QAAI,CAAC,QAAQ,cAAc,CAAC,QAAQ,KAAM,QAAO;AAGjD,UAAM,WAAW,QAAQ,YAAY,QAAQ;AAC7C,QAAI,YAAY,SAAS,YAAY,MAAM,WAAW,gBAAgB,YAAY,EAAG,QAAO;AAG5F,QAAI,QAAQ,OAAQ,QAAO,QAAQ,MAAM;AACzC,QAAI,QAAQ,MAAO,QAAO,QAAQ,KAAK;AAEvC,WAAO,QAAQ,cAAc,QAAQ,QAAQ;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,oBACpB,YAC4B;AAC5B,MAAI,CAAC,WAAW,WAAW,kBAAkB,EAAG,QAAO;AACvD,QAAM,MAAM,WAAW,MAAM,CAAC;AAC9B,SAAO;AAAA,IACL,OAAO,IAAI,MAAM,GAAG,EAAE;AAAA,IACtB,YAAY,UAAU,IAAI,MAAM,GAAG,EAAE,CAAC;AAAA,EACxC;AACF;;;AC3GA,IAAM,qBAAqB;AAAA;AAAA,EAEzB;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AACF;AAGA,SAAS,iBAAiB,MAAsB;AAC9C,SAAO,KAEJ,QAAQ,6CAA6C,EAAE,EAEvD,UAAU,MAAM;AACrB;AAMO,SAAS,gBAAgB,SAA2B;AACzD,QAAM,aAAa,iBAAiB,OAAO;AAC3C,QAAM,UAAoB,CAAC;AAE3B,aAAW,WAAW,oBAAoB;AACxC,QAAI,QAAQ,KAAK,UAAU,GAAG;AAC5B,cAAQ,KAAK,QAAQ,OAAO,MAAM,GAAG,EAAE,CAAC;AAAA,IAC1C;AAAA,EACF;AAGA,QAAM,aAAa,WAAW,MAAM,2BAA2B;AAC/D,MAAI,YAAY;AACd,eAAW,OAAO,YAAY;AAC5B,UAAI;AACF,cAAM,UAAU,KAAK,GAAG;AACxB,YAAI,mBAAmB,KAAK,OAAK,EAAE,KAAK,OAAO,CAAC,GAAG;AACjD,kBAAQ,KAAK,0BAA0B;AAAA,QACzC;AAAA,MACF,QAAQ;AAAA,MAAsB;AAAA,IAChC;AAAA,EACF;AAEA,SAAO;AACT;AAcO,SAAS,uBACd,UACA,YAAY,KACG;AACf,SAAO,SACJ,OAAO,CAAC,MAAM,EAAE,SAAS,QAAQ,EACjC,IAAI,CAAC,MAAM;AACV,UAAM,aAAa,iBAAiB,EAAE,OAAO;AAC7C,UAAM,WAAW,WACd,QAAQ,2EAA2E,YAAY,EAC/F,MAAM,GAAG,SAAS;AACrB,WAAO,EAAE,MAAM,EAAE,MAAM,SAAS,SAAS;AAAA,EAC3C,CAAC;AACL;AAOO,SAAS,6BACd,UACA,YAAY,KAC8C;AAC1D,QAAM,WAAW,uBAAuB,UAAU,SAAS;AAC3D,QAAM,aAAa,SAAS,IAAI,OAAK,EAAE,OAAO,EAAE,KAAK,GAAG;AACxD,QAAM,oBAAoB,gBAAgB,UAAU;AACpD,SAAO,EAAE,UAAU,UAAU,kBAAkB;AACjD;AASO,SAAS,6BACd,QACA,cACQ;AACR,MAAI,CAAC,gBAAgB,aAAa,SAAS,GAAI,QAAO;AAGtD,QAAM,SAAS,aACZ,MAAM,OAAO,EACb,IAAI,OAAK,EAAE,KAAK,CAAC,EACjB,OAAO,OAAK,EAAE,UAAU,EAAE;AAE7B,MAAI,WAAW;AACf,aAAW,SAAS,QAAQ;AAE1B,UAAM,MAAM,SAAS,YAAY,EAAE,QAAQ,MAAM,YAAY,CAAC;AAC9D,QAAI,OAAO,GAAG;AACZ,iBAAW,SAAS,MAAM,GAAG,GAAG,IAAI,0CAAqC,SAAS,MAAM,MAAM,MAAM,MAAM;AAAA,IAC5G;AAAA,EACF;AAEA,SAAO;AACT;;;AFlHO,SAAS,mBAAmB,QAAuB;AACxD,QAAM,KAAK,IAAI,KAAK;AACpB,QAAM,SAAS,OAAO,oBAAoB;AAC1C,QAAM,iBAAiC,OAAO,kBAAkB,IAAI,qBAAqB;AACzF,QAAM,kBAAkB,OAAO,aAAa,EAAE,OAAO,IAAI,eAAe,GAAG;AAC3E,QAAM,aAAa,OAAO,cAAc,IAAI,iBAAiB;AAC7D,QAAM,gBAAgB,OAAO,iBAAiB;AAI9C,KAAG,IAAI,2BAA2B,OAAO,MAAM;AAC7C,UAAM,OAAO,EAAE,IAAI,MAAM,MAAM;AAC/B,UAAM,QAAQ,MAAM,OAAO,aAAa,IAAI;AAC5C,QAAI,CAAC,MAAO,QAAO,EAAE,KAAK,EAAE,OAAO,mCAAmC,GAAG,GAAG;AAE5E,UAAM,iBAAiD;AAAA,MACrD;AAAA,QACE,MAAM;AAAA,QACN,UAAU,OAAO,KAAK;AAAA,QACtB,UAAU,OAAO,KAAK;AAAA,QACtB,kBAAkB,OAAO,KAAK;AAAA,MAChC;AAAA,IACF;AACA,QAAI,OAAO,KAAK;AACd,qBAAe,KAAK;AAAA,QAClB,MAAM;AAAA,QACN,OAAO,OAAO,IAAI;AAAA,QAClB,QAAQ,OAAO,IAAI,UAAU;AAAA,MAC/B,CAAC;AAAA,IACH;AACA,mBAAe,KAAK,EAAE,MAAM,WAAW,QAAQ,YAAY,CAAC;AAE5D,WAAO,EAAE,KAAK;AAAA,MACZ,MAAM,MAAM;AAAA,MACZ,SAAS;AAAA,QACP,eAAe,MAAM;AAAA,QACrB,UAAU;AAAA,QACV,sBAAsB,MAAM;AAAA,MAC9B;AAAA,MACA,SAAS;AAAA,QACP,MAAM,MAAM,kBAAkB,cAAc;AAAA,QAC5C,UAAU,MAAM,mBAAmB,OAAO,WAAW;AAAA,MACvD;AAAA,MACA,iBAAiB;AAAA,MACjB,cAAc,CAAC,oBAAoB,WAAW;AAAA,MAC9C,mBAAmB;AAAA,IACrB,CAAC;AAAA,EACH,CAAC;AAID,KAAG,KAAK,2BAA2B,OAAO,MAAM;AAC9C,UAAM,OAAO,EAAE,IAAI,MAAM,MAAM;AAC/B,UAAM,UAAU,KAAK,IAAI;AAGzB,UAAM,QAAQ,MAAM,OAAO,aAAa,IAAI;AAC5C,QAAI,CAAC,OAAO;AACV,aAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,mBAAmB,MAAM,YAAY,EAAE,GAAG,GAAG;AAAA,IACjF;AAGA,UAAM,gBAAgB,SAAS,EAAE,IAAI,OAAO,gBAAgB,KAAK,KAAK,EAAE;AACxE,QAAI,gBAAgB,OAAO;AACzB,aAAO,EAAE;AAAA,QACP,EAAE,OAAO,EAAE,SAAS,qCAAqC,MAAM,kBAAkB,EAAE;AAAA,QACnF;AAAA,MACF;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,aAAO,MAAM,EAAE,IAAI,KAAK;AAAA,IAC1B,QAAQ;AACN,aAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,gBAAgB,MAAM,kBAAkB,EAAE,GAAG,GAAG;AAAA,IACpF;AACA,QAAI,CAAC,KAAK,UAAU,QAAQ;AAC1B,aAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,2BAA2B,MAAM,kBAAkB,EAAE,GAAG,GAAG;AAAA,IAC/F;AAGA,UAAM,kBAAkB,EAAE,IAAI,OAAO,qBAAqB;AAC1D,UAAM,aAAa,EAAE,IAAI,OAAO,eAAe,KAAK;AACpD,QAAI,aAA4B;AAChC,QAAI,gBAA+B;AACnC,QAAI,UAA6B;AAEjC,QAAI,iBAAiB;AACnB,YAAM,SAAS,MAAM,WAAW,iBAAiB,OAAO,MAAM,UAAU;AACxE,UAAI,CAAC,QAAQ;AACX,eAAO,EAAE;AAAA,UACP,EAAE,OAAO,EAAE,SAAS,+BAA+B,MAAM,wBAAwB,MAAM,qBAAqB,EAAE;AAAA,UAC9G,EAAE,QAAQ,KAAK,SAAS,EAAE,sBAAsB,YAAY,EAAE;AAAA,QAChE;AAAA,MACF;AACA,mBAAa;AACb,sBAAgB;AAAA,IAClB,WAAW,OAAO,OAAO,WAAW,YAAY,EAAE,WAAW,UAAU,GAAG;AACxE,YAAM,SAAS,MAAM,UAAU,YAAY,OAAO,KAAK,OAAO,IAAI;AAClE,UAAI,CAAC,QAAQ;AACX,cAAM,QAAQ,OAAO,IAAI;AACzB,cAAM,SAAS,OAAO,IAAI,UAAU;AACpC,eAAO,EAAE;AAAA,UACP,EAAE,OAAO,EAAE,SAAS,8BAA8B,MAAM,wBAAwB,MAAM,yBAAyB,EAAE;AAAA,UACjH,EAAE,QAAQ,KAAK,SAAS,EAAE,oBAAoB,kBAAkB,KAAK,cAAc,MAAM,IAAI,EAAE;AAAA,QACjG;AAAA,MACF;AACA,mBAAa;AACb,sBAAgB;AAAA,IAClB,WAAW,WAAW,WAAW,SAAS,GAAG;AAC3C,YAAM,SAAS,OAAO,gBAAgB;AACtC,YAAM,MAAM,MAAM,OAAO,UAAU;AACnC,UAAI,CAAC,KAAK;AACR,eAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,mBAAmB,MAAM,uBAAuB,EAAE,GAAG,GAAG;AAAA,MAC5F;AAGA,UAAI,IAAI,UAAU,IAAI,OAAO,SAAS,KAAK,CAAC,IAAI,OAAO,SAAS,aAAa,GAAG;AAC9E,eAAO,EAAE;AAAA,UACP,EAAE,OAAO,EAAE,SAAS,mCAAmC,aAAa,IAAI,MAAM,aAAa,MAAM,qBAAqB,EAAE;AAAA,UACxH;AAAA,QACF;AAAA,MACF;AAEA,mBAAa,IAAI;AACjB,sBAAgB;AAChB,gBAAU;AAAA,IACZ,OAAO;AAEL,YAAM,UAAoB,CAAC,MAAM;AACjC,UAAI,OAAO,IAAK,SAAQ,KAAK,KAAK;AAClC,cAAQ,KAAK,SAAS;AAEtB,YAAM,UAAkC,EAAE,sBAAsB,QAAQ,KAAK,IAAI,EAAE;AACnF,UAAI,OAAO,KAAK;AACd,gBAAQ,kBAAkB,IAAI,kBAAkB,OAAO,IAAI,KAAK,cAAc,OAAO,IAAI,UAAU,cAAc;AAAA,MACnH;AAEA,aAAO,EAAE,KAAK;AAAA,QACZ,OAAO;AAAA,UACL,SAAS;AAAA,UACT,MAAM;AAAA,UACN,iBAAiB;AAAA,UACjB,MAAM;AAAA,YACJ,UAAU,OAAO,KAAK;AAAA,YACtB,UAAU,OAAO,KAAK;AAAA,YACtB,iBAAiB,OAAO,KAAK;AAAA,YAC7B,8BAA8B;AAAA,UAChC;AAAA,UACA,GAAI,OAAO,MAAM;AAAA,YACf,KAAK,EAAE,OAAO,OAAO,IAAI,OAAO,QAAQ,OAAO,IAAI,UAAU,eAAe;AAAA,UAC9E,IAAI,CAAC;AAAA,UACL,SAAS;AAAA,YACP,cAAc,OAAO,UAAU,GAAG,OAAO,OAAO,WAAW,IAAI,cAAc;AAAA,UAC/E;AAAA,QACF;AAAA,MACF,GAAG,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,IAC7B;AAGA,UAAM,qBAAqB,SAAS,qBAChC,EAAE,OAAO,QAAQ,oBAAoB,eAAe,GAAG,IACvD;AAEJ,UAAM,KAAK,MAAM,eAAe,YAAa,oBAAoB,cAAc;AAC/E,QAAI,CAAC,GAAG,SAAS;AACf,aAAO,EAAE;AAAA,QACP,EAAE,OAAO,EAAE,SAAS,uBAAuB,MAAM,oBAAoB,aAAa,GAAG,kBAAkB,EAAE;AAAA,QACzG,EAAE,QAAQ,KAAK,SAAS,EAAE,eAAe,OAAO,GAAG,qBAAqB,EAAE,EAAE,EAAE;AAAA,MAChF;AAAA,IACF;AAGA,UAAM,EAAE,UAAU,UAAU,kBAAkB,IAAI,6BAA6B,KAAK,UAAU,MAAM;AAEpG,QAAI,kBAAkB,SAAS,GAAG;AAEhC,cAAQ,KAAK,2CAA2C,UAAU,KAAK,kBAAkB,KAAK,IAAI,CAAC,EAAE;AAErG,UAAI,OAAO,gBAAgB;AACzB,eAAO,EAAE;AAAA,UACP,EAAE,OAAO,EAAE,SAAS,yDAAyD,MAAM,2BAA2B,EAAE;AAAA,UAChH;AAAA,QACF;AAAA,MACF;AAAA,IAEF;AAEA,UAAM,cAAc,SACjB,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,EAC/B,IAAI,CAAC,MAAM,EAAE,OAAO,EACpB,KAAK,MAAM;AAEd,QAAI,CAAC,aAAa;AAChB,aAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,4BAA4B,MAAM,kBAAkB,EAAE,GAAG,GAAG;AAAA,IAChG;AAGA,QAAI,cAAc,KAAK,KAAK,YAAY,SAAS,CAAC;AAClD,QAAI,eAAe;AAEnB,UAAM,SAAS,IAAI,eAAe;AAAA,MAChC,MAAM,MAAM,YAAY;AACtB,cAAM,UAAU,IAAI,YAAY;AAChC,cAAM,YAAY,CAAC,aAAqB;AAEtC,gBAAM,QAAQ,6BAA6B,UAAU,MAAM,YAAY;AACvE,0BAAgB,KAAK,KAAK,MAAM,SAAS,CAAC;AAC1C,gBAAM,QAA6B;AAAA,YACjC,IAAI,YAAY,KAAK,IAAI,CAAC;AAAA,YAC1B,QAAQ;AAAA,YACR,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,YACrC,OAAO,MAAM;AAAA,YACb,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,EAAE,SAAS,MAAM,GAAG,eAAe,KAAK,CAAC;AAAA,UACxE;AACA,qBAAW,QAAQ,QAAQ,OAAO,SAAS,KAAK,UAAU,KAAK,CAAC;AAAA;AAAA,CAAM,CAAC;AAAA,QACzE;AAEA,YAAI;AACF,gBAAM,MAAM,MAAM,OAAO,WAAW,KAAK;AACzC,gBAAM,eAAe,IAAI,aAAa,aAAa;AAAA,YACjD,WAAW,YAAY,UAAU;AAAA,YACjC,cAAc,MAAM;AAAA,UACtB,CAAC;AAED,2BAAiB,SAAS,cAAc;AACtC,gBACE,MAAM,SAAS,0BACf,MAAM,MAAM,MAAM,SAAS,UAC3B,MAAM,KAAK,OACX;AACA,wBAAU,MAAM,KAAK,KAAK;AAAA,YAC5B;AAAA,UACF;AAGA,gBAAM,OAA4B;AAAA,YAChC,IAAI,YAAY,KAAK,IAAI,CAAC;AAAA,YAC1B,QAAQ;AAAA,YACR,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,YACrC,OAAO,MAAM;AAAA,YACb,SAAS,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,eAAe,OAAO,CAAC;AAAA,UAC1D;AACA,qBAAW,QAAQ,QAAQ,OAAO,SAAS,KAAK,UAAU,IAAI,CAAC;AAAA;AAAA,CAAM,CAAC;AACtE,qBAAW,QAAQ,QAAQ,OAAO,kBAAkB,CAAC;AAGrD,gBAAM,aAAa,cAAc,gBAAgB,MAAM;AACvD,gBAAM,cAAc,aAAa,IAAI,MAAM;AAC3C,gBAAM,cAAc,YAAY,MAAM;AAEtC,gBAAM,OAAO,YAAY;AAAA,YACvB,SAAS,MAAM;AAAA,YACf,WAAW,MAAM;AAAA,YACjB;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,cAAc;AAAA,YACd,gBAAgB;AAAA,YAChB,gBAAgB;AAAA,YAChB,YAAY,KAAK,IAAI,IAAI;AAAA,UAC3B,CAAC;AAED,cAAI,OAAO,eAAe;AACxB,kBAAM,OAAO,cAAc,EAAE,QAAQ,eAAe,WAAwB,GAAG,SAAS,EAAE,MAAM,SAAO;AACrG,sBAAQ,MAAM,yCAAyC,UAAU,KAAK,eAAe,QAAQ,IAAI,UAAU,GAAG,EAAE;AAAA,YAClH,CAAC;AAAA,UACH;AAAA,QACF,SAAS,KAAK;AAEZ,gBAAM,cAAc,eAAe,QAC9B,IAAI,QAAQ,SAAS,GAAG,KAAK,IAAI,QAAQ,SAAS,IAAI,IAAI,yBAAyB,IAAI,UACxF;AACJ,qBAAW;AAAA,YACT,QAAQ,OAAO,SAAS,KAAK,UAAU,EAAE,OAAO,EAAE,SAAS,aAAa,MAAM,eAAe,EAAE,CAAC,CAAC;AAAA;AAAA,CAAM;AAAA,UACzG;AAAA,QACF,UAAE;AACA,qBAAW,MAAM;AAAA,QACnB;AAAA,MACF;AAAA,IACF,CAAC;AAED,WAAO,IAAI,SAAS,QAAQ;AAAA,MAC1B,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,iBAAiB;AAAA,QACjB,gBAAgB,MAAM;AAAA,QACtB,mBAAmB,MAAM,kBAAkB,cAAc;AAAA,QACzD,oBAAoB;AAAA,QACpB,qBAAqB,kBAAkB,SAAS,YAAY;AAAA,QAC5D,GAAI,GAAG,cAAc,SAAY,EAAE,yBAAyB,OAAO,GAAG,SAAS,EAAE,IAAI,CAAC;AAAA,MACxF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAED,SAAO;AACT;","names":[]}
|
|
@@ -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/** In-memory nonce store with automatic eviction */\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"],"mappings":";AAaO,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;","names":[]}
|