@tangle-network/agent-gateway 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @tangle-network/agent-gateway
2
2
 
3
- Hono middleware that turns any Tangle agent app into a paid API. Wrap your chat endpoint to accept API keys, x402 SpendAuth, or MPP credentials — with scope enforcement, per-key rate limits, nonce replay protection, prompt-injection detection, and publish routes for the marketplace.
3
+ Hono middleware that turns any Tangle agent app into a paid API.
4
+ It exposes one shared request pipeline for API keys, x402 SpendAuth, and MPP credentials, with scope enforcement, per-key rate limits, nonce replay protection, prompt-injection detection, and publish routes for the marketplace.
4
5
 
5
6
  ## Install
6
7
 
@@ -11,17 +12,37 @@ npm install @tangle-network/agent-gateway
11
12
  ## Usage
12
13
 
13
14
  ```ts
14
- import { createAgentGateway } from '@tangle-network/agent-gateway'
15
+ import {
16
+ createAgentGateway,
17
+ verifyApiKeyFromStore,
18
+ } from '@tangle-network/agent-gateway'
15
19
  import { Hono } from 'hono'
16
20
 
17
21
  const app = new Hono()
18
- app.use('/chat/*', createAgentGateway({
19
- apiKeyStore: myKeyStore,
20
- x402: { verifierUrl: 'https://router.tangle.tools/x402/verify' },
21
- rateLimits: { perKey: { rpm: 60 } },
22
+ app.route('/v1/agents', createAgentGateway({
23
+ resolveAgent: loadPublishedAgent,
24
+ getSandbox: openAgentSandbox,
25
+ recordUsage: recordUsageEvent,
26
+ x402: {
27
+ operatorAddress: '0x…',
28
+ chainId: 3799,
29
+ verifySigner: verifySpendAuthSignature,
30
+ },
31
+ verifyApiKey: (authHeader) => verifyApiKeyFromStore(authHeader, apiKeyStore),
22
32
  }))
23
33
  ```
24
34
 
35
+ `x402.verifySigner` is required for production.
36
+ Set `x402.demoMode: true` only for local development and tests; that explicit mode also enables the built-in `sk_agent_*` demo key verifier.
37
+
38
+ MPP is method-specific.
39
+ Configure `mpp.verifySigner` for production MPP credentials; it receives the decoded JSON payload when available plus the original decoded credential, and returns the authenticated consumer ID or `null`.
40
+ The default `blueprintevm` method may reuse `x402.verifySigner` when its credential has the compatible x402 payload shape.
41
+ Other methods are not accepted until they have their own verifier.
42
+
43
+ The same authentication, authorization, rate-limit, filtering, sandbox, settlement, and usage-recording pipeline is used by the OpenAI-compatible and A2A endpoints.
44
+ Wire protocol handlers only translate their request and response shapes.
45
+
25
46
  ## A2A protocol
26
47
 
27
48
  The gateway speaks Google's A2A protocol alongside its OpenAI-compatible surface: discovery via `.well-known/agent.json`, JSON-RPC 2.0 dispatch for `message/send`, `message/stream`, `tasks/get`, `tasks/cancel`, `tasks/resubscribe`, and the four `tasks/pushNotificationConfig/*` methods. Long-horizon agents — durable tasks across worker restarts, webhook delivery on terminal state, `input-required` pauses with multi-turn continuation — are documented in [`docs/a2a-long-horizon.md`](./docs/a2a-long-horizon.md).
@@ -260,6 +260,15 @@ function generateRequestId() {
260
260
  }
261
261
 
262
262
  // src/verify.ts
