@tangle-network/agent-gateway 0.1.0 → 0.3.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/dist/{chunk-EMGS63QE.js → chunk-4DUR2B47.js} +127 -17
- package/dist/chunk-4DUR2B47.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 +13 -3
- 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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MemoryNonceStore
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-M7ZJAK4K.js";
|
|
4
4
|
import {
|
|
5
5
|
MemoryRateLimitStore,
|
|
6
6
|
checkRateLimit
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-XCTXHZ76.js";
|
|
8
8
|
|
|
9
9
|
// src/middleware.ts
|
|
10
10
|
import { Hono } from "hono";
|
|
@@ -138,6 +138,84 @@ function redactSystemPromptFromOutput(output, systemPrompt) {
|
|
|
138
138
|
return redacted;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// src/observer.ts
|
|
142
|
+
var ConsoleObserver = class {
|
|
143
|
+
constructor(log = (e) => console.log(JSON.stringify(e))) {
|
|
144
|
+
this.log = log;
|
|
145
|
+
}
|
|
146
|
+
log;
|
|
147
|
+
emit(level, event, ctx, rest = {}) {
|
|
148
|
+
this.log({
|
|
149
|
+
level,
|
|
150
|
+
event,
|
|
151
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
152
|
+
requestId: ctx.requestId,
|
|
153
|
+
agentSlug: ctx.agentSlug,
|
|
154
|
+
durationMs: Date.now() - ctx.startMs,
|
|
155
|
+
...rest
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
onRequestStart(ctx) {
|
|
159
|
+
this.emit("info", "gateway.request.start", ctx);
|
|
160
|
+
}
|
|
161
|
+
onPaymentVerified(ctx, info) {
|
|
162
|
+
this.emit("info", "gateway.payment.verified", ctx, info);
|
|
163
|
+
}
|
|
164
|
+
onAuthFailure(ctx, reason) {
|
|
165
|
+
this.emit("warn", "gateway.auth.failure", ctx, reason);
|
|
166
|
+
}
|
|
167
|
+
onRateLimited(ctx, info) {
|
|
168
|
+
this.emit("warn", "gateway.rate_limit", ctx, info);
|
|
169
|
+
}
|
|
170
|
+
onBodyTooLarge(ctx, contentLength) {
|
|
171
|
+
this.emit("warn", "gateway.body_too_large", ctx, { contentLength });
|
|
172
|
+
}
|
|
173
|
+
onInjectionDetected(ctx, info) {
|
|
174
|
+
this.emit("warn", "gateway.injection", ctx, info);
|
|
175
|
+
}
|
|
176
|
+
onRequestComplete(ctx, usage) {
|
|
177
|
+
this.emit("info", "gateway.request.complete", ctx, usage);
|
|
178
|
+
}
|
|
179
|
+
onStreamError(ctx, info) {
|
|
180
|
+
this.emit("error", "gateway.stream.error", ctx, info);
|
|
181
|
+
}
|
|
182
|
+
onSettlementError(ctx, info) {
|
|
183
|
+
this.emit("error", "gateway.settlement.error", ctx, info);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var CompositeObserver = class {
|
|
187
|
+
constructor(observers) {
|
|
188
|
+
this.observers = observers;
|
|
189
|
+
}
|
|
190
|
+
observers;
|
|
191
|
+
async fanOut(event, ...args) {
|
|
192
|
+
for (const obs of this.observers) {
|
|
193
|
+
const fn = obs[event];
|
|
194
|
+
if (!fn) continue;
|
|
195
|
+
try {
|
|
196
|
+
await fn.apply(obs, args);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.warn(`[agent-gateway] observer ${event} threw:`, err instanceof Error ? err.message : err);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
onRequestStart = (ctx) => this.fanOut("onRequestStart", ctx);
|
|
203
|
+
onPaymentVerified = (ctx, info) => this.fanOut("onPaymentVerified", ctx, info);
|
|
204
|
+
onAuthFailure = (ctx, reason) => this.fanOut("onAuthFailure", ctx, reason);
|
|
205
|
+
onRateLimited = (ctx, info) => this.fanOut("onRateLimited", ctx, info);
|
|
206
|
+
onBodyTooLarge = (ctx, contentLength) => this.fanOut("onBodyTooLarge", ctx, contentLength);
|
|
207
|
+
onInjectionDetected = (ctx, info) => this.fanOut("onInjectionDetected", ctx, info);
|
|
208
|
+
onRequestComplete = (ctx, usage) => this.fanOut("onRequestComplete", ctx, usage);
|
|
209
|
+
onStreamError = (ctx, info) => this.fanOut("onStreamError", ctx, info);
|
|
210
|
+
onSettlementError = (ctx, info) => this.fanOut("onSettlementError", ctx, info);
|
|
211
|
+
};
|
|
212
|
+
function generateRequestId() {
|
|
213
|
+
const bytes = new Uint8Array(16);
|
|
214
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
215
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
216
|
+
return `req_${hex}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
141
219
|
// src/middleware.ts
|
|
142
220
|
function createAgentGateway(config) {
|
|
143
221
|
const gw = new Hono();
|
|
@@ -146,6 +224,7 @@ function createAgentGateway(config) {
|
|
|
146
224
|
const globalRateLimit = config.rateLimit ?? { limit: 60, windowSeconds: 60 };
|
|
147
225
|
const nonceStore = config.nonceStore ?? new MemoryNonceStore();
|
|
148
226
|
const requiredScope = config.requiredScope ?? "chat";
|
|
227
|
+
const obs = config.observer;
|
|
149
228
|
gw.get("/:slug/chat/completions", async (c) => {
|
|
150
229
|
const slug = c.req.param("slug");
|
|
151
230
|
const agent = await config.resolveAgent(slug);
|
|
@@ -185,12 +264,16 @@ function createAgentGateway(config) {
|
|
|
185
264
|
gw.post("/:slug/chat/completions", async (c) => {
|
|
186
265
|
const slug = c.req.param("slug");
|
|
187
266
|
const startMs = Date.now();
|
|
267
|
+
const requestId = generateRequestId();
|
|
268
|
+
const ctx = { requestId, agentSlug: slug, startMs };
|
|
269
|
+
await obs?.onRequestStart?.(ctx);
|
|
188
270
|
const agent = await config.resolveAgent(slug);
|
|
189
271
|
if (!agent) {
|
|
190
272
|
return c.json({ error: { message: "Agent not found", type: "not_found" } }, 404);
|
|
191
273
|
}
|
|
192
274
|
const contentLength = parseInt(c.req.header("Content-Length") ?? "0", 10);
|
|
193
275
|
if (contentLength > 65536) {
|
|
276
|
+
await obs?.onBodyTooLarge?.(ctx, contentLength);
|
|
194
277
|
return c.json(
|
|
195
278
|
{ error: { message: "Request body too large (max 64KB)", type: "invalid_request" } },
|
|
196
279
|
413
|
|
@@ -213,9 +296,10 @@ function createAgentGateway(config) {
|
|
|
213
296
|
if (spendAuthHeader) {
|
|
214
297
|
const signer = await verifyX402(spendAuthHeader, config.x402, nonceStore);
|
|
215
298
|
if (!signer) {
|
|
299
|
+
await obs?.onAuthFailure?.(ctx, { method: "x402", code: "invalid_spend_auth", httpStatus: 402 });
|
|
216
300
|
return c.json(
|
|
217
301
|
{ error: { message: "Invalid X-Payment-Signature", type: "authentication_error", code: "invalid_spend_auth" } },
|
|
218
|
-
{ status: 402, headers: { "X-Payment-Required": "spendauth" } }
|
|
302
|
+
{ status: 402, headers: { "X-Payment-Required": "spendauth", "X-Request-Id": requestId } }
|
|
219
303
|
);
|
|
220
304
|
}
|
|
221
305
|
consumerId = signer;
|
|
@@ -225,9 +309,10 @@ function createAgentGateway(config) {
|
|
|
225
309
|
if (!signer) {
|
|
226
310
|
const realm = config.mpp.realm;
|
|
227
311
|
const method = config.mpp.method ?? "blueprintevm";
|
|
312
|
+
await obs?.onAuthFailure?.(ctx, { method: "mpp", code: "invalid_mpp_credential", httpStatus: 401 });
|
|
228
313
|
return c.json(
|
|
229
314
|
{ error: { message: "Invalid Payment credential", type: "authentication_error", code: "invalid_mpp_credential" } },
|
|
230
|
-
{ status: 401, headers: { "WWW-Authenticate": `Payment realm="${realm}", method="${method}"
|
|
315
|
+
{ status: 401, headers: { "WWW-Authenticate": `Payment realm="${realm}", method="${method}"`, "X-Request-Id": requestId } }
|
|
231
316
|
);
|
|
232
317
|
}
|
|
233
318
|
consumerId = signer;
|
|
@@ -236,22 +321,31 @@ function createAgentGateway(config) {
|
|
|
236
321
|
const verify = config.verifyApiKey ?? defaultVerifyApiKey;
|
|
237
322
|
const key = await verify(authHeader);
|
|
238
323
|
if (!key) {
|
|
239
|
-
|
|
324
|
+
await obs?.onAuthFailure?.(ctx, { method: "apikey", code: "invalid_api_key", httpStatus: 401 });
|
|
325
|
+
return c.json(
|
|
326
|
+
{ error: { message: "Invalid API key", type: "authentication_error" } },
|
|
327
|
+
{ status: 401, headers: { "X-Request-Id": requestId } }
|
|
328
|
+
);
|
|
240
329
|
}
|
|
241
330
|
if (key.scopes && key.scopes.length > 0 && !key.scopes.includes(requiredScope)) {
|
|
331
|
+
await obs?.onAuthFailure?.(ctx, { method: "apikey", code: "insufficient_scope", httpStatus: 403 });
|
|
242
332
|
return c.json(
|
|
243
333
|
{ error: { message: `API key missing required scope: ${requiredScope}`, type: "forbidden", code: "insufficient_scope" } },
|
|
244
|
-
403
|
|
334
|
+
{ status: 403, headers: { "X-Request-Id": requestId } }
|
|
245
335
|
);
|
|
246
336
|
}
|
|
247
337
|
consumerId = key.consumerId;
|
|
248
338
|
paymentMethod = "apikey";
|
|
249
339
|
keyInfo = key;
|
|
250
340
|
} else {
|
|
341
|
+
await obs?.onAuthFailure?.(ctx, { method: "none", code: "payment_required", httpStatus: 402 });
|
|
251
342
|
const methods = ["x402"];
|
|
252
343
|
if (config.mpp) methods.push("mpp");
|
|
253
344
|
methods.push("api_key");
|
|
254
|
-
const headers = {
|
|
345
|
+
const headers = {
|
|
346
|
+
"X-Payment-Required": methods.join(", "),
|
|
347
|
+
"X-Request-Id": requestId
|
|
348
|
+
};
|
|
255
349
|
if (config.mpp) {
|
|
256
350
|
headers["WWW-Authenticate"] = `Payment realm="${config.mpp.realm}", method="${config.mpp.method ?? "blueprintevm"}"`;
|
|
257
351
|
}
|
|
@@ -275,21 +369,27 @@ function createAgentGateway(config) {
|
|
|
275
369
|
}
|
|
276
370
|
}, { status: 402, headers });
|
|
277
371
|
}
|
|
372
|
+
await obs?.onPaymentVerified?.(ctx, { method: paymentMethod, consumerId, keyId: keyInfo?.keyId });
|
|
278
373
|
const effectiveRateLimit = keyInfo?.rateLimitPerMinute ? { limit: keyInfo.rateLimitPerMinute, windowSeconds: 60 } : globalRateLimit;
|
|
279
374
|
const rl = await checkRateLimit(consumerId, effectiveRateLimit, rateLimitStore);
|
|
280
375
|
if (!rl.allowed) {
|
|
376
|
+
await obs?.onRateLimited?.(ctx, { consumerId, retryAfterSeconds: rl.retryAfterSeconds ?? 60 });
|
|
281
377
|
return c.json(
|
|
282
378
|
{ error: { message: "Rate limit exceeded", type: "rate_limit_error", retry_after: rl.retryAfterSeconds } },
|
|
283
|
-
{ status: 429, headers: { "Retry-After": String(rl.retryAfterSeconds ?? 60) } }
|
|
379
|
+
{ status: 429, headers: { "Retry-After": String(rl.retryAfterSeconds ?? 60), "X-Request-Id": requestId } }
|
|
284
380
|
);
|
|
285
381
|
}
|
|
286
382
|
const { messages: filtered, injectionWarnings } = filterConsumerMessagesStrict(body.messages, maxLen);
|
|
287
383
|
if (injectionWarnings.length > 0) {
|
|
288
|
-
|
|
384
|
+
await obs?.onInjectionDetected?.(ctx, {
|
|
385
|
+
consumerId,
|
|
386
|
+
patterns: injectionWarnings,
|
|
387
|
+
blocked: !!config.blockInjection
|
|
388
|
+
});
|
|
289
389
|
if (config.blockInjection) {
|
|
290
390
|
return c.json(
|
|
291
391
|
{ error: { message: "Request rejected: potential prompt injection detected", type: "content_policy_violation" } },
|
|
292
|
-
400
|
|
392
|
+
{ status: 400, headers: { "X-Request-Id": requestId } }
|
|
293
393
|
);
|
|
294
394
|
}
|
|
295
395
|
}
|
|
@@ -297,7 +397,7 @@ function createAgentGateway(config) {
|
|
|
297
397
|
if (!userMessage) {
|
|
298
398
|
return c.json({ error: { message: "No user message provided", type: "invalid_request" } }, 400);
|
|
299
399
|
}
|
|
300
|
-
|
|
400
|
+
const inputTokens = Math.ceil(userMessage.length / 4);
|
|
301
401
|
let outputTokens = 0;
|
|
302
402
|
const stream = new ReadableStream({
|
|
303
403
|
async start(controller) {
|
|
@@ -341,7 +441,7 @@ function createAgentGateway(config) {
|
|
|
341
441
|
const totalCost = (inputTokens + outputTokens) * agent.pricePerTokenUsd;
|
|
342
442
|
const ownerEarned = totalCost * (1 - agent.platformFeePercent);
|
|
343
443
|
const platformFee = totalCost * agent.platformFeePercent;
|
|
344
|
-
|
|
444
|
+
const usageEvent = {
|
|
345
445
|
agentId: agent.id,
|
|
346
446
|
agentSlug: agent.slug,
|
|
347
447
|
consumerId,
|
|
@@ -352,14 +452,20 @@ function createAgentGateway(config) {
|
|
|
352
452
|
ownerEarnedUsd: ownerEarned,
|
|
353
453
|
platformFeeUsd: platformFee,
|
|
354
454
|
durationMs: Date.now() - startMs
|
|
355
|
-
}
|
|
455
|
+
};
|
|
456
|
+
await config.recordUsage(usageEvent);
|
|
457
|
+
await obs?.onRequestComplete?.(ctx, usageEvent);
|
|
356
458
|
if (config.settlePayment) {
|
|
357
|
-
await config.settlePayment({ method: paymentMethod, consumerId }, totalCost).catch((err) => {
|
|
358
|
-
|
|
459
|
+
await config.settlePayment({ method: paymentMethod, consumerId }, totalCost).catch(async (err) => {
|
|
460
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
461
|
+
console.error(`[agent-gateway] settlement failed for ${consumerId}: ${msg}`);
|
|
462
|
+
await obs?.onSettlementError?.(ctx, { consumerId, method: paymentMethod, errorMessage: msg });
|
|
359
463
|
});
|
|
360
464
|
}
|
|
361
465
|
} catch (err) {
|
|
362
|
-
const
|
|
466
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
467
|
+
const safeMessage = rawMessage.includes("/") || rawMessage.includes("\\") ? "Internal agent error" : rawMessage;
|
|
468
|
+
await obs?.onStreamError?.(ctx, { consumerId, errorMessage: rawMessage });
|
|
363
469
|
controller.enqueue(
|
|
364
470
|
encoder.encode(`data: ${JSON.stringify({ error: { message: safeMessage, type: "server_error" } })}
|
|
365
471
|
|
|
@@ -374,6 +480,7 @@ function createAgentGateway(config) {
|
|
|
374
480
|
headers: {
|
|
375
481
|
"Content-Type": "text/event-stream",
|
|
376
482
|
"Cache-Control": "no-cache",
|
|
483
|
+
"X-Request-Id": requestId,
|
|
377
484
|
"X-Agent-Slug": agent.slug,
|
|
378
485
|
"X-Agent-Hosting": agent.sandboxEndpoint ? "sovereign" : "centralized",
|
|
379
486
|
"X-Payment-Method": paymentMethod,
|
|
@@ -393,6 +500,9 @@ export {
|
|
|
393
500
|
filterConsumerMessages,
|
|
394
501
|
filterConsumerMessagesStrict,
|
|
395
502
|
redactSystemPromptFromOutput,
|
|
503
|
+
ConsoleObserver,
|
|
504
|
+
CompositeObserver,
|
|
505
|
+
generateRequestId,
|
|
396
506
|
createAgentGateway
|
|
397
507
|
};
|
|
398
|
-
//# sourceMappingURL=chunk-
|
|
508
|
+
//# sourceMappingURL=chunk-4DUR2B47.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/verify.ts","../src/filter.ts","../src/observer.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'\nimport { generateRequestId, type GatewayObserver, type RequestContext } from './observer'\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 const obs: GatewayObserver | undefined = config.observer\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 const requestId = generateRequestId()\n const ctx: RequestContext = { requestId, agentSlug: slug, startMs }\n\n await obs?.onRequestStart?.(ctx)\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 await obs?.onBodyTooLarge?.(ctx, contentLength)\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 await obs?.onAuthFailure?.(ctx, { method: 'x402', code: 'invalid_spend_auth', httpStatus: 402 })\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', 'X-Request-Id': requestId } },\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 await obs?.onAuthFailure?.(ctx, { method: 'mpp', code: 'invalid_mpp_credential', httpStatus: 401 })\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}\"`, 'X-Request-Id': requestId } },\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 await obs?.onAuthFailure?.(ctx, { method: 'apikey', code: 'invalid_api_key', httpStatus: 401 })\n return c.json(\n { error: { message: 'Invalid API key', type: 'authentication_error' } },\n { status: 401, headers: { 'X-Request-Id': requestId } },\n )\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 await obs?.onAuthFailure?.(ctx, { method: 'apikey', code: 'insufficient_scope', httpStatus: 403 })\n return c.json(\n { error: { message: `API key missing required scope: ${requiredScope}`, type: 'forbidden', code: 'insufficient_scope' } },\n { status: 403, headers: { 'X-Request-Id': requestId } },\n )\n }\n\n consumerId = key.consumerId\n paymentMethod = 'apikey'\n keyInfo = key\n } else {\n // No payment — return 402 with instructions\n await obs?.onAuthFailure?.(ctx, { method: 'none', code: 'payment_required', httpStatus: 402 })\n const methods: string[] = ['x402']\n if (config.mpp) methods.push('mpp')\n methods.push('api_key')\n\n const headers: Record<string, string> = {\n 'X-Payment-Required': methods.join(', '),\n 'X-Request-Id': requestId,\n }\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 await obs?.onPaymentVerified?.(ctx, { method: paymentMethod, consumerId: consumerId!, keyId: keyInfo?.keyId })\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 await obs?.onRateLimited?.(ctx, { consumerId: consumerId!, retryAfterSeconds: rl.retryAfterSeconds ?? 60 })\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), 'X-Request-Id': requestId } },\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 await obs?.onInjectionDetected?.(ctx, {\n consumerId: consumerId!,\n patterns: injectionWarnings,\n blocked: !!config.blockInjection,\n })\n\n if (config.blockInjection) {\n return c.json(\n { error: { message: 'Request rejected: potential prompt injection detected', type: 'content_policy_violation' } },\n { status: 400, headers: { 'X-Request-Id': requestId } },\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 const 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 const usageEvent = {\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 await config.recordUsage(usageEvent)\n await obs?.onRequestComplete?.(ctx, usageEvent)\n\n if (config.settlePayment) {\n await config.settlePayment({ method: paymentMethod, consumerId: consumerId! }, totalCost).catch(async err => {\n const msg = err instanceof Error ? err.message : String(err)\n console.error(`[agent-gateway] settlement failed for ${consumerId}: ${msg}`)\n await obs?.onSettlementError?.(ctx, { consumerId: consumerId!, method: paymentMethod, errorMessage: msg })\n })\n }\n } catch (err) {\n // Sanitize error — never expose stack traces or internal paths\n const rawMessage = err instanceof Error ? err.message : String(err)\n const safeMessage =\n rawMessage.includes('/') || rawMessage.includes('\\\\')\n ? 'Internal agent error'\n : rawMessage\n await obs?.onStreamError?.(ctx, { consumerId: consumerId!, errorMessage: rawMessage })\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-Request-Id': requestId,\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","/**\n * Observability hook surface.\n *\n * Consumers implement GatewayObserver to wire the gateway into their existing\n * telemetry stack (Langfuse, OTEL, structured logs, Prometheus, etc.) without\n * the gateway itself depending on any of those libraries.\n *\n * Every event carries a requestId so downstream metrics can correlate the\n * payment verification, sandbox execution, and settlement for one request.\n * When no observer is configured, the gateway stays silent.\n */\n\nimport type { PaymentMethod, GatewayUsageEvent } from './types'\n\nexport interface RequestContext {\n requestId: string\n agentSlug: string\n startMs: number\n}\n\nexport interface AuthFailureReason {\n method: 'x402' | 'mpp' | 'apikey' | 'none'\n code: string\n httpStatus: number\n}\n\nexport interface GatewayObserver {\n /** Called at the start of every chat completions POST. */\n onRequestStart?: (ctx: RequestContext) => void | Promise<void>\n\n /** Called when a payment method has been successfully verified. */\n onPaymentVerified?: (ctx: RequestContext, info: {\n method: PaymentMethod\n consumerId: string\n keyId?: string\n }) => void | Promise<void>\n\n /** Called when auth fails — every branch. */\n onAuthFailure?: (ctx: RequestContext, reason: AuthFailureReason) => void | Promise<void>\n\n /** Called when a consumer hits the rate limit. */\n onRateLimited?: (ctx: RequestContext, info: {\n consumerId: string\n retryAfterSeconds: number\n }) => void | Promise<void>\n\n /** Called when the request body exceeds the 64KB limit. */\n onBodyTooLarge?: (ctx: RequestContext, contentLength: number) => void | Promise<void>\n\n /**\n * Called when prompt-injection patterns are detected.\n * `blocked` is true when blockInjection config is on and the request was\n * rejected; false when the patterns were logged but the request proceeded.\n */\n onInjectionDetected?: (ctx: RequestContext, info: {\n consumerId: string\n patterns: string[]\n blocked: boolean\n }) => void | Promise<void>\n\n /** Called after a successful stream completes and recordUsage has fired. */\n onRequestComplete?: (ctx: RequestContext, usage: GatewayUsageEvent) => void | Promise<void>\n\n /** Called when the sandbox throws. The error message is pre-scrubbed. */\n onStreamError?: (ctx: RequestContext, info: {\n consumerId: string\n errorMessage: string\n }) => void | Promise<void>\n\n /** Called when settlement fails. Payment already occurred; this is async bookkeeping. */\n onSettlementError?: (ctx: RequestContext, info: {\n consumerId: string\n method: PaymentMethod\n errorMessage: string\n }) => void | Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Convenience implementations\n// ---------------------------------------------------------------------------\n\n/**\n * Structured-log observer. Emits one JSON line per event on the `log` function.\n * Default sink: console.log. Production consumers usually pipe their own\n * structured logger (pino, winston, the cf Logs binding).\n *\n * Usage:\n * new ConsoleObserver(({ level, event, ...rest }) => logger.info({ event, ...rest }))\n */\nexport class ConsoleObserver implements GatewayObserver {\n constructor(\n private readonly log: (entry: Record<string, unknown>) => void = (e) => console.log(JSON.stringify(e)),\n ) {}\n\n private emit(level: 'info' | 'warn' | 'error', event: string, ctx: RequestContext, rest: Record<string, unknown> = {}) {\n this.log({\n level,\n event,\n time: new Date().toISOString(),\n requestId: ctx.requestId,\n agentSlug: ctx.agentSlug,\n durationMs: Date.now() - ctx.startMs,\n ...rest,\n })\n }\n\n onRequestStart(ctx: RequestContext) { this.emit('info', 'gateway.request.start', ctx) }\n onPaymentVerified(ctx: RequestContext, info: { method: PaymentMethod; consumerId: string; keyId?: string }) {\n this.emit('info', 'gateway.payment.verified', ctx, info)\n }\n onAuthFailure(ctx: RequestContext, reason: AuthFailureReason) {\n this.emit('warn', 'gateway.auth.failure', ctx, reason as unknown as Record<string, unknown>)\n }\n onRateLimited(ctx: RequestContext, info: { consumerId: string; retryAfterSeconds: number }) {\n this.emit('warn', 'gateway.rate_limit', ctx, info)\n }\n onBodyTooLarge(ctx: RequestContext, contentLength: number) {\n this.emit('warn', 'gateway.body_too_large', ctx, { contentLength })\n }\n onInjectionDetected(ctx: RequestContext, info: { consumerId: string; patterns: string[]; blocked: boolean }) {\n this.emit('warn', 'gateway.injection', ctx, info)\n }\n onRequestComplete(ctx: RequestContext, usage: GatewayUsageEvent) {\n this.emit('info', 'gateway.request.complete', ctx, usage as unknown as Record<string, unknown>)\n }\n onStreamError(ctx: RequestContext, info: { consumerId: string; errorMessage: string }) {\n this.emit('error', 'gateway.stream.error', ctx, info)\n }\n onSettlementError(ctx: RequestContext, info: { consumerId: string; method: PaymentMethod; errorMessage: string }) {\n this.emit('error', 'gateway.settlement.error', ctx, info)\n }\n}\n\n/**\n * Compose multiple observers into one. Errors in any individual observer\n * don't break the others (fire-and-forget telemetry).\n */\nexport class CompositeObserver implements GatewayObserver {\n constructor(private readonly observers: GatewayObserver[]) {}\n\n private async fanOut<K extends keyof GatewayObserver>(event: K, ...args: unknown[]): Promise<void> {\n for (const obs of this.observers) {\n const fn = obs[event] as ((...a: unknown[]) => void | Promise<void>) | undefined\n if (!fn) continue\n try {\n await fn.apply(obs, args)\n } catch (err) {\n console.warn(`[agent-gateway] observer ${event} threw:`, err instanceof Error ? err.message : err)\n }\n }\n }\n\n onRequestStart = (ctx: RequestContext) => this.fanOut('onRequestStart', ctx)\n onPaymentVerified = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onPaymentVerified']>[1]) =>\n this.fanOut('onPaymentVerified', ctx, info)\n onAuthFailure = (ctx: RequestContext, reason: AuthFailureReason) =>\n this.fanOut('onAuthFailure', ctx, reason)\n onRateLimited = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onRateLimited']>[1]) =>\n this.fanOut('onRateLimited', ctx, info)\n onBodyTooLarge = (ctx: RequestContext, contentLength: number) =>\n this.fanOut('onBodyTooLarge', ctx, contentLength)\n onInjectionDetected = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onInjectionDetected']>[1]) =>\n this.fanOut('onInjectionDetected', ctx, info)\n onRequestComplete = (ctx: RequestContext, usage: GatewayUsageEvent) =>\n this.fanOut('onRequestComplete', ctx, usage)\n onStreamError = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onStreamError']>[1]) =>\n this.fanOut('onStreamError', ctx, info)\n onSettlementError = (ctx: RequestContext, info: Parameters<Required<GatewayObserver>['onSettlementError']>[1]) =>\n this.fanOut('onSettlementError', ctx, info)\n}\n\n/**\n * Generate a request-id. Crypto-random 16 bytes, hex-encoded with an `req_` prefix.\n * Works in Workers, Node, and browsers — all have globalThis.crypto.\n */\nexport function generateRequestId(): string {\n const bytes = new Uint8Array(16)\n globalThis.crypto.getRandomValues(bytes)\n const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('')\n return `req_${hex}`\n}\n"],"mappings":"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","names":[]}
|
|
@@ -26,8 +26,28 @@ var MemoryNonceStore = class {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
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
|
+
};
|
|
29
48
|
|
|
30
49
|
export {
|
|
31
|
-
MemoryNonceStore
|
|
50
|
+
MemoryNonceStore,
|
|
51
|
+
KvNonceStore
|
|
32
52
|
};
|
|
33
|
-
//# sourceMappingURL=chunk-
|
|
53
|
+
//# sourceMappingURL=chunk-M7ZJAK4K.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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":[]}
|
|
@@ -23,6 +23,31 @@ var MemoryRateLimitStore = class {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
|
+
var KvRateLimitStore = class {
|
|
27
|
+
constructor(kv, prefix = "rl") {
|
|
28
|
+
this.kv = kv;
|
|
29
|
+
this.prefix = prefix;
|
|
30
|
+
}
|
|
31
|
+
kv;
|
|
32
|
+
prefix;
|
|
33
|
+
async get(key) {
|
|
34
|
+
const raw = await this.kv.get(this.key(key));
|
|
35
|
+
if (!raw) return [];
|
|
36
|
+
try {
|
|
37
|
+
const arr = JSON.parse(raw);
|
|
38
|
+
return Array.isArray(arr) ? arr.filter((t) => typeof t === "number") : [];
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async set(key, timestamps, ttlSeconds) {
|
|
44
|
+
const ttl = Math.max(ttlSeconds, 60);
|
|
45
|
+
await this.kv.put(this.key(key), JSON.stringify(timestamps), { expirationTtl: ttl });
|
|
46
|
+
}
|
|
47
|
+
key(key) {
|
|
48
|
+
return `${this.prefix}:${key}`;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
26
51
|
async function checkRateLimit(consumerId, config, store) {
|
|
27
52
|
const now = Date.now();
|
|
28
53
|
const windowMs = config.windowSeconds * 1e3;
|
|
@@ -50,6 +75,7 @@ async function checkRateLimit(consumerId, config, store) {
|
|
|
50
75
|
|
|
51
76
|
export {
|
|
52
77
|
MemoryRateLimitStore,
|
|
78
|
+
KvRateLimitStore,
|
|
53
79
|
checkRateLimit
|
|
54
80
|
};
|
|
55
|
-
//# sourceMappingURL=chunk-
|
|
81
|
+
//# sourceMappingURL=chunk-XCTXHZ76.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rate-limit.ts"],"sourcesContent":["/**\n * Sliding-window rate limiter.\n *\n * Two implementations:\n * - MemoryRateLimitStore — single-worker, ephemeral, good for tests\n * - KvRateLimitStore — Cloudflare Workers KV, distributed\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// ---------------------------------------------------------------------------\n// In-memory implementation\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\n// ---------------------------------------------------------------------------\n// Cloudflare KV implementation\n// ---------------------------------------------------------------------------\n\n/** Minimal KV shape — see nonce-store.ts for rationale. */\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 RateLimitStore for distributed Cloudflare Workers deployments.\n *\n * Stores timestamp arrays per consumer. Reads are O(1); writes replace the\n * full array (cap is already filtered by checkRateLimit before write).\n *\n * Consistency note: Workers KV is eventually consistent within ~60s. An\n * attacker sitting on two isolates could technically exceed the limit by\n * ~2x for that window. For payment rate limits this is acceptable; for\n * abuse prevention on free endpoints consider Durable Objects instead.\n */\nexport class KvRateLimitStore implements RateLimitStore {\n constructor(\n private readonly kv: KVNamespace,\n private readonly prefix: string = 'rl',\n ) {}\n\n async get(key: string): Promise<number[]> {\n const raw = await this.kv.get(this.key(key))\n if (!raw) return []\n try {\n const arr = JSON.parse(raw) as unknown\n return Array.isArray(arr) ? (arr as number[]).filter((t) => typeof t === 'number') : []\n } catch {\n return []\n }\n }\n\n async set(key: string, timestamps: number[], 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(key), JSON.stringify(timestamps), { expirationTtl: ttl })\n }\n\n private key(key: string): string {\n return `${this.prefix}:${key}`\n }\n}\n\n// ---------------------------------------------------------------------------\n// Core limiter\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":";AAkCO,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;AAwBO,IAAM,mBAAN,MAAiD;AAAA,EACtD,YACmB,IACA,SAAiB,MAClC;AAFiB;AACA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA,EAGnB,MAAM,IAAI,KAAgC;AACxC,UAAM,MAAM,MAAM,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,CAAC;AAC3C,QAAI,CAAC,IAAK,QAAO,CAAC;AAClB,QAAI;AACF,YAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,aAAO,MAAM,QAAQ,GAAG,IAAK,IAAiB,OAAO,CAAC,MAAM,OAAO,MAAM,QAAQ,IAAI,CAAC;AAAA,IACxF,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,KAAa,YAAsB,YAAmC;AAE9E,UAAM,MAAM,KAAK,IAAI,YAAY,EAAE;AACnC,UAAM,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,GAAG,KAAK,UAAU,UAAU,GAAG,EAAE,eAAe,IAAI,CAAC;AAAA,EACrF;AAAA,EAEQ,IAAI,KAAqB;AAC/B,WAAO,GAAG,KAAK,MAAM,IAAI,GAAG;AAAA,EAC9B;AACF;AAMA,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":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { createAgentGateway } from './middleware.js';
|
|
2
|
-
import { ApiKeyInfo, MppConfig, X402Config, ChatMessage } from './types.js';
|
|
3
|
-
export { AgentMeta, ChatCompletionChunk, ChatCompletionRequest, GatewayConfig, GatewayUsageEvent, PaymentMethod, PaymentResult, SandboxBox, SandboxStreamEvent } from './types.js';
|
|
2
|
+
import { A as ApiKeyInfo, M as MppConfig, X as X402Config, C as ChatMessage } from './types-14xV8J4G.js';
|
|
3
|
+
export { a as AgentMeta, b as AuthFailureReason, c as ChatCompletionChunk, d as ChatCompletionRequest, e as CompositeObserver, f as ConsoleObserver, G as GatewayConfig, g as GatewayObserver, h as GatewayUsageEvent, P as PaymentMethod, i as PaymentResult, R as RequestContext, S as SandboxBox, j as SandboxStreamEvent, k as generateRequestId } from './types-14xV8J4G.js';
|
|
4
4
|
import { NonceStore } from './nonce-store.js';
|
|
5
|
-
export { MemoryNonceStore } from './nonce-store.js';
|
|
6
|
-
export { MemoryRateLimitStore, RateLimitConfig, RateLimitResult, RateLimitStore, checkRateLimit } from './rate-limit.js';
|
|
5
|
+
export { KvNonceStore, MemoryNonceStore } from './nonce-store.js';
|
|
6
|
+
export { KvRateLimitStore, MemoryRateLimitStore, RateLimitConfig, RateLimitResult, RateLimitStore, checkRateLimit } from './rate-limit.js';
|
|
7
7
|
export { ApiKey, ApiKeyCreateRequest, ApiKeyRoutesConfig, ApiKeyStore, createApiKeyRoutes, verifyApiKeyFromStore } from './api-keys.js';
|
|
8
8
|
export { PublishRequest, PublishRoutesConfig, PublishStore, PublishedConfig, createPublishRoutes } from './publish.js';
|
|
9
9
|
import 'hono/types';
|
package/dist/index.js
CHANGED
|
@@ -3,26 +3,35 @@ import {
|
|
|
3
3
|
verifyApiKeyFromStore
|
|
4
4
|
} from "./chunk-5O75YDQP.js";
|
|
5
5
|
import {
|
|
6
|
+
CompositeObserver,
|
|
7
|
+
ConsoleObserver,
|
|
6
8
|
createAgentGateway,
|
|
7
9
|
defaultVerifyApiKey,
|
|
8
10
|
detectInjection,
|
|
9
11
|
filterConsumerMessages,
|
|
10
12
|
filterConsumerMessagesStrict,
|
|
13
|
+
generateRequestId,
|
|
11
14
|
redactSystemPromptFromOutput,
|
|
12
15
|
verifyMpp,
|
|
13
16
|
verifyX402
|
|
14
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-4DUR2B47.js";
|
|
15
18
|
import {
|
|
19
|
+
KvNonceStore,
|
|
16
20
|
MemoryNonceStore
|
|
17
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-M7ZJAK4K.js";
|
|
18
22
|
import {
|
|
19
23
|
createPublishRoutes
|
|
20
24
|
} from "./chunk-5ZJPIIIV.js";
|
|
21
25
|
import {
|
|
26
|
+
KvRateLimitStore,
|
|
22
27
|
MemoryRateLimitStore,
|
|
23
28
|
checkRateLimit
|
|
24
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-XCTXHZ76.js";
|
|
25
30
|
export {
|
|
31
|
+
CompositeObserver,
|
|
32
|
+
ConsoleObserver,
|
|
33
|
+
KvNonceStore,
|
|
34
|
+
KvRateLimitStore,
|
|
26
35
|
MemoryNonceStore,
|
|
27
36
|
MemoryRateLimitStore,
|
|
28
37
|
checkRateLimit,
|
|
@@ -33,6 +42,7 @@ export {
|
|
|
33
42
|
detectInjection,
|
|
34
43
|
filterConsumerMessages,
|
|
35
44
|
filterConsumerMessagesStrict,
|
|
45
|
+
generateRequestId,
|
|
36
46
|
redactSystemPromptFromOutput,
|
|
37
47
|
verifyApiKeyFromStore,
|
|
38
48
|
verifyMpp,
|
package/dist/middleware.d.ts
CHANGED
package/dist/middleware.js
CHANGED
package/dist/nonce-store.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ interface NonceStore {
|
|
|
8
8
|
/** Mark nonce as used. TTL = how long to remember it (seconds). */
|
|
9
9
|
markSeen(nonce: string, ttlSeconds: number): Promise<void>;
|
|
10
10
|
}
|
|
11
|
-
/** In-memory nonce store with automatic eviction */
|
|
11
|
+
/** In-memory nonce store with automatic eviction. Use in tests or single-worker deploys. */
|
|
12
12
|
declare class MemoryNonceStore implements NonceStore {
|
|
13
13
|
private seen;
|
|
14
14
|
private lastEviction;
|
|
@@ -16,5 +16,47 @@ declare class MemoryNonceStore implements NonceStore {
|
|
|
16
16
|
markSeen(nonce: string, ttlSeconds: number): Promise<void>;
|
|
17
17
|
private evictExpired;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Minimal KVNamespace shape — matches Cloudflare Workers' @cloudflare/workers-types
|
|
21
|
+
* without pulling that package as a dep. Production consumers cast their KV
|
|
22
|
+
* binding to this interface at the construction site.
|
|
23
|
+
*/
|
|
24
|
+
interface KVNamespace {
|
|
25
|
+
get(key: string, options?: {
|
|
26
|
+
type?: 'text' | 'json';
|
|
27
|
+
}): Promise<string | null>;
|
|
28
|
+
put(key: string, value: string, options?: {
|
|
29
|
+
expirationTtl?: number;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
delete(key: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* KV-backed NonceStore for distributed Cloudflare Workers deployments.
|
|
35
|
+
*
|
|
36
|
+
* Why this exists: MemoryNonceStore works on a single worker instance, but
|
|
37
|
+
* Cloudflare routes requests across multiple isolates. Without shared state,
|
|
38
|
+
* an attacker could retry a replayed nonce against a different isolate and
|
|
39
|
+
* have it accepted. This implementation uses Workers KV with native TTL so
|
|
40
|
+
* the nonce automatically expires at payment-expiry time.
|
|
41
|
+
*
|
|
42
|
+
* TTL precision: KV is eventually consistent (propagation ~60s). For x402
|
|
43
|
+
* with 10-minute expiry windows this is fine — by the time KV propagates,
|
|
44
|
+
* the payment itself would be expired anyway.
|
|
45
|
+
*
|
|
46
|
+
* Usage:
|
|
47
|
+
* const nonceStore = new KvNonceStore(env.NONCE_KV, 'x402')
|
|
48
|
+
* createAgentGateway({ ...config, nonceStore })
|
|
49
|
+
*/
|
|
50
|
+
declare class KvNonceStore implements NonceStore {
|
|
51
|
+
private readonly kv;
|
|
52
|
+
/** Key prefix to namespace within a shared KV (default: "nonce"). */
|
|
53
|
+
private readonly prefix;
|
|
54
|
+
constructor(kv: KVNamespace,
|
|
55
|
+
/** Key prefix to namespace within a shared KV (default: "nonce"). */
|
|
56
|
+
prefix?: string);
|
|
57
|
+
hasSeen(nonce: string): Promise<boolean>;
|
|
58
|
+
markSeen(nonce: string, ttlSeconds: number): Promise<void>;
|
|
59
|
+
private key;
|
|
60
|
+
}
|
|
19
61
|
|
|
20
|
-
export { MemoryNonceStore, type NonceStore };
|
|
62
|
+
export { type KVNamespace, KvNonceStore, MemoryNonceStore, type NonceStore };
|