@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 +27 -6
- package/dist/{chunk-3IKQWFKX.js → chunk-Q4YAIEZY.js} +92 -32
- package/dist/chunk-Q4YAIEZY.js.map +1 -0
- package/dist/index.d.ts +13 -9
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.js +1 -1
- package/dist/{types-CX2V06cN.d.ts → types-DEsMmS-X.d.ts} +15 -2
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/a2a/agent-card.ts +4 -3
- package/src/a2a/handler.ts +1 -2
- package/src/dispatch.ts +37 -16
- package/src/index.ts +7 -1
- package/src/middleware.ts +6 -6
- package/src/types.ts +15 -2
- package/src/verify.ts +93 -26
- package/dist/chunk-3IKQWFKX.js.map +0 -1
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.
|
|
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 {
|
|
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.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
455
|
-
|
|
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
|
|
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
|
|
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-
|
|
1763
|
+
//# sourceMappingURL=chunk-Q4YAIEZY.js.map
|