263
+ function isApiKeyAuthEnabled(config) {
264
+ return config.verifyApiKey !== void 0 || config.x402.demoMode === true;
265
+ }
266
+ function isMppAuthEnabled(config) {
267
+ const method = config.mpp?.method ?? "blueprintevm";
268
+ return Boolean(
269
+ config.mpp && (config.mpp.verifySigner !== void 0 || method === "blueprintevm" && config.x402.verifySigner !== void 0 || config.x402.demoMode === true)
270
+ );
271
+ }
263
272
  async function verifyX402(spendAuthHeader, config, nonceStore) {
264
273
  try {
265
274
  const raw = JSON.parse(spendAuthHeader);
@@ -271,36 +280,72 @@ async function verifyX402(spendAuthHeader, config, nonceStore) {
271
280
  if (expiry < BigInt(Math.floor(Date.now() / 1e3))) return null;
272
281
  if (amount <= 0n) return null;
273
282
  const nonceKey = `${raw.commitment}:${nonce.toString()}`;
274
- if (nonceStore) {
275
- if (await nonceStore.hasSeen(nonceKey)) return null;
276
- const ttl = Math.min(Number(expiry) - Math.floor(Date.now() / 1e3), 3600);
277
- await nonceStore.markSeen(nonceKey, Math.max(ttl, 60));
278
- }
283
+ if (nonceStore && await nonceStore.hasSeen(nonceKey)) return null;
279
284
  if (config.verifySigner) {
280
285
  const verified = await config.verifySigner(raw);
281
286
  if (!verified) return null;
282
287
  } else if (!config.demoMode) {
283
288
  return null;
284
289
  }
290
+ if (nonceStore) {
291
+ const ttl = Math.min(Number(expiry) - Math.floor(Date.now() / 1e3), 3600);
292
+ await nonceStore.markSeen(nonceKey, Math.max(ttl, 60));
293
+ }
285
294
  return raw.commitment;
286
295
  } catch {
287
296
  return null;
288
297
  }
289
298
  }
290
- async function verifyMpp(authHeader, _config, x402Config) {
299
+ async function verifyMpp(authHeader, config, x402Config, nonceStore) {
291
300
  const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i);
292
301
  if (!match) return null;
293
- const [, , credentialB64] = match;
302
+ const [, method, credentialB64] = match;
303
+ if (config.method && method !== config.method) return null;
294
304
  try {
305
+ if (!/^[A-Za-z0-9_-]+$/.test(credentialB64)) return null;
295
306
  const decoded = Buffer.from(credentialB64, "base64url").toString("utf-8");
296
- const credential = JSON.parse(decoded);
297
- const payload = credential.payload ?? credential;
298
- if (!payload.commitment && !payload.from) return null;
307
+ let payload = {};
308
+ try {
309
+ const credential = JSON.parse(decoded);
310
+ if (credential && typeof credential === "object" && !Array.isArray(credential)) {
311
+ const nested = credential.payload;
312
+ payload = nested && typeof nested === "object" && !Array.isArray(nested) ? nested : credential;
313
+ }
314
+ } catch {
315
+ }
316
+ const nonceKey = nonceStore && payload.nonce !== void 0 ? `mpp:${method}:${String(payload.commitment ?? payload.from ?? "unknown")}:${String(payload.nonce)}` : null;
317
+ if (nonceKey && await nonceStore.hasSeen(nonceKey)) return null;
318
+ let consumerId = null;
319
+ if (config.verifySigner) {
320
+ consumerId = await config.verifySigner(payload, { method, credential: decoded });
321
+ } else if (method === "blueprintevm" && x402Config.verifySigner && payload.commitment) {
322
+ const verified = await x402Config.verifySigner(payload);
323
+ consumerId = verified ? String(payload.commitment) : null;
324
+ } else if (x402Config.demoMode) {
325
+ const identity = payload.commitment ?? payload.from;
326
+ if (typeof identity !== "string" || identity.length === 0) return null;
327
+ consumerId = identity;
328
+ } else {
329
+ return null;
330
+ }
331
+ if (!consumerId) return null;
299
332
  const operator = payload.operator ?? payload.to;
300
- if (operator && operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) return null;
301
- if (payload.amount) BigInt(payload.amount);
302
- if (payload.nonce) BigInt(payload.nonce);
303
- return payload.commitment ?? payload.from ?? null;
333
+ if (operator !== void 0) {
334
+ if (typeof operator !== "string" || operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) {
335
+ return null;
336
+ }
337
+ }
338
+ if (payload.amount !== void 0 && BigInt(String(payload.amount)) <= 0n) return null;
339
+ if (payload.nonce !== void 0) BigInt(String(payload.nonce));
340
+ if (payload.expiry !== void 0 && BigInt(String(payload.expiry)) < BigInt(Math.floor(Date.now() / 1e3))) {
341
+ return null;
342
+ }
343
+ if (nonceStore && payload.nonce !== void 0) {
344
+ const expiry = payload.expiry === void 0 ? Math.floor(Date.now() / 1e3) + 3600 : Number(payload.expiry);
345
+ const ttl = Math.min(expiry - Math.floor(Date.now() / 1e3), 3600);
346
+ await nonceStore.markSeen(nonceKey, Math.max(ttl, 60));
347
+ }
348
+ return consumerId;
304
349
  } catch {
305
350
  return null;
306
351
  }
@@ -321,7 +366,7 @@ async function authenticateAndGuard(c, slug, messages, config, state) {
321
366
  const ctx = { requestId, agentSlug: slug, startMs };
322
367
  await state.obs?.onRequestStart?.(ctx);
323
368
  const agent = await config.resolveAgent(slug);
324
- if (!agent) {
369
+ if (!agent || !agent.enabled) {
325
370
  return c.json({ error: { message: "Agent not found", type: "not_found" } }, 404);
326
371
  }
327
372
  if (!messages?.length) {
@@ -359,8 +404,8 @@ async function authenticateAndGuard(c, slug, messages, config, state) {
359
404
  }
360
405
  consumerId = signer;
361
406
  paymentMethod = "x402";
362
- } else if (config.mpp && authHeader.toLowerCase().startsWith("payment ")) {
363
- const signer = await verifyMpp(authHeader, config.mpp, config.x402);
407
+ } else if (isMppAuthEnabled(config) && authHeader.toLowerCase().startsWith("payment ")) {
408
+ const signer = await verifyMpp(authHeader, config.mpp, config.x402, state.nonceStore);
364
409
  if (!signer) {
365
410
  const realm = config.mpp.realm;
366
411
  const method = config.mpp.method ?? "blueprintevm";
@@ -389,7 +434,18 @@ async function authenticateAndGuard(c, slug, messages, config, state) {
389
434
  consumerId = signer;
390
435
  paymentMethod = "mpp";
391
436
  } else if (authHeader.startsWith("Bearer ")) {
392
- const verify = config.verifyApiKey ?? defaultVerifyApiKey;
437
+ const verify = config.verifyApiKey ?? (config.x402.demoMode ? defaultVerifyApiKey : null);
438
+ if (!verify || !isApiKeyAuthEnabled(config)) {
439
+ await state.obs?.onAuthFailure?.(ctx, {
440
+ method: "apikey",
441
+ code: "api_keys_not_configured",
442
+ httpStatus: 401
443
+ });
444
+ return c.json(
445
+ { error: { message: "API key authentication is not configured", type: "authentication_error" } },
446
+ { status: 401, headers: { "X-Request-Id": requestId } }
447
+ );
448
+ }
393
449
  const key = await verify(authHeader);
394
450
  if (!key) {
395
451
  await state.obs?.onAuthFailure?.(ctx, {
@@ -429,13 +485,13 @@ async function authenticateAndGuard(c, slug, messages, config, state) {
429
485
  httpStatus: 402
430
486
  });
431
487
  const methods = ["x402"];
432
- if (config.mpp) methods.push("mpp");
433
- methods.push("api_key");
488
+ if (isMppAuthEnabled(config)) methods.push("mpp");
489
+ if (isApiKeyAuthEnabled(config)) methods.push("api_key");
434
490
  const headers = {
435
491
  "X-Payment-Required": methods.join(", "),
436
492
  "X-Request-Id": requestId
437
493
  };
438
- if (config.mpp) {
494
+ if (isMppAuthEnabled(config) && config.mpp) {
439
495
  headers["WWW-Authenticate"] = `Payment realm="${config.mpp.realm}", method="${config.mpp.method ?? "blueprintevm"}"`;
440
496
  }
441
497
  return c.json(
@@ -450,10 +506,12 @@ async function authenticateAndGuard(c, slug, messages, config, state) {
450
506
  credits_address: config.x402.creditsAddress,
451
507
  estimated_amount_per_request: "20000"
452
508
  },
453
- ...config.mpp ? { mpp: { realm: config.mpp.realm, method: config.mpp.method ?? "blueprintevm" } } : {},
454
- api_key: {
455
- purchase_url: config.baseUrl ? `${config.baseUrl}/agents/${slug}/api-keys` : void 0
456
- }
509
+ ...isMppAuthEnabled(config) && config.mpp ? { mpp: { realm: config.mpp.realm, method: config.mpp.method ?? "blueprintevm" } } : {},
510
+ ...isApiKeyAuthEnabled(config) ? {
511
+ api_key: {
512
+ purchase_url: config.baseUrl ? `${config.baseUrl}/agents/${slug}/api-keys` : void 0
513
+ }
514
+ } : {}
457
515
  }
458
516
  },
459
517
  { status: 402, headers }
@@ -631,8 +689,8 @@ function estimateTokens(text) {
631
689
  // src/a2a/agent-card.ts
632
690
  function buildAgentCard(agent, config, agentUrl) {
633
691
  const schemes = ["x402"];
634
- if (config.mpp) schemes.push("mpp");
635
- schemes.push("Bearer");
692
+ if (isMppAuthEnabled(config)) schemes.push("mpp");
693
+ if (isApiKeyAuthEnabled(config)) schemes.push("Bearer");
636
694
  const skills = agent.skills && agent.skills.length > 0 ? agent.skills : [
637
695
  {
638
696
  id: "chat",
@@ -940,7 +998,7 @@ function createA2AHandlers(deps) {
940
998
  const slug = c.req.param("slug");
941
999
  if (!slug) return c.json({ error: "slug required" }, 400);
942
1000
  const agent = await deps.config.resolveAgent(slug);
943
- if (!agent) {
1001
+ if (!agent || !agent.enabled) {
944
1002
  return c.json({ error: "Agent not found or not published" }, 404);
945
1003
  }
946
1004
  const url = new URL(c.req.url);
@@ -1535,7 +1593,7 @@ function createAgentGateway(config) {
1535
1593
  gw.get("/:slug/chat/completions", async (c) => {
1536
1594
  const slug = c.req.param("slug");
1537
1595
  const agent = await config.resolveAgent(slug);
1538
- if (!agent) return c.json({ error: "Agent not found or not published" }, 404);
1596
+ if (!agent || !agent.enabled) return c.json({ error: "Agent not found or not published" }, 404);
1539
1597
  const paymentMethods = [
1540
1598
  {
1541
1599
  type: "x402",
@@ -1544,14 +1602,14 @@ function createAgentGateway(config) {
1544
1602
  credits_contract: config.x402.creditsAddress
1545
1603
  }
1546
1604
  ];
1547
- if (config.mpp) {
1605
+ if (isMppAuthEnabled(config)) {
1548
1606
  paymentMethods.push({
1549
1607
  type: "mpp",
1550
1608
  realm: config.mpp.realm,
1551
1609
  method: config.mpp.method ?? "blueprintevm"
1552
1610
  });
1553
1611
  }
1554
- paymentMethods.push({ type: "api_key", prefix: "sk_agent_" });
1612
+ if (isApiKeyAuthEnabled(config)) paymentMethods.push({ type: "api_key", prefix: "sk_agent_" });
1555
1613
  return c.json({
1556
1614
  slug: agent.slug,
1557
1615
  pricing: {
@@ -1690,6 +1748,8 @@ export {
1690
1748
  ConsoleObserver,
1691
1749
  CompositeObserver,
1692
1750
  generateRequestId,
1751
+ isApiKeyAuthEnabled,
1752
+ isMppAuthEnabled,
1693
1753
  verifyX402,
1694
1754
  verifyMpp,
1695
1755
  defaultVerifyApiKey,
@@ -1700,4 +1760,4 @@ export {
1700
1760
  InMemoryTaskStore,
1701
1761
  createAgentGateway
1702
1762
  };
1703
- //# sourceMappingURL=chunk-3IKQWFKX.js.map
1763
+ //# sourceMappingURL=chunk-Q4YAIEZY.js.map