@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
|
@@ -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";
|
|
@@ -80,15 +80,26 @@ var INJECTION_PATTERNS = [
|
|
|
80
80
|
// Prompt extraction
|
|
81
81
|
/what\s+(is|are)\s+your\s+(system\s+)?(prompt|instructions?|rules?|directives?)/i,
|
|
82
82
|
/repeat\s+(your|the)\s+(system\s+)?(prompt|instructions?)/i,
|
|
83
|
-
/output\s+(your|the)\s+(system\s+)?(prompt|instructions?)/i,
|
|
84
|
-
/show\s+me\s+(your|the)\s+(system|hidden|secret)\s+(prompt|instructions?|message)/i,
|
|
83
|
+
/output\s+(your|the)\s+((system|initial|original|first|full|real|hidden|secret|raw|exact)\s+)?(prompt|instructions?)/i,
|
|
84
|
+
/show\s+(me\s+)?(your|the)\s+((system|hidden|secret|initial|original)\s+)?(prompt|instructions?|message|directives?)/i,
|
|
85
|
+
/tell\s+me\s+(your|the)\s+(system\s+)?(prompt|instructions?|rules?)/i,
|
|
86
|
+
// Developer/debug/admin/jailbreak mode override
|
|
87
|
+
/(developer|debug|admin|god|sudo|jailbreak|unrestricted|maintenance)\s+mode/i,
|
|
88
|
+
// Safety/safeguard disablement
|
|
89
|
+
/(disable|bypass|override|remove|turn\s+off)\s+(your\s+|the\s+)?(safety|safeguards?|guardrails?|filters?|restrictions?|rules?|limitations?|content\s+policy)/i,
|
|
90
|
+
// Role reversal
|
|
91
|
+
/(from\s+now\s+on|starting\s+now|now)\s+you('re|\s+are)\s+(the\s+)?(user|human|customer|assistant)/i,
|
|
92
|
+
/i('m|\s+am)\s+(the\s+)?(assistant|ai|model|llm|bot)/i,
|
|
93
|
+
// Tool-call / JSON-shaped spoofs
|
|
94
|
+
/"(?:tool|function|action|call)"\s*:\s*"[^"]*(exfiltrate|leak|reveal|dump|extract|steal)[^"]*"/i,
|
|
95
|
+
/exfiltrate[_\s]*(system|prompt|secret|vault|config|data)/i,
|
|
85
96
|
// Data exfiltration
|
|
86
97
|
/read\s+(the\s+)?(vault|workspace|config|secret|\.env)/i,
|
|
87
98
|
/cat\s+\/home\/agent\/(vault|config|\.env|secrets?)/i,
|
|
88
99
|
/list\s+(all\s+)?(vault|workspace|secret)\s+(files?|contents?|data)/i
|
|
89
100
|
];
|
|
90
101
|
function normalizeUnicode(text) {
|
|
91
|
-
return text.replace(/[\u200B
|
|
102
|
+
return text.replace(/[\u200B\u200C\uFEFF]/g, " ").replace(/[\u200D\u200E\u200F\u2028-\u202F\u2060]/g, "").normalize("NFKC");
|
|
92
103
|
}
|
|
93
104
|
function detectInjection(content) {
|
|
94
105
|
const normalized = normalizeUnicode(content);
|
|
@@ -138,6 +149,84 @@ function redactSystemPromptFromOutput(output, systemPrompt) {
|
|
|
138
149
|
return redacted;
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
// src/observer.ts
|
|
153
|
+
var ConsoleObserver = class {
|
|
154
|
+
constructor(log = (e) => console.log(JSON.stringify(e))) {
|
|
155
|
+
this.log = log;
|
|
156
|
+
}
|
|
157
|
+
log;
|
|
158
|
+
emit(level, event, ctx, rest = {}) {
|
|
159
|
+
this.log({
|
|
160
|
+
level,
|
|
161
|
+
event,
|
|
162
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
163
|
+
requestId: ctx.requestId,
|
|
164
|
+
agentSlug: ctx.agentSlug,
|
|
165
|
+
durationMs: Date.now() - ctx.startMs,
|
|
166
|
+
...rest
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
onRequestStart(ctx) {
|
|
170
|
+
this.emit("info", "gateway.request.start", ctx);
|
|
171
|
+
}
|
|
172
|
+
onPaymentVerified(ctx, info) {
|
|
173
|
+
this.emit("info", "gateway.payment.verified", ctx, info);
|
|
174
|
+
}
|
|
175
|
+
onAuthFailure(ctx, reason) {
|
|
176
|
+
this.emit("warn", "gateway.auth.failure", ctx, reason);
|
|
177
|
+
}
|
|
178
|
+
onRateLimited(ctx, info) {
|
|
179
|
+
this.emit("warn", "gateway.rate_limit", ctx, info);
|
|
180
|
+
}
|
|
181
|
+
onBodyTooLarge(ctx, contentLength) {
|
|
182
|
+
this.emit("warn", "gateway.body_too_large", ctx, { contentLength });
|
|
183
|
+
}
|
|
184
|
+
onInjectionDetected(ctx, info) {
|
|
185
|
+
this.emit("warn", "gateway.injection", ctx, info);
|
|
186
|
+
}
|
|
187
|
+
onRequestComplete(ctx, usage) {
|
|
188
|
+
this.emit("info", "gateway.request.complete", ctx, usage);
|
|
189
|
+
}
|
|
190
|
+
onStreamError(ctx, info) {
|
|
191
|
+
this.emit("error", "gateway.stream.error", ctx, info);
|
|
192
|
+
}
|
|
193
|
+
onSettlementError(ctx, info) {
|
|
194
|
+
this.emit("error", "gateway.settlement.error", ctx, info);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
var CompositeObserver = class {
|
|
198
|
+
constructor(observers) {
|
|
199
|
+
this.observers = observers;
|
|
200
|
+
}
|
|
201
|
+
observers;
|
|
202
|
+
async fanOut(event, ...args) {
|
|
203
|
+
for (const obs of this.observers) {
|
|
204
|
+
const fn = obs[event];
|
|
205
|
+
if (!fn) continue;
|
|
206
|
+
try {
|
|
207
|
+
await fn.apply(obs, args);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.warn(`[agent-gateway] observer ${event} threw:`, err instanceof Error ? err.message : err);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
onRequestStart = (ctx) => this.fanOut("onRequestStart", ctx);
|
|
214
|
+
onPaymentVerified = (ctx, info) => this.fanOut("onPaymentVerified", ctx, info);
|
|
215
|
+
onAuthFailure = (ctx, reason) => this.fanOut("onAuthFailure", ctx, reason);
|
|
216
|
+
onRateLimited = (ctx, info) => this.fanOut("onRateLimited", ctx, info);
|
|
217
|
+
onBodyTooLarge = (ctx, contentLength) => this.fanOut("onBodyTooLarge", ctx, contentLength);
|
|
218
|
+
onInjectionDetected = (ctx, info) => this.fanOut("onInjectionDetected", ctx, info);
|
|
219
|
+
onRequestComplete = (ctx, usage) => this.fanOut("onRequestComplete", ctx, usage);
|
|
220
|
+
onStreamError = (ctx, info) => this.fanOut("onStreamError", ctx, info);
|
|
221
|
+
onSettlementError = (ctx, info) => this.fanOut("onSettlementError", ctx, info);
|
|
222
|
+
};
|
|
223
|
+
function generateRequestId() {
|
|
224
|
+
const bytes = new Uint8Array(16);
|
|
225
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
226
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
227
|
+
return `req_${hex}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
141
230
|
// src/middleware.ts
|
|
142
231
|
function createAgentGateway(config) {
|
|
143
232
|
const gw = new Hono();
|
|
@@ -146,6 +235,7 @@ function createAgentGateway(config) {
|
|
|
146
235
|
const globalRateLimit = config.rateLimit ?? { limit: 60, windowSeconds: 60 };
|
|
147
236
|
const nonceStore = config.nonceStore ?? new MemoryNonceStore();
|
|
148
237
|
const requiredScope = config.requiredScope ?? "chat";
|
|
238
|
+
const obs = config.observer;
|
|
149
239
|
gw.get("/:slug/chat/completions", async (c) => {
|
|
150
240
|
const slug = c.req.param("slug");
|
|
151
241
|
const agent = await config.resolveAgent(slug);
|
|
@@ -185,12 +275,16 @@ function createAgentGateway(config) {
|
|
|
185
275
|
gw.post("/:slug/chat/completions", async (c) => {
|
|
186
276
|
const slug = c.req.param("slug");
|
|
187
277
|
const startMs = Date.now();
|
|
278
|
+
const requestId = generateRequestId();
|
|
279
|
+
const ctx = { requestId, agentSlug: slug, startMs };
|
|
280
|
+
await obs?.onRequestStart?.(ctx);
|
|
188
281
|
const agent = await config.resolveAgent(slug);
|
|
189
282
|
if (!agent) {
|
|
190
283
|
return c.json({ error: { message: "Agent not found", type: "not_found" } }, 404);
|
|
191
284
|
}
|
|
192
285
|
const contentLength = parseInt(c.req.header("Content-Length") ?? "0", 10);
|
|
193
286
|
if (contentLength > 65536) {
|
|
287
|
+
await obs?.onBodyTooLarge?.(ctx, contentLength);
|
|
194
288
|
return c.json(
|
|
195
289
|
{ error: { message: "Request body too large (max 64KB)", type: "invalid_request" } },
|
|
196
290
|
413
|
|
@@ -213,9 +307,10 @@ function createAgentGateway(config) {
|
|
|
213
307
|
if (spendAuthHeader) {
|
|
214
308
|
const signer = await verifyX402(spendAuthHeader, config.x402, nonceStore);
|
|
215
309
|
if (!signer) {
|
|
310
|
+
await obs?.onAuthFailure?.(ctx, { method: "x402", code: "invalid_spend_auth", httpStatus: 402 });
|
|
216
311
|
return c.json(
|
|
217
312
|
{ error: { message: "Invalid X-Payment-Signature", type: "authentication_error", code: "invalid_spend_auth" } },
|
|
218
|
-
{ status: 402, headers: { "X-Payment-Required": "spendauth" } }
|
|
313
|
+
{ status: 402, headers: { "X-Payment-Required": "spendauth", "X-Request-Id": requestId } }
|
|
219
314
|
);
|
|
220
315
|
}
|
|
221
316
|
consumerId = signer;
|
|
@@ -225,9 +320,10 @@ function createAgentGateway(config) {
|
|
|
225
320
|
if (!signer) {
|
|
226
321
|
const realm = config.mpp.realm;
|
|
227
322
|
const method = config.mpp.method ?? "blueprintevm";
|
|
323
|
+
await obs?.onAuthFailure?.(ctx, { method: "mpp", code: "invalid_mpp_credential", httpStatus: 401 });
|
|
228
324
|
return c.json(
|
|
229
325
|
{ error: { message: "Invalid Payment credential", type: "authentication_error", code: "invalid_mpp_credential" } },
|
|
230
|
-
{ status: 401, headers: { "WWW-Authenticate": `Payment realm="${realm}", method="${method}"
|
|
326
|
+
{ status: 401, headers: { "WWW-Authenticate": `Payment realm="${realm}", method="${method}"`, "X-Request-Id": requestId } }
|
|
231
327
|
);
|
|
232
328
|
}
|
|
233
329
|
consumerId = signer;
|
|
@@ -236,22 +332,31 @@ function createAgentGateway(config) {
|
|
|
236
332
|
const verify = config.verifyApiKey ?? defaultVerifyApiKey;
|
|
237
333
|
const key = await verify(authHeader);
|
|
238
334
|
if (!key) {
|
|
239
|
-
|
|
335
|
+
await obs?.onAuthFailure?.(ctx, { method: "apikey", code: "invalid_api_key", httpStatus: 401 });
|
|
336
|
+
return c.json(
|
|
337
|
+
{ error: { message: "Invalid API key", type: "authentication_error" } },
|
|
338
|
+
{ status: 401, headers: { "X-Request-Id": requestId } }
|
|
339
|
+
);
|
|
240
340
|
}
|
|
241
341
|
if (key.scopes && key.scopes.length > 0 && !key.scopes.includes(requiredScope)) {
|
|
342
|
+
await obs?.onAuthFailure?.(ctx, { method: "apikey", code: "insufficient_scope", httpStatus: 403 });
|
|
242
343
|
return c.json(
|
|
243
344
|
{ error: { message: `API key missing required scope: ${requiredScope}`, type: "forbidden", code: "insufficient_scope" } },
|
|
244
|
-
403
|
|
345
|
+
{ status: 403, headers: { "X-Request-Id": requestId } }
|
|
245
346
|
);
|
|
246
347
|
}
|
|
247
348
|
consumerId = key.consumerId;
|
|
248
349
|
paymentMethod = "apikey";
|
|
249
350
|
keyInfo = key;
|
|
250
351
|
} else {
|
|
352
|
+
await obs?.onAuthFailure?.(ctx, { method: "none", code: "payment_required", httpStatus: 402 });
|
|
251
353
|
const methods = ["x402"];
|
|
252
354
|
if (config.mpp) methods.push("mpp");
|
|
253
355
|
methods.push("api_key");
|
|
254
|
-
const headers = {
|
|
356
|
+
const headers = {
|
|
357
|
+
"X-Payment-Required": methods.join(", "),
|
|
358
|
+
"X-Request-Id": requestId
|
|
359
|
+
};
|
|
255
360
|
if (config.mpp) {
|
|
256
361
|
headers["WWW-Authenticate"] = `Payment realm="${config.mpp.realm}", method="${config.mpp.method ?? "blueprintevm"}"`;
|
|
257
362
|
}
|
|
@@ -275,21 +380,27 @@ function createAgentGateway(config) {
|
|
|
275
380
|
}
|
|
276
381
|
}, { status: 402, headers });
|
|
277
382
|
}
|
|
383
|
+
await obs?.onPaymentVerified?.(ctx, { method: paymentMethod, consumerId, keyId: keyInfo?.keyId });
|
|
278
384
|
const effectiveRateLimit = keyInfo?.rateLimitPerMinute ? { limit: keyInfo.rateLimitPerMinute, windowSeconds: 60 } : globalRateLimit;
|
|
279
385
|
const rl = await checkRateLimit(consumerId, effectiveRateLimit, rateLimitStore);
|
|
280
386
|
if (!rl.allowed) {
|
|
387
|
+
await obs?.onRateLimited?.(ctx, { consumerId, retryAfterSeconds: rl.retryAfterSeconds ?? 60 });
|
|
281
388
|
return c.json(
|
|
282
389
|
{ error: { message: "Rate limit exceeded", type: "rate_limit_error", retry_after: rl.retryAfterSeconds } },
|
|
283
|
-
{ status: 429, headers: { "Retry-After": String(rl.retryAfterSeconds ?? 60) } }
|
|
390
|
+
{ status: 429, headers: { "Retry-After": String(rl.retryAfterSeconds ?? 60), "X-Request-Id": requestId } }
|
|
284
391
|
);
|
|
285
392
|
}
|
|
286
393
|
const { messages: filtered, injectionWarnings } = filterConsumerMessagesStrict(body.messages, maxLen);
|
|
287
394
|
if (injectionWarnings.length > 0) {
|
|
288
|
-
|
|
395
|
+
await obs?.onInjectionDetected?.(ctx, {
|
|
396
|
+
consumerId,
|
|
397
|
+
patterns: injectionWarnings,
|
|
398
|
+
blocked: !!config.blockInjection
|
|
399
|
+
});
|
|
289
400
|
if (config.blockInjection) {
|
|
290
401
|
return c.json(
|
|
291
402
|
{ error: { message: "Request rejected: potential prompt injection detected", type: "content_policy_violation" } },
|
|
292
|
-
400
|
|
403
|
+
{ status: 400, headers: { "X-Request-Id": requestId } }
|
|
293
404
|
);
|
|
294
405
|
}
|
|
295
406
|
}
|
|
@@ -297,7 +408,7 @@ function createAgentGateway(config) {
|
|
|
297
408
|
if (!userMessage) {
|
|
298
409
|
return c.json({ error: { message: "No user message provided", type: "invalid_request" } }, 400);
|
|
299
410
|
}
|
|
300
|
-
|
|
411
|
+
const inputTokens = Math.ceil(userMessage.length / 4);
|
|
301
412
|
let outputTokens = 0;
|
|
302
413
|
const stream = new ReadableStream({
|
|
303
414
|
async start(controller) {
|
|
@@ -341,7 +452,7 @@ function createAgentGateway(config) {
|
|
|
341
452
|
const totalCost = (inputTokens + outputTokens) * agent.pricePerTokenUsd;
|
|
342
453
|
const ownerEarned = totalCost * (1 - agent.platformFeePercent);
|
|
343
454
|
const platformFee = totalCost * agent.platformFeePercent;
|
|
344
|
-
|
|
455
|
+
const usageEvent = {
|
|
345
456
|
agentId: agent.id,
|
|
346
457
|
agentSlug: agent.slug,
|
|
347
458
|
consumerId,
|
|
@@ -352,14 +463,20 @@ function createAgentGateway(config) {
|
|
|
352
463
|
ownerEarnedUsd: ownerEarned,
|
|
353
464
|
platformFeeUsd: platformFee,
|
|
354
465
|
durationMs: Date.now() - startMs
|
|
355
|
-
}
|
|
466
|
+
};
|
|
467
|
+
await config.recordUsage(usageEvent);
|
|
468
|
+
await obs?.onRequestComplete?.(ctx, usageEvent);
|
|
356
469
|
if (config.settlePayment) {
|
|
357
|
-
await config.settlePayment({ method: paymentMethod, consumerId }, totalCost).catch((err) => {
|
|
358
|
-
|
|
470
|
+
await config.settlePayment({ method: paymentMethod, consumerId }, totalCost).catch(async (err) => {
|
|
471
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
472
|
+
console.error(`[agent-gateway] settlement failed for ${consumerId}: ${msg}`);
|
|
473
|
+
await obs?.onSettlementError?.(ctx, { consumerId, method: paymentMethod, errorMessage: msg });
|
|
359
474
|
});
|
|
360
475
|
}
|
|
361
476
|
} catch (err) {
|
|
362
|
-
const
|
|
477
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
478
|
+
const safeMessage = rawMessage.includes("/") || rawMessage.includes("\\") ? "Internal agent error" : rawMessage;
|
|
479
|
+
await obs?.onStreamError?.(ctx, { consumerId, errorMessage: rawMessage });
|
|
363
480
|
controller.enqueue(
|
|
364
481
|
encoder.encode(`data: ${JSON.stringify({ error: { message: safeMessage, type: "server_error" } })}
|
|
365
482
|
|
|
@@ -374,6 +491,7 @@ function createAgentGateway(config) {
|
|
|
374
491
|
headers: {
|
|
375
492
|
"Content-Type": "text/event-stream",
|
|
376
493
|
"Cache-Control": "no-cache",
|
|
494
|
+
"X-Request-Id": requestId,
|
|
377
495
|
"X-Agent-Slug": agent.slug,
|
|
378
496
|
"X-Agent-Hosting": agent.sandboxEndpoint ? "sovereign" : "centralized",
|
|
379
497
|
"X-Payment-Method": paymentMethod,
|
|
@@ -393,6 +511,9 @@ export {
|
|
|
393
511
|
filterConsumerMessages,
|
|
394
512
|
filterConsumerMessagesStrict,
|
|
395
513
|
redactSystemPromptFromOutput,
|
|
514
|
+
ConsoleObserver,
|
|
515
|
+
CompositeObserver,
|
|
516
|
+
generateRequestId,
|
|
396
517
|
createAgentGateway
|
|
397
518
|
};
|
|
398
|
-
//# sourceMappingURL=chunk-
|
|
519
|
+
//# sourceMappingURL=chunk-FI5HMLGY.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|initial|original|first|full|real|hidden|secret|raw|exact)\\s+)?(prompt|instructions?)/i,\n /show\\s+(me\\s+)?(your|the)\\s+((system|hidden|secret|initial|original)\\s+)?(prompt|instructions?|message|directives?)/i,\n /tell\\s+me\\s+(your|the)\\s+(system\\s+)?(prompt|instructions?|rules?)/i,\n // Developer/debug/admin/jailbreak mode override\n /(developer|debug|admin|god|sudo|jailbreak|unrestricted|maintenance)\\s+mode/i,\n // Safety/safeguard disablement\n /(disable|bypass|override|remove|turn\\s+off)\\s+(your\\s+|the\\s+)?(safety|safeguards?|guardrails?|filters?|restrictions?|rules?|limitations?|content\\s+policy)/i,\n // Role reversal\n /(from\\s+now\\s+on|starting\\s+now|now)\\s+you('re|\\s+are)\\s+(the\\s+)?(user|human|customer|assistant)/i,\n /i('m|\\s+am)\\s+(the\\s+)?(assistant|ai|model|llm|bot)/i,\n // Tool-call / JSON-shaped spoofs\n /\"(?:tool|function|action|call)\"\\s*:\\s*\"[^\"]*(exfiltrate|leak|reveal|dump|extract|steal)[^\"]*\"/i,\n /exfiltrate[_\\s]*(system|prompt|secret|vault|config|data)/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.\n//\n// Attacks insert zero-width chars between every word so `\\s+`-anchored\n// regexes don't fire. Stripping entirely leaves attack text as one glued\n// token which ALSO dodges `\\s+`. Split the handling: U+200B ZWSP,\n// U+200C ZWNJ, U+FEFF ZWNBSP are commonly used in attacks and get\n// replaced with a real SPACE so regexes see tokenized text. U+200D ZWJ\n// + directional marks are legitimately used in scripts like Hindi and\n// Arabic; strip without replacement so we don't spuriously split\n// legitimate words. NFKC collapses homoglyphs (Cyrillic a → Latin a, fullwidth → ascii).\nfunction normalizeUnicode(text: string): string {\n return text\n .replace(/[\\u200B\\u200C\\uFEFF]/g, ' ')\n .replace(/[\\u200D\\u200E\\u200F\\u2028-\\u202F\\u2060]/g, '')\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-FI5HMLGY.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