@tangle-network/agent-gateway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/api-keys.d.ts +63 -0
- package/dist/api-keys.js +9 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/chunk-4FULF5LW.js +55 -0
- package/dist/chunk-4FULF5LW.js.map +1 -0
- package/dist/chunk-5O75YDQP.js +89 -0
- package/dist/chunk-5O75YDQP.js.map +1 -0
- package/dist/chunk-5ZJPIIIV.js +56 -0
- package/dist/chunk-5ZJPIIIV.js.map +1 -0
- package/dist/chunk-EMGS63QE.js +398 -0
- package/dist/chunk-EMGS63QE.js.map +1 -0
- package/dist/chunk-Z22ALGHW.js +33 -0
- package/dist/chunk-Z22ALGHW.js.map +1 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +19 -0
- package/dist/middleware.js +9 -0
- package/dist/middleware.js.map +1 -0
- package/dist/nonce-store.d.ts +20 -0
- package/dist/nonce-store.js +7 -0
- package/dist/nonce-store.js.map +1 -0
- package/dist/publish.d.ts +42 -0
- package/dist/publish.js +7 -0
- package/dist/publish.js.map +1 -0
- package/dist/rate-limit.d.ts +33 -0
- package/dist/rate-limit.js +9 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
- package/src/api-keys.ts +187 -0
- package/src/filter.ts +138 -0
- package/src/index.ts +49 -0
- package/src/middleware.ts +321 -0
- package/src/nonce-store.ts +43 -0
- package/src/publish.ts +116 -0
- package/src/rate-limit.ts +87 -0
- package/src/types.ts +177 -0
- package/src/verify.ts +112 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @tangle-network/agent-gateway
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tangle-network/agent-gateway
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createAgentGateway } from '@tangle-network/agent-gateway'
|
|
15
|
+
import { Hono } from 'hono'
|
|
16
|
+
|
|
17
|
+
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
|
+
}))
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Tier
|
|
26
|
+
|
|
27
|
+
Marketplace tier of the [agent-builder](https://github.com/drewstone/tangle-agent-builder) three-tier architecture (Forge / Workbench / Marketplace). Used by every `*.tangle.tools` agent app that publishes a paid API.
|
|
28
|
+
|
|
29
|
+
## Related
|
|
30
|
+
|
|
31
|
+
- [`@tangle-network/agent-client`](https://github.com/tangle-network/agent-client) — consumer SDK for calling endpoints this gateway fronts
|
|
32
|
+
- [`@tangle-network/agent-eval`](https://github.com/tangle-network/agent-eval) — evaluation framework for agents published behind this gateway
|
|
33
|
+
- [`@tangle-network/tcloud`](https://github.com/tangle-network/tcloud) — consumer SDK for Tangle platform services (router, sandbox, browser)
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as hono_types from 'hono/types';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
|
|
4
|
+
interface ApiKey {
|
|
5
|
+
id: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
name: string;
|
|
8
|
+
keyHash: string;
|
|
9
|
+
keyPrefix: string;
|
|
10
|
+
scopes: string[];
|
|
11
|
+
rateLimit: number;
|
|
12
|
+
dailyLimit: number;
|
|
13
|
+
spendingLimitCents: number | null;
|
|
14
|
+
spentCents: number;
|
|
15
|
+
lastUsedAt: Date | null;
|
|
16
|
+
expiresAt: Date | null;
|
|
17
|
+
createdAt: Date;
|
|
18
|
+
}
|
|
19
|
+
interface ApiKeyCreateRequest {
|
|
20
|
+
name: string;
|
|
21
|
+
scopes?: string[];
|
|
22
|
+
rateLimit?: number;
|
|
23
|
+
dailyLimit?: number;
|
|
24
|
+
spendingLimitCents?: number;
|
|
25
|
+
expiresAt?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Each agent implements this against their DB */
|
|
28
|
+
interface ApiKeyStore {
|
|
29
|
+
create(userId: string, data: {
|
|
30
|
+
name: string;
|
|
31
|
+
keyHash: string;
|
|
32
|
+
keyPrefix: string;
|
|
33
|
+
scopes: string[];
|
|
34
|
+
rateLimit: number;
|
|
35
|
+
dailyLimit: number;
|
|
36
|
+
spendingLimitCents: number | null;
|
|
37
|
+
expiresAt: Date | null;
|
|
38
|
+
}): Promise<ApiKey>;
|
|
39
|
+
list(userId: string): Promise<Omit<ApiKey, 'keyHash'>[]>;
|
|
40
|
+
findByHash(keyHash: string): Promise<ApiKey | null>;
|
|
41
|
+
delete(userId: string, keyId: string): Promise<boolean>;
|
|
42
|
+
recordUsage(keyId: string, costCents: number): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
declare function verifyApiKeyFromStore(authHeader: string, store: ApiKeyStore, prefix?: string): Promise<{
|
|
45
|
+
key: ApiKey;
|
|
46
|
+
keyId: string;
|
|
47
|
+
consumerId: string;
|
|
48
|
+
scopes: string[];
|
|
49
|
+
rateLimitPerMinute: number;
|
|
50
|
+
dailyLimit: number;
|
|
51
|
+
} | null>;
|
|
52
|
+
interface ApiKeyRoutesConfig {
|
|
53
|
+
store: ApiKeyStore;
|
|
54
|
+
/** Get the authenticated user ID from the request. Return null if not authenticated. */
|
|
55
|
+
getAuthUserId: (request: Request) => Promise<string | null>;
|
|
56
|
+
/** Key prefix (default: "ak_") */
|
|
57
|
+
prefix?: string;
|
|
58
|
+
/** Valid scopes for this agent (default: ["chat"]) */
|
|
59
|
+
validScopes?: string[];
|
|
60
|
+
}
|
|
61
|
+
declare function createApiKeyRoutes(config: ApiKeyRoutesConfig): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
|
|
62
|
+
|
|
63
|
+
export { type ApiKey, type ApiKeyCreateRequest, type ApiKeyRoutesConfig, type ApiKeyStore, createApiKeyRoutes, verifyApiKeyFromStore };
|
package/dist/api-keys.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/rate-limit.ts
|
|
2
|
+
var MemoryRateLimitStore = class {
|
|
3
|
+
store = /* @__PURE__ */ new Map();
|
|
4
|
+
lastEviction = Date.now();
|
|
5
|
+
async get(key) {
|
|
6
|
+
this.evictExpired();
|
|
7
|
+
const entry = this.store.get(key);
|
|
8
|
+
if (!entry || entry.expiresAt < Date.now()) {
|
|
9
|
+
this.store.delete(key);
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return entry.timestamps;
|
|
13
|
+
}
|
|
14
|
+
async set(key, timestamps, ttlSeconds) {
|
|
15
|
+
this.store.set(key, { timestamps, expiresAt: Date.now() + ttlSeconds * 1e3 });
|
|
16
|
+
}
|
|
17
|
+
evictExpired() {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
if (now - this.lastEviction < 3e4) return;
|
|
20
|
+
this.lastEviction = now;
|
|
21
|
+
for (const [key, entry] of this.store) {
|
|
22
|
+
if (entry.expiresAt < now) this.store.delete(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
async function checkRateLimit(consumerId, config, store) {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const windowMs = config.windowSeconds * 1e3;
|
|
29
|
+
const cutoff = now - windowMs;
|
|
30
|
+
const key = `rl:${consumerId}`;
|
|
31
|
+
const timestamps = (await store.get(key)).filter((t) => t > cutoff);
|
|
32
|
+
if (timestamps.length >= config.limit) {
|
|
33
|
+
const oldestInWindow = Math.min(...timestamps);
|
|
34
|
+
const resetAt = oldestInWindow + windowMs;
|
|
35
|
+
return {
|
|
36
|
+
allowed: false,
|
|
37
|
+
remaining: 0,
|
|
38
|
+
resetAt,
|
|
39
|
+
retryAfterSeconds: Math.ceil((resetAt - now) / 1e3)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
timestamps.push(now);
|
|
43
|
+
await store.set(key, timestamps, config.windowSeconds * 2);
|
|
44
|
+
return {
|
|
45
|
+
allowed: true,
|
|
46
|
+
remaining: config.limit - timestamps.length,
|
|
47
|
+
resetAt: now + windowMs
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
MemoryRateLimitStore,
|
|
53
|
+
checkRateLimit
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=chunk-4FULF5LW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rate-limit.ts"],"sourcesContent":["/**\n * Sliding window rate limiter.\n * In-memory by default. Override with KV-backed store for Workers.\n */\n\nexport interface RateLimitConfig {\n /** Max requests per window (default: 60) */\n limit: number\n /** Window size in seconds (default: 60) */\n windowSeconds: number\n}\n\nexport interface RateLimitResult {\n allowed: boolean\n remaining: number\n resetAt: number\n retryAfterSeconds?: number\n}\n\nexport interface RateLimitStore {\n /** Get timestamps of recent requests for this key */\n get(key: string): Promise<number[]>\n /** Set timestamps for this key (with TTL) */\n set(key: string, timestamps: number[], ttlSeconds: number): Promise<void>\n}\n\n/** In-memory rate limit store with periodic eviction */\nexport class MemoryRateLimitStore implements RateLimitStore {\n private store = new Map<string, { timestamps: number[]; expiresAt: number }>()\n private lastEviction = Date.now()\n\n async get(key: string): Promise<number[]> {\n this.evictExpired()\n const entry = this.store.get(key)\n if (!entry || entry.expiresAt < Date.now()) {\n this.store.delete(key)\n return []\n }\n return entry.timestamps\n }\n\n async set(key: string, timestamps: number[], ttlSeconds: number): Promise<void> {\n this.store.set(key, { timestamps, expiresAt: Date.now() + ttlSeconds * 1000 })\n }\n\n private evictExpired() {\n const now = Date.now()\n if (now - this.lastEviction < 30_000) return\n this.lastEviction = now\n for (const [key, entry] of this.store) {\n if (entry.expiresAt < now) this.store.delete(key)\n }\n }\n}\n\nexport async function checkRateLimit(\n consumerId: string,\n config: RateLimitConfig,\n store: RateLimitStore,\n): Promise<RateLimitResult> {\n const now = Date.now()\n const windowMs = config.windowSeconds * 1000\n const cutoff = now - windowMs\n\n const key = `rl:${consumerId}`\n const timestamps = (await store.get(key)).filter(t => t > cutoff)\n\n if (timestamps.length >= config.limit) {\n const oldestInWindow = Math.min(...timestamps)\n const resetAt = oldestInWindow + windowMs\n return {\n allowed: false,\n remaining: 0,\n resetAt,\n retryAfterSeconds: Math.ceil((resetAt - now) / 1000),\n }\n }\n\n timestamps.push(now)\n await store.set(key, timestamps, config.windowSeconds * 2)\n\n return {\n allowed: true,\n remaining: config.limit - timestamps.length,\n resetAt: now + windowMs,\n }\n}\n"],"mappings":";AA2BO,IAAM,uBAAN,MAAqD;AAAA,EAClD,QAAQ,oBAAI,IAAyD;AAAA,EACrE,eAAe,KAAK,IAAI;AAAA,EAEhC,MAAM,IAAI,KAAgC;AACxC,SAAK,aAAa;AAClB,UAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;AAChC,QAAI,CAAC,SAAS,MAAM,YAAY,KAAK,IAAI,GAAG;AAC1C,WAAK,MAAM,OAAO,GAAG;AACrB,aAAO,CAAC;AAAA,IACV;AACA,WAAO,MAAM;AAAA,EACf;AAAA,EAEA,MAAM,IAAI,KAAa,YAAsB,YAAmC;AAC9E,SAAK,MAAM,IAAI,KAAK,EAAE,YAAY,WAAW,KAAK,IAAI,IAAI,aAAa,IAAK,CAAC;AAAA,EAC/E;AAAA,EAEQ,eAAe;AACrB,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,MAAM,KAAK,eAAe,IAAQ;AACtC,SAAK,eAAe;AACpB,eAAW,CAAC,KAAK,KAAK,KAAK,KAAK,OAAO;AACrC,UAAI,MAAM,YAAY,IAAK,MAAK,MAAM,OAAO,GAAG;AAAA,IAClD;AAAA,EACF;AACF;AAEA,eAAsB,eACpB,YACA,QACA,OAC0B;AAC1B,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,WAAW,OAAO,gBAAgB;AACxC,QAAM,SAAS,MAAM;AAErB,QAAM,MAAM,MAAM,UAAU;AAC5B,QAAM,cAAc,MAAM,MAAM,IAAI,GAAG,GAAG,OAAO,OAAK,IAAI,MAAM;AAEhE,MAAI,WAAW,UAAU,OAAO,OAAO;AACrC,UAAM,iBAAiB,KAAK,IAAI,GAAG,UAAU;AAC7C,UAAM,UAAU,iBAAiB;AACjC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,WAAW;AAAA,MACX;AAAA,MACA,mBAAmB,KAAK,MAAM,UAAU,OAAO,GAAI;AAAA,IACrD;AAAA,EACF;AAEA,aAAW,KAAK,GAAG;AACnB,QAAM,MAAM,IAAI,KAAK,YAAY,OAAO,gBAAgB,CAAC;AAEzD,SAAO;AAAA,IACL,SAAS;AAAA,IACT,WAAW,OAAO,QAAQ,WAAW;AAAA,IACrC,SAAS,MAAM;AAAA,EACjB;AACF;","names":[]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/api-keys.ts
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
function generateRawKey(prefix) {
|
|
4
|
+
const bytes = new Uint8Array(16);
|
|
5
|
+
crypto.getRandomValues(bytes);
|
|
6
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7
|
+
return `${prefix}${hex}`;
|
|
8
|
+
}
|
|
9
|
+
async function hashKey(raw) {
|
|
10
|
+
const encoded = new TextEncoder().encode(raw);
|
|
11
|
+
const digest = await crypto.subtle.digest("SHA-256", encoded);
|
|
12
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
13
|
+
}
|
|
14
|
+
async function verifyApiKeyFromStore(authHeader, store, prefix = "ak_") {
|
|
15
|
+
const bearerPrefix = `Bearer ${prefix}`;
|
|
16
|
+
if (!authHeader.startsWith(bearerPrefix)) return null;
|
|
17
|
+
const rawKey = authHeader.slice(7);
|
|
18
|
+
const keyHash = await hashKey(rawKey);
|
|
19
|
+
const key = await store.findByHash(keyHash);
|
|
20
|
+
if (!key) return null;
|
|
21
|
+
if (key.expiresAt && key.expiresAt < /* @__PURE__ */ new Date()) return null;
|
|
22
|
+
if (key.spendingLimitCents !== null && key.spentCents >= key.spendingLimitCents) return null;
|
|
23
|
+
return {
|
|
24
|
+
key,
|
|
25
|
+
consumerId: `apikey:${key.id}`,
|
|
26
|
+
keyId: key.id,
|
|
27
|
+
scopes: key.scopes,
|
|
28
|
+
rateLimitPerMinute: key.rateLimit,
|
|
29
|
+
dailyLimit: key.dailyLimit
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function createApiKeyRoutes(config) {
|
|
33
|
+
const router = new Hono();
|
|
34
|
+
const prefix = config.prefix ?? "ak_";
|
|
35
|
+
const validScopes = config.validScopes ?? ["chat"];
|
|
36
|
+
router.get("/", async (c) => {
|
|
37
|
+
const userId = await config.getAuthUserId(c.req.raw);
|
|
38
|
+
if (!userId) return c.json({ error: "Unauthorized" }, 401);
|
|
39
|
+
const keys = await config.store.list(userId);
|
|
40
|
+
return c.json({ keys });
|
|
41
|
+
});
|
|
42
|
+
router.post("/", async (c) => {
|
|
43
|
+
const userId = await config.getAuthUserId(c.req.raw);
|
|
44
|
+
if (!userId) return c.json({ error: "Unauthorized" }, 401);
|
|
45
|
+
const body = await c.req.json();
|
|
46
|
+
if (!body.name?.trim()) return c.json({ error: "name is required" }, 400);
|
|
47
|
+
const scopes = (body.scopes ?? ["chat"]).filter((s) => validScopes.includes(s));
|
|
48
|
+
if (scopes.length === 0) scopes.push("chat");
|
|
49
|
+
const rawKey = generateRawKey(prefix);
|
|
50
|
+
const keyHash = await hashKey(rawKey);
|
|
51
|
+
const keyPrefix = rawKey.slice(0, prefix.length + 8);
|
|
52
|
+
const created = await config.store.create(userId, {
|
|
53
|
+
name: body.name.trim(),
|
|
54
|
+
keyHash,
|
|
55
|
+
keyPrefix,
|
|
56
|
+
scopes,
|
|
57
|
+
rateLimit: body.rateLimit ?? 60,
|
|
58
|
+
dailyLimit: body.dailyLimit ?? 1e3,
|
|
59
|
+
spendingLimitCents: body.spendingLimitCents ?? null,
|
|
60
|
+
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null
|
|
61
|
+
});
|
|
62
|
+
return c.json({
|
|
63
|
+
key: rawKey,
|
|
64
|
+
id: created.id,
|
|
65
|
+
name: created.name,
|
|
66
|
+
keyPrefix: created.keyPrefix,
|
|
67
|
+
scopes: created.scopes,
|
|
68
|
+
rateLimit: created.rateLimit,
|
|
69
|
+
dailyLimit: created.dailyLimit,
|
|
70
|
+
spendingLimitCents: created.spendingLimitCents,
|
|
71
|
+
expiresAt: created.expiresAt,
|
|
72
|
+
_notice: "Store this key securely. It will not be shown again."
|
|
73
|
+
}, 201);
|
|
74
|
+
});
|
|
75
|
+
router.delete("/:keyId", async (c) => {
|
|
76
|
+
const userId = await config.getAuthUserId(c.req.raw);
|
|
77
|
+
if (!userId) return c.json({ error: "Unauthorized" }, 401);
|
|
78
|
+
const deleted = await config.store.delete(userId, c.req.param("keyId"));
|
|
79
|
+
if (!deleted) return c.json({ error: "API key not found" }, 404);
|
|
80
|
+
return c.json({ deleted: true });
|
|
81
|
+
});
|
|
82
|
+
return router;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
verifyApiKeyFromStore,
|
|
87
|
+
createApiKeyRoutes
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=chunk-5O75YDQP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/api-keys.ts"],"sourcesContent":["/**\n * API key management — create, list, verify, revoke.\n *\n * The gateway package provides:\n * - Types and interfaces (ApiKeyStore)\n * - A Hono router for CRUD (createApiKeyRoutes)\n * - A verifyApiKey function that checks against the store\n *\n * Each agent implements ApiKeyStore against their own DB.\n */\n\nimport { Hono } from 'hono'\n\n// --- Types ---\n\nexport interface ApiKey {\n id: string\n userId: string\n name: string\n keyHash: string\n keyPrefix: string\n scopes: string[]\n rateLimit: number // requests per minute\n dailyLimit: number // requests per day\n spendingLimitCents: number | null // max spend in cents (null = unlimited)\n spentCents: number // running total spent\n lastUsedAt: Date | null\n expiresAt: Date | null\n createdAt: Date\n}\n\nexport interface ApiKeyCreateRequest {\n name: string\n scopes?: string[]\n rateLimit?: number\n dailyLimit?: number\n spendingLimitCents?: number\n expiresAt?: string\n}\n\n/** Each agent implements this against their DB */\nexport interface ApiKeyStore {\n create(userId: string, data: {\n name: string\n keyHash: string\n keyPrefix: string\n scopes: string[]\n rateLimit: number\n dailyLimit: number\n spendingLimitCents: number | null\n expiresAt: Date | null\n }): Promise<ApiKey>\n\n list(userId: string): Promise<Omit<ApiKey, 'keyHash'>[]>\n\n findByHash(keyHash: string): Promise<ApiKey | null>\n\n delete(userId: string, keyId: string): Promise<boolean>\n\n recordUsage(keyId: string, costCents: number): Promise<void>\n}\n\n// --- Key generation ---\n\nfunction generateRawKey(prefix: string): string {\n const bytes = new Uint8Array(16)\n crypto.getRandomValues(bytes)\n const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')\n return `${prefix}${hex}`\n}\n\nasync function hashKey(raw: string): Promise<string> {\n const encoded = new TextEncoder().encode(raw)\n const digest = await crypto.subtle.digest('SHA-256', encoded)\n return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('')\n}\n\n// --- Verification ---\n\nexport async function verifyApiKeyFromStore(\n authHeader: string,\n store: ApiKeyStore,\n prefix = 'ak_',\n): Promise<{ key: ApiKey; keyId: string; consumerId: string; scopes: string[]; rateLimitPerMinute: number; dailyLimit: number } | null> {\n const bearerPrefix = `Bearer ${prefix}`\n if (!authHeader.startsWith(bearerPrefix)) return null\n\n const rawKey = authHeader.slice(7) // strip \"Bearer \"\n const keyHash = await hashKey(rawKey)\n const key = await store.findByHash(keyHash)\n if (!key) return null\n\n // Check expiry\n if (key.expiresAt && key.expiresAt < new Date()) return null\n\n // Check spending limit\n if (key.spendingLimitCents !== null && key.spentCents >= key.spendingLimitCents) return null\n\n return {\n key,\n consumerId: `apikey:${key.id}`,\n keyId: key.id,\n scopes: key.scopes,\n rateLimitPerMinute: key.rateLimit,\n dailyLimit: key.dailyLimit,\n }\n}\n\n// --- CRUD Routes ---\n\nexport interface ApiKeyRoutesConfig {\n store: ApiKeyStore\n /** Get the authenticated user ID from the request. Return null if not authenticated. */\n getAuthUserId: (request: Request) => Promise<string | null>\n /** Key prefix (default: \"ak_\") */\n prefix?: string\n /** Valid scopes for this agent (default: [\"chat\"]) */\n validScopes?: string[]\n}\n\nexport function createApiKeyRoutes(config: ApiKeyRoutesConfig) {\n const router = new Hono()\n const prefix = config.prefix ?? 'ak_'\n const validScopes = config.validScopes ?? ['chat']\n\n // List keys\n router.get('/', async (c) => {\n const userId = await config.getAuthUserId(c.req.raw)\n if (!userId) return c.json({ error: 'Unauthorized' }, 401)\n\n const keys = await config.store.list(userId)\n return c.json({ keys })\n })\n\n // Create key\n router.post('/', async (c) => {\n const userId = await config.getAuthUserId(c.req.raw)\n if (!userId) return c.json({ error: 'Unauthorized' }, 401)\n\n const body = await c.req.json<ApiKeyCreateRequest>()\n if (!body.name?.trim()) return c.json({ error: 'name is required' }, 400)\n\n const scopes = (body.scopes ?? ['chat']).filter(s => validScopes.includes(s))\n if (scopes.length === 0) scopes.push('chat')\n\n const rawKey = generateRawKey(prefix)\n const keyHash = await hashKey(rawKey)\n const keyPrefix = rawKey.slice(0, prefix.length + 8)\n\n const created = await config.store.create(userId, {\n name: body.name.trim(),\n keyHash,\n keyPrefix,\n scopes,\n rateLimit: body.rateLimit ?? 60,\n dailyLimit: body.dailyLimit ?? 1000,\n spendingLimitCents: body.spendingLimitCents ?? null,\n expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,\n })\n\n return c.json({\n key: rawKey,\n id: created.id,\n name: created.name,\n keyPrefix: created.keyPrefix,\n scopes: created.scopes,\n rateLimit: created.rateLimit,\n dailyLimit: created.dailyLimit,\n spendingLimitCents: created.spendingLimitCents,\n expiresAt: created.expiresAt,\n _notice: 'Store this key securely. It will not be shown again.',\n }, 201)\n })\n\n // Delete key\n router.delete('/:keyId', async (c) => {\n const userId = await config.getAuthUserId(c.req.raw)\n if (!userId) return c.json({ error: 'Unauthorized' }, 401)\n\n const deleted = await config.store.delete(userId, c.req.param('keyId'))\n if (!deleted) return c.json({ error: 'API key not found' }, 404)\n\n return c.json({ deleted: true })\n })\n\n return router\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/publish.ts
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
function createPublishRoutes(config) {
|
|
4
|
+
const router = new Hono();
|
|
5
|
+
router.get("/:resourceId/publish", async (c) => {
|
|
6
|
+
const userId = await config.getAuthUserId(c.req.raw);
|
|
7
|
+
if (!userId) return c.json({ error: "Unauthorized" }, 401);
|
|
8
|
+
const resourceId = c.req.param("resourceId");
|
|
9
|
+
const owns = await config.store.verifyOwnership(userId, resourceId);
|
|
10
|
+
if (!owns) return c.json({ error: "Not found" }, 404);
|
|
11
|
+
const published = await config.store.getPublishedConfig(userId, resourceId);
|
|
12
|
+
return c.json({ published });
|
|
13
|
+
});
|
|
14
|
+
router.post("/:resourceId/publish", async (c) => {
|
|
15
|
+
const userId = await config.getAuthUserId(c.req.raw);
|
|
16
|
+
if (!userId) return c.json({ error: "Unauthorized" }, 401);
|
|
17
|
+
const resourceId = c.req.param("resourceId");
|
|
18
|
+
const owns = await config.store.verifyOwnership(userId, resourceId);
|
|
19
|
+
if (!owns) return c.json({ error: "Not found" }, 404);
|
|
20
|
+
const body = await c.req.json();
|
|
21
|
+
const slug = body.slug ?? resourceId;
|
|
22
|
+
const publishedConfig = {
|
|
23
|
+
enabled: true,
|
|
24
|
+
slug,
|
|
25
|
+
pricePerTokenUsd: body.pricePerTokenUsd ?? 2e-5,
|
|
26
|
+
platformFeePercent: body.platformFeePercent ?? 0.2,
|
|
27
|
+
sandboxEndpoint: body.sandboxEndpoint ?? null,
|
|
28
|
+
remoteSandboxId: body.remoteSandboxId ?? null,
|
|
29
|
+
remoteBearerToken: body.remoteBearerToken ?? null,
|
|
30
|
+
publishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
31
|
+
};
|
|
32
|
+
await config.store.setPublishedConfig(userId, resourceId, publishedConfig);
|
|
33
|
+
const base = config.baseUrl ?? "";
|
|
34
|
+
return c.json({
|
|
35
|
+
success: true,
|
|
36
|
+
published: publishedConfig,
|
|
37
|
+
gatewayUrl: `${base}/v1/agents/${slug}/chat/completions`,
|
|
38
|
+
discoveryUrl: `${base}/v1/agents/${slug}/chat/completions`
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
router.post("/:resourceId/unpublish", async (c) => {
|
|
42
|
+
const userId = await config.getAuthUserId(c.req.raw);
|
|
43
|
+
if (!userId) return c.json({ error: "Unauthorized" }, 401);
|
|
44
|
+
const resourceId = c.req.param("resourceId");
|
|
45
|
+
const owns = await config.store.verifyOwnership(userId, resourceId);
|
|
46
|
+
if (!owns) return c.json({ error: "Not found" }, 404);
|
|
47
|
+
await config.store.clearPublishedConfig(userId, resourceId);
|
|
48
|
+
return c.json({ success: true, published: null });
|
|
49
|
+
});
|
|
50
|
+
return router;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
createPublishRoutes
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=chunk-5ZJPIIIV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/publish.ts"],"sourcesContent":["/**\n * Publishing routes — let agent owners publish/unpublish their workspaces\n * as paid API endpoints.\n */\n\nimport { Hono } from 'hono'\n\n// --- Types ---\n\nexport interface PublishedConfig {\n enabled: boolean\n slug: string\n pricePerTokenUsd: number\n platformFeePercent: number\n /** Remote operator endpoint for sovereignty mode */\n sandboxEndpoint?: string | null\n remoteSandboxId?: string | null\n remoteBearerToken?: string | null\n publishedAt: string\n}\n\nexport interface PublishRequest {\n slug?: string\n pricePerTokenUsd?: number\n platformFeePercent?: number\n sandboxEndpoint?: string | null\n remoteSandboxId?: string | null\n remoteBearerToken?: string | null\n}\n\n/** Each agent implements this against their workspace/session model */\nexport interface PublishStore {\n /** Get current published config for a workspace/session */\n getPublishedConfig(ownerId: string, resourceId: string): Promise<PublishedConfig | null>\n /** Set published config */\n setPublishedConfig(ownerId: string, resourceId: string, config: PublishedConfig): Promise<void>\n /** Clear published config (unpublish) */\n clearPublishedConfig(ownerId: string, resourceId: string): Promise<void>\n /** Check the resource exists and the user owns it */\n verifyOwnership(ownerId: string, resourceId: string): Promise<boolean>\n}\n\n// --- Routes ---\n\nexport interface PublishRoutesConfig {\n store: PublishStore\n getAuthUserId: (request: Request) => Promise<string | null>\n /** Base URL for gateway endpoint display (e.g. \"https://gtm.tangle.tools\") */\n baseUrl?: string\n}\n\nexport function createPublishRoutes(config: PublishRoutesConfig) {\n const router = new Hono()\n\n // Get publish status\n router.get('/:resourceId/publish', async (c) => {\n const userId = await config.getAuthUserId(c.req.raw)\n if (!userId) return c.json({ error: 'Unauthorized' }, 401)\n\n const resourceId = c.req.param('resourceId')\n const owns = await config.store.verifyOwnership(userId, resourceId)\n if (!owns) return c.json({ error: 'Not found' }, 404)\n\n const published = await config.store.getPublishedConfig(userId, resourceId)\n return c.json({ published })\n })\n\n // Publish\n router.post('/:resourceId/publish', async (c) => {\n const userId = await config.getAuthUserId(c.req.raw)\n if (!userId) return c.json({ error: 'Unauthorized' }, 401)\n\n const resourceId = c.req.param('resourceId')\n const owns = await config.store.verifyOwnership(userId, resourceId)\n if (!owns) return c.json({ error: 'Not found' }, 404)\n\n const body = await c.req.json<PublishRequest>()\n const slug = body.slug ?? resourceId\n\n const publishedConfig: PublishedConfig = {\n enabled: true,\n slug,\n pricePerTokenUsd: body.pricePerTokenUsd ?? 0.00002,\n platformFeePercent: body.platformFeePercent ?? 0.20,\n sandboxEndpoint: body.sandboxEndpoint ?? null,\n remoteSandboxId: body.remoteSandboxId ?? null,\n remoteBearerToken: body.remoteBearerToken ?? null,\n publishedAt: new Date().toISOString(),\n }\n\n await config.store.setPublishedConfig(userId, resourceId, publishedConfig)\n\n const base = config.baseUrl ?? ''\n return c.json({\n success: true,\n published: publishedConfig,\n gatewayUrl: `${base}/v1/agents/${slug}/chat/completions`,\n discoveryUrl: `${base}/v1/agents/${slug}/chat/completions`,\n })\n })\n\n // Unpublish\n router.post('/:resourceId/unpublish', async (c) => {\n const userId = await config.getAuthUserId(c.req.raw)\n if (!userId) return c.json({ error: 'Unauthorized' }, 401)\n\n const resourceId = c.req.param('resourceId')\n const owns = await config.store.verifyOwnership(userId, resourceId)\n if (!owns) return c.json({ error: 'Not found' }, 404)\n\n await config.store.clearPublishedConfig(userId, resourceId)\n return c.json({ success: true, published: null })\n })\n\n return router\n}\n"],"mappings":";AAKA,SAAS,YAAY;AA8Cd,SAAS,oBAAoB,QAA6B;AAC/D,QAAM,SAAS,IAAI,KAAK;AAGxB,SAAO,IAAI,wBAAwB,OAAO,MAAM;AAC9C,UAAM,SAAS,MAAM,OAAO,cAAc,EAAE,IAAI,GAAG;AACnD,QAAI,CAAC,OAAQ,QAAO,EAAE,KAAK,EAAE,OAAO,eAAe,GAAG,GAAG;AAEzD,UAAM,aAAa,EAAE,IAAI,MAAM,YAAY;AAC3C,UAAM,OAAO,MAAM,OAAO,MAAM,gBAAgB,QAAQ,UAAU;AAClE,QAAI,CAAC,KAAM,QAAO,EAAE,KAAK,EAAE,OAAO,YAAY,GAAG,GAAG;AAEpD,UAAM,YAAY,MAAM,OAAO,MAAM,mBAAmB,QAAQ,UAAU;AAC1E,WAAO,EAAE,KAAK,EAAE,UAAU,CAAC;AAAA,EAC7B,CAAC;AAGD,SAAO,KAAK,wBAAwB,OAAO,MAAM;AAC/C,UAAM,SAAS,MAAM,OAAO,cAAc,EAAE,IAAI,GAAG;AACnD,QAAI,CAAC,OAAQ,QAAO,EAAE,KAAK,EAAE,OAAO,eAAe,GAAG,GAAG;AAEzD,UAAM,aAAa,EAAE,IAAI,MAAM,YAAY;AAC3C,UAAM,OAAO,MAAM,OAAO,MAAM,gBAAgB,QAAQ,UAAU;AAClE,QAAI,CAAC,KAAM,QAAO,EAAE,KAAK,EAAE,OAAO,YAAY,GAAG,GAAG;AAEpD,UAAM,OAAO,MAAM,EAAE,IAAI,KAAqB;AAC9C,UAAM,OAAO,KAAK,QAAQ;AAE1B,UAAM,kBAAmC;AAAA,MACvC,SAAS;AAAA,MACT;AAAA,MACA,kBAAkB,KAAK,oBAAoB;AAAA,MAC3C,oBAAoB,KAAK,sBAAsB;AAAA,MAC/C,iBAAiB,KAAK,mBAAmB;AAAA,MACzC,iBAAiB,KAAK,mBAAmB;AAAA,MACzC,mBAAmB,KAAK,qBAAqB;AAAA,MAC7C,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACtC;AAEA,UAAM,OAAO,MAAM,mBAAmB,QAAQ,YAAY,eAAe;AAEzE,UAAM,OAAO,OAAO,WAAW;AAC/B,WAAO,EAAE,KAAK;AAAA,MACZ,SAAS;AAAA,MACT,WAAW;AAAA,MACX,YAAY,GAAG,IAAI,cAAc,IAAI;AAAA,MACrC,cAAc,GAAG,IAAI,cAAc,IAAI;AAAA,IACzC,CAAC;AAAA,EACH,CAAC;AAGD,SAAO,KAAK,0BAA0B,OAAO,MAAM;AACjD,UAAM,SAAS,MAAM,OAAO,cAAc,EAAE,IAAI,GAAG;AACnD,QAAI,CAAC,OAAQ,QAAO,EAAE,KAAK,EAAE,OAAO,eAAe,GAAG,GAAG;AAEzD,UAAM,aAAa,EAAE,IAAI,MAAM,YAAY;AAC3C,UAAM,OAAO,MAAM,OAAO,MAAM,gBAAgB,QAAQ,UAAU;AAClE,QAAI,CAAC,KAAM,QAAO,EAAE,KAAK,EAAE,OAAO,YAAY,GAAG,GAAG;AAEpD,UAAM,OAAO,MAAM,qBAAqB,QAAQ,UAAU;AAC1D,WAAO,EAAE,KAAK,EAAE,SAAS,MAAM,WAAW,KAAK,CAAC;AAAA,EAClD,CAAC;AAED,SAAO;AACT;","names":[]}
|