@nkmc/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/dist/chunk-56RA53VS.js +37 -0
- package/dist/chunk-CZJ75YTV.js +969 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/http.cjs +1772 -0
- package/dist/http.d.cts +49 -0
- package/dist/http.d.ts +49 -0
- package/dist/http.js +748 -0
- package/dist/index.cjs +2436 -0
- package/dist/index.d.cts +436 -0
- package/dist/index.d.ts +436 -0
- package/dist/index.js +1434 -0
- package/dist/proxy-ClPcDgsO.d.cts +283 -0
- package/dist/proxy-qpda1ANS.d.ts +283 -0
- package/dist/proxy.cjs +148 -0
- package/dist/proxy.d.cts +6 -0
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +90 -0
- package/dist/testing.cjs +865 -0
- package/dist/testing.d.cts +12 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +831 -0
- package/dist/tunnels-BviBEaih.d.cts +12 -0
- package/dist/tunnels-DFHNgmN7.d.ts +12 -0
- package/dist/types-C6JC9oTm.d.cts +21 -0
- package/dist/types-C6JC9oTm.d.ts +21 -0
- package/package.json +47 -0
- package/src/__tests__/sqlite-integration.test.ts +384 -0
- package/src/credential/d1-vault.ts +134 -0
- package/src/credential/memory-vault.ts +50 -0
- package/src/credential/types.ts +16 -0
- package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
- package/src/d1/sqlite-adapter.ts +59 -0
- package/src/d1/types.ts +22 -0
- package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
- package/src/federation/__tests__/peer-client.test.ts +205 -0
- package/src/federation/__tests__/peer-store.test.ts +114 -0
- package/src/federation/d1-peer-store.ts +164 -0
- package/src/federation/peer-backend.ts +60 -0
- package/src/federation/peer-client.ts +122 -0
- package/src/federation/peer-store.ts +45 -0
- package/src/federation/types.ts +39 -0
- package/src/http/app.ts +152 -0
- package/src/http/lib/dns.ts +30 -0
- package/src/http/middleware/admin-auth.ts +18 -0
- package/src/http/middleware/agent-auth.ts +27 -0
- package/src/http/middleware/publish-auth.ts +39 -0
- package/src/http/routes/__tests__/federation.test.ts +364 -0
- package/src/http/routes/__tests__/peers.test.ts +290 -0
- package/src/http/routes/__tests__/proxy.test.ts +159 -0
- package/src/http/routes/auth.ts +39 -0
- package/src/http/routes/byok.ts +62 -0
- package/src/http/routes/credentials.ts +40 -0
- package/src/http/routes/domains.ts +174 -0
- package/src/http/routes/federation.ts +170 -0
- package/src/http/routes/fs.ts +89 -0
- package/src/http/routes/peers.ts +103 -0
- package/src/http/routes/proxy.ts +57 -0
- package/src/http/routes/registry.ts +222 -0
- package/src/http/routes/tunnels.ts +124 -0
- package/src/http.ts +9 -0
- package/src/index.ts +63 -0
- package/src/metering/d1-store.ts +123 -0
- package/src/metering/memory-store.ts +29 -0
- package/src/metering/pricing-guard.ts +68 -0
- package/src/metering/types.ts +25 -0
- package/src/onboard/apis-guru.ts +64 -0
- package/src/onboard/index.ts +4 -0
- package/src/onboard/manifest.ts +362 -0
- package/src/onboard/pipeline.ts +214 -0
- package/src/onboard/types.ts +72 -0
- package/src/proxy/__tests__/tool-registry.test.ts +93 -0
- package/src/proxy/tool-registry.ts +122 -0
- package/src/proxy.ts +12 -0
- package/src/registry/context7-backend.ts +93 -0
- package/src/registry/context7.ts +54 -0
- package/src/registry/d1-store.ts +242 -0
- package/src/registry/memory-store.ts +101 -0
- package/src/registry/openapi-compiler.ts +284 -0
- package/src/registry/resolver.ts +196 -0
- package/src/registry/rpc-compiler.ts +142 -0
- package/src/registry/skill-parser.ts +119 -0
- package/src/registry/skill-to-config.ts +239 -0
- package/src/registry/source-refresher.ts +83 -0
- package/src/registry/types.ts +129 -0
- package/src/registry/virtual-files.ts +76 -0
- package/src/testing/sqlite-d1.ts +64 -0
- package/src/testing.ts +2 -0
- package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
- package/src/tunnel/__tests__/tunnel.test.ts +542 -0
- package/src/tunnel/cloudflare-provider.ts +121 -0
- package/src/tunnel/memory-store.ts +30 -0
- package/src/tunnel/types.ts +28 -0
- package/test/credential/d1-vault.test.ts +127 -0
- package/test/credential/injection.test.ts +67 -0
- package/test/credential/memory-vault.test.ts +63 -0
- package/test/http/app.test.ts +300 -0
- package/test/http/byok-e2e.test.ts +240 -0
- package/test/http/byok.test.ts +115 -0
- package/test/http/credentials.test.ts +57 -0
- package/test/http/e2e.test.ts +260 -0
- package/test/integration/authenticated-apis.test.ts +185 -0
- package/test/integration/free-apis-e2e.test.ts +222 -0
- package/test/metering/d1-store.test.ts +82 -0
- package/test/metering/memory-store.test.ts +76 -0
- package/test/metering/pricing-guard.test.ts +108 -0
- package/test/onboard/apis-guru.test.ts +57 -0
- package/test/onboard/e2e.test.ts +70 -0
- package/test/onboard/pipeline.test.ts +318 -0
- package/test/onboard/real-apis.test.ts +483 -0
- package/test/registry/compilation-correctness.test.ts +132 -0
- package/test/registry/context7-backend.test.ts +88 -0
- package/test/registry/context7-e2e.test.ts +92 -0
- package/test/registry/context7.test.ts +73 -0
- package/test/registry/d1-store.test.ts +184 -0
- package/test/registry/integration.test.ts +129 -0
- package/test/registry/lazy-mount.test.ts +138 -0
- package/test/registry/memory-store.test.ts +171 -0
- package/test/registry/openapi-compiler.test.ts +267 -0
- package/test/registry/openapi-e2e.test.ts +154 -0
- package/test/registry/passthrough-e2e.test.ts +109 -0
- package/test/registry/resolver-peer.test.ts +299 -0
- package/test/registry/resolver.test.ts +228 -0
- package/test/registry/rpc-compiler.test.ts +112 -0
- package/test/registry/skill-parser.test.ts +151 -0
- package/test/registry/skill-to-config.test.ts +151 -0
- package/test/registry/skill-to-rpc-config.test.ts +142 -0
- package/test/registry/source-refresher.test.ts +90 -0
- package/test/registry/virtual-files.test.ts +96 -0
- package/tsconfig.json +4 -0
- package/tsup.config.ts +8 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { RegistryStore, EndpointRecord, SourceConfig } from "../../registry/types.js";
|
|
3
|
+
import { parseSkillMd } from "../../registry/skill-parser.js";
|
|
4
|
+
import { fetchAndCompile } from "../../registry/openapi-compiler.js";
|
|
5
|
+
import type { Env } from "../app.js";
|
|
6
|
+
import type { PublishAuthContext } from "../middleware/publish-auth.js";
|
|
7
|
+
|
|
8
|
+
/** Well-known paths to probe for OpenAPI specs */
|
|
9
|
+
const OPENAPI_PATHS = [
|
|
10
|
+
"/openapi.json",
|
|
11
|
+
"/openapi.yaml",
|
|
12
|
+
"/swagger.json",
|
|
13
|
+
"/swagger.yaml",
|
|
14
|
+
"/docs/openapi.json",
|
|
15
|
+
"/api-docs",
|
|
16
|
+
"/api/openapi.json",
|
|
17
|
+
"/.well-known/openapi.json",
|
|
18
|
+
"/.well-known/openapi.yaml",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export interface RegistryRouteOptions {
|
|
22
|
+
store: RegistryStore;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registryRoutes(options: RegistryRouteOptions) {
|
|
26
|
+
const { store } = options;
|
|
27
|
+
const app = new Hono<Env>();
|
|
28
|
+
|
|
29
|
+
// Register a service (body = skill.md content)
|
|
30
|
+
app.post("/services", async (c) => {
|
|
31
|
+
const domain = c.req.query("domain");
|
|
32
|
+
if (!domain) {
|
|
33
|
+
return c.json({ error: "Missing ?domain= query parameter" }, 400);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Publish token scope check: token must match the target domain
|
|
37
|
+
const auth = c.get("publishAuth" as never) as PublishAuthContext | undefined;
|
|
38
|
+
if (auth?.type === "publish" && auth.domain !== domain) {
|
|
39
|
+
return c.json(
|
|
40
|
+
{ error: `Token is scoped to "${auth.domain}", cannot register "${domain}"` },
|
|
41
|
+
403,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const contentType = c.req.header("Content-Type") ?? "";
|
|
46
|
+
let skillMd: string;
|
|
47
|
+
|
|
48
|
+
if (contentType.includes("text/markdown") || contentType.includes("text/plain")) {
|
|
49
|
+
skillMd = await c.req.text();
|
|
50
|
+
} else {
|
|
51
|
+
// Try JSON body with skillMd field (and optional pre-compiled endpoints)
|
|
52
|
+
const body = await c.req.json<{ skillMd: string; endpoints?: EndpointRecord[]; source?: SourceConfig }>().catch(() => null);
|
|
53
|
+
if (!body?.skillMd) {
|
|
54
|
+
return c.json({ error: "Body must be skill.md text or JSON with skillMd field" }, 400);
|
|
55
|
+
}
|
|
56
|
+
skillMd = body.skillMd;
|
|
57
|
+
// If pre-compiled endpoints are provided, use them after parsing
|
|
58
|
+
if (Array.isArray(body.endpoints) && body.endpoints.length > 0) {
|
|
59
|
+
if (!skillMd.trim()) {
|
|
60
|
+
return c.json({ error: "Empty skill.md content" }, 400);
|
|
61
|
+
}
|
|
62
|
+
const isFirstParty = c.req.query("first_party") === "true";
|
|
63
|
+
const authMode = c.req.query("auth_mode");
|
|
64
|
+
const record = parseSkillMd(domain, skillMd, { isFirstParty });
|
|
65
|
+
record.endpoints = body.endpoints;
|
|
66
|
+
if (body.source) {
|
|
67
|
+
record.source = body.source;
|
|
68
|
+
}
|
|
69
|
+
if (authMode === "nkmc-jwt") {
|
|
70
|
+
record.authMode = "nkmc-jwt";
|
|
71
|
+
}
|
|
72
|
+
await store.put(domain, record);
|
|
73
|
+
return c.json({ ok: true, domain, name: record.name }, 201);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!skillMd.trim()) {
|
|
78
|
+
return c.json({ error: "Empty skill.md content" }, 400);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isFirstParty = c.req.query("first_party") === "true";
|
|
82
|
+
const authMode = c.req.query("auth_mode");
|
|
83
|
+
const record = parseSkillMd(domain, skillMd, { isFirstParty });
|
|
84
|
+
if (authMode === "nkmc-jwt") {
|
|
85
|
+
record.authMode = "nkmc-jwt";
|
|
86
|
+
}
|
|
87
|
+
await store.put(domain, record);
|
|
88
|
+
|
|
89
|
+
return c.json({ ok: true, domain, name: record.name }, 201);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Discover: auto-detect OpenAPI spec from a running service URL and register
|
|
93
|
+
app.post("/services/discover", async (c) => {
|
|
94
|
+
const body = await c.req.json<{ url: string; domain?: string; specUrl?: string }>().catch(() => null);
|
|
95
|
+
if (!body?.url) {
|
|
96
|
+
return c.json({ error: "Missing 'url' field (base URL of the service)" }, 400);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const baseUrl = body.url.replace(/\/+$/, "");
|
|
100
|
+
|
|
101
|
+
// Derive domain from URL if not provided
|
|
102
|
+
let domain: string;
|
|
103
|
+
try {
|
|
104
|
+
domain = body.domain ?? new URL(baseUrl).hostname;
|
|
105
|
+
} catch {
|
|
106
|
+
return c.json({ error: "Invalid URL" }, 400);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Publish token scope check
|
|
110
|
+
const auth = c.get("publishAuth" as never) as PublishAuthContext | undefined;
|
|
111
|
+
if (auth?.type === "publish" && auth.domain !== domain) {
|
|
112
|
+
return c.json(
|
|
113
|
+
{ error: `Token is scoped to "${auth.domain}", cannot register "${domain}"` },
|
|
114
|
+
403,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If specUrl is provided, use it directly
|
|
119
|
+
if (body.specUrl) {
|
|
120
|
+
try {
|
|
121
|
+
const result = await fetchAndCompile(body.specUrl, { domain });
|
|
122
|
+
await store.put(domain, result.record);
|
|
123
|
+
return c.json({
|
|
124
|
+
ok: true,
|
|
125
|
+
domain,
|
|
126
|
+
name: result.record.name,
|
|
127
|
+
endpoints: result.record.endpoints.length,
|
|
128
|
+
source: body.specUrl,
|
|
129
|
+
}, 201);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return c.json({ error: `Failed to compile spec: ${err instanceof Error ? err.message : err}` }, 400);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Auto-discover: probe well-known paths
|
|
136
|
+
for (const path of OPENAPI_PATHS) {
|
|
137
|
+
const specUrl = `${baseUrl}${path}`;
|
|
138
|
+
try {
|
|
139
|
+
const resp = await fetch(specUrl, { method: "GET", headers: { Accept: "application/json, application/yaml" } });
|
|
140
|
+
if (!resp.ok) continue;
|
|
141
|
+
|
|
142
|
+
const text = await resp.text();
|
|
143
|
+
if (!text.trim() || text.length < 20) continue;
|
|
144
|
+
|
|
145
|
+
// Try to compile — if it fails, try next path
|
|
146
|
+
try {
|
|
147
|
+
const result = await fetchAndCompile(specUrl, { domain });
|
|
148
|
+
await store.put(domain, result.record);
|
|
149
|
+
return c.json({
|
|
150
|
+
ok: true,
|
|
151
|
+
domain,
|
|
152
|
+
name: result.record.name,
|
|
153
|
+
endpoints: result.record.endpoints.length,
|
|
154
|
+
source: specUrl,
|
|
155
|
+
}, 201);
|
|
156
|
+
} catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return c.json({
|
|
165
|
+
error: "Could not find OpenAPI spec",
|
|
166
|
+
probed: OPENAPI_PATHS.map((p) => `${baseUrl}${p}`),
|
|
167
|
+
hint: "Use --spec-url to provide the spec location directly",
|
|
168
|
+
}, 404);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// List all services
|
|
172
|
+
app.get("/services", async (c) => {
|
|
173
|
+
const query = c.req.query("q");
|
|
174
|
+
if (query) {
|
|
175
|
+
const results = await store.search(query);
|
|
176
|
+
return c.json(results);
|
|
177
|
+
}
|
|
178
|
+
const list = await store.list();
|
|
179
|
+
return c.json(list);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Get service details
|
|
183
|
+
app.get("/services/:domain", async (c) => {
|
|
184
|
+
const domain = c.req.param("domain");
|
|
185
|
+
const record = await store.get(domain);
|
|
186
|
+
if (!record) {
|
|
187
|
+
return c.json({ error: "Service not found" }, 404);
|
|
188
|
+
}
|
|
189
|
+
return c.json(record);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// List versions of a service
|
|
193
|
+
app.get("/services/:domain/versions", async (c) => {
|
|
194
|
+
const domain = c.req.param("domain");
|
|
195
|
+
const versions = await store.listVersions(domain);
|
|
196
|
+
return c.json({ domain, versions });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Get specific version of a service
|
|
200
|
+
app.get("/services/:domain/versions/:version", async (c) => {
|
|
201
|
+
const domain = c.req.param("domain");
|
|
202
|
+
const version = c.req.param("version");
|
|
203
|
+
const record = await store.getVersion(domain, version);
|
|
204
|
+
if (!record) {
|
|
205
|
+
return c.json({ error: "Version not found" }, 404);
|
|
206
|
+
}
|
|
207
|
+
return c.json(record);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Delete a service
|
|
211
|
+
app.delete("/services/:domain", async (c) => {
|
|
212
|
+
const domain = c.req.param("domain");
|
|
213
|
+
const existing = await store.get(domain);
|
|
214
|
+
if (!existing) {
|
|
215
|
+
return c.json({ error: "Service not found" }, 404);
|
|
216
|
+
}
|
|
217
|
+
await store.delete(domain);
|
|
218
|
+
return c.json({ ok: true, domain });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return app;
|
|
222
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../app.js";
|
|
3
|
+
import type { TunnelStore, TunnelProvider } from "../../tunnel/types.js";
|
|
4
|
+
import { nanoid } from "nanoid";
|
|
5
|
+
|
|
6
|
+
export interface TunnelRouteOptions {
|
|
7
|
+
tunnelStore: TunnelStore;
|
|
8
|
+
tunnelProvider: TunnelProvider;
|
|
9
|
+
tunnelDomain: string; // e.g. "tunnel.example.com"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function tunnelRoutes(options: TunnelRouteOptions) {
|
|
13
|
+
const { tunnelStore, tunnelProvider, tunnelDomain } = options;
|
|
14
|
+
const app = new Hono<Env>();
|
|
15
|
+
|
|
16
|
+
// POST /tunnels/create — create a tunnel for the authenticated agent
|
|
17
|
+
app.post("/create", async (c) => {
|
|
18
|
+
const agent = c.get("agent");
|
|
19
|
+
const body = await c.req.json<{
|
|
20
|
+
advertisedDomains?: string[];
|
|
21
|
+
gatewayName?: string;
|
|
22
|
+
}>().catch(() => ({} as { advertisedDomains?: string[]; gatewayName?: string }));
|
|
23
|
+
|
|
24
|
+
// Check if agent already has a tunnel
|
|
25
|
+
const existing = await tunnelStore.getByAgent(agent.id);
|
|
26
|
+
if (existing && existing.status === "active") {
|
|
27
|
+
return c.json({
|
|
28
|
+
tunnelId: existing.id,
|
|
29
|
+
publicUrl: existing.publicUrl,
|
|
30
|
+
message: "Tunnel already exists",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const id = nanoid(12);
|
|
35
|
+
const hostname = `${id}.${tunnelDomain}`;
|
|
36
|
+
const publicUrl = `https://${hostname}`;
|
|
37
|
+
|
|
38
|
+
// Create via Cloudflare API
|
|
39
|
+
const { tunnelId, tunnelToken } = await tunnelProvider.create(
|
|
40
|
+
`nkmc-${agent.id}-${id}`,
|
|
41
|
+
hostname,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
|
|
46
|
+
// Store record
|
|
47
|
+
await tunnelStore.put({
|
|
48
|
+
id,
|
|
49
|
+
agentId: agent.id,
|
|
50
|
+
tunnelId,
|
|
51
|
+
publicUrl,
|
|
52
|
+
status: "active",
|
|
53
|
+
createdAt: now,
|
|
54
|
+
advertisedDomains: body.advertisedDomains ?? [],
|
|
55
|
+
gatewayName: body.gatewayName,
|
|
56
|
+
lastSeen: now,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return c.json({ tunnelId: id, tunnelToken, publicUrl }, 201);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// DELETE /tunnels/:id — delete a tunnel
|
|
63
|
+
app.delete("/:id", async (c) => {
|
|
64
|
+
const id = c.req.param("id");
|
|
65
|
+
const agent = c.get("agent");
|
|
66
|
+
|
|
67
|
+
const record = await tunnelStore.get(id);
|
|
68
|
+
if (!record) return c.json({ error: "Tunnel not found" }, 404);
|
|
69
|
+
if (record.agentId !== agent.id)
|
|
70
|
+
return c.json({ error: "Not your tunnel" }, 403);
|
|
71
|
+
|
|
72
|
+
await tunnelProvider.delete(record.tunnelId);
|
|
73
|
+
await tunnelStore.delete(id);
|
|
74
|
+
|
|
75
|
+
return c.json({ ok: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// GET /tunnels — list agent's tunnels
|
|
79
|
+
app.get("/", async (c) => {
|
|
80
|
+
const agent = c.get("agent");
|
|
81
|
+
const all = await tunnelStore.list();
|
|
82
|
+
const mine = all.filter((t) => t.agentId === agent.id);
|
|
83
|
+
return c.json({ tunnels: mine });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// GET /tunnels/discover — list all online gateways (public info only)
|
|
87
|
+
// Optional: ?domain=api.openai.com — filter by advertised domain
|
|
88
|
+
app.get("/discover", async (c) => {
|
|
89
|
+
const domain = c.req.query("domain");
|
|
90
|
+
const all = await tunnelStore.list();
|
|
91
|
+
|
|
92
|
+
let results = all.filter((t) => t.status === "active");
|
|
93
|
+
if (domain) {
|
|
94
|
+
results = results.filter((t) => t.advertisedDomains.includes(domain));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Return public info only — no tunnelToken, no internal IDs
|
|
98
|
+
return c.json({
|
|
99
|
+
gateways: results.map((t) => ({
|
|
100
|
+
id: t.id,
|
|
101
|
+
name: t.gatewayName ?? `gateway-${t.id}`,
|
|
102
|
+
publicUrl: t.publicUrl,
|
|
103
|
+
advertisedDomains: t.advertisedDomains,
|
|
104
|
+
})),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// POST /tunnels/heartbeat — update advertised domains and confirm online
|
|
109
|
+
app.post("/heartbeat", async (c) => {
|
|
110
|
+
const agent = c.get("agent");
|
|
111
|
+
const body = await c.req.json<{ advertisedDomains?: string[] }>();
|
|
112
|
+
|
|
113
|
+
const record = await tunnelStore.getByAgent(agent.id);
|
|
114
|
+
if (!record) return c.json({ error: "No active tunnel" }, 404);
|
|
115
|
+
|
|
116
|
+
record.advertisedDomains = body.advertisedDomains ?? record.advertisedDomains;
|
|
117
|
+
record.lastSeen = Date.now();
|
|
118
|
+
await tunnelStore.put(record);
|
|
119
|
+
|
|
120
|
+
return c.json({ ok: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return app;
|
|
124
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createGateway, type GatewayOptions, type Env } from "./http/app.js";
|
|
2
|
+
export { adminAuth } from "./http/middleware/admin-auth.js";
|
|
3
|
+
export { publishOrAdminAuth, type PublishAuthContext } from "./http/middleware/publish-auth.js";
|
|
4
|
+
export { agentAuth } from "./http/middleware/agent-auth.js";
|
|
5
|
+
export { authRoutes, type AuthRouteOptions } from "./http/routes/auth.js";
|
|
6
|
+
export { registryRoutes, type RegistryRouteOptions } from "./http/routes/registry.js";
|
|
7
|
+
export { domainRoutes, type DomainRouteOptions } from "./http/routes/domains.js";
|
|
8
|
+
export { fsRoutes, type FsRouteOptions } from "./http/routes/fs.js";
|
|
9
|
+
export { tunnelRoutes, type TunnelRouteOptions } from "./http/routes/tunnels.js";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const VERSION = "0.1.0";
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
EndpointSummary,
|
|
5
|
+
EndpointRecord,
|
|
6
|
+
EndpointPricing,
|
|
7
|
+
EndpointAnnotations,
|
|
8
|
+
ServiceRecord,
|
|
9
|
+
SearchResult,
|
|
10
|
+
ServiceSummary,
|
|
11
|
+
ServiceStatus,
|
|
12
|
+
VersionSummary,
|
|
13
|
+
SourceConfig,
|
|
14
|
+
RegistryStore,
|
|
15
|
+
} from "./registry/types.js";
|
|
16
|
+
|
|
17
|
+
export { MemoryRegistryStore } from "./registry/memory-store.js";
|
|
18
|
+
export { D1RegistryStore } from "./registry/d1-store.js";
|
|
19
|
+
export { parseSkillMd, parsePricingAnnotation, type ParseOptions } from "./registry/skill-parser.js";
|
|
20
|
+
export { skillToHttpConfig } from "./registry/skill-to-config.js";
|
|
21
|
+
export {
|
|
22
|
+
createRegistryResolver,
|
|
23
|
+
extractDomainPath,
|
|
24
|
+
type RegistryResolverHooks,
|
|
25
|
+
type RegistryResolverOptions,
|
|
26
|
+
} from "./registry/resolver.js";
|
|
27
|
+
|
|
28
|
+
export type { MeterRecord, MeterQuery, MeterStore } from "./metering/types.js";
|
|
29
|
+
export { MemoryMeterStore } from "./metering/memory-store.js";
|
|
30
|
+
export { D1MeterStore } from "./metering/d1-store.js";
|
|
31
|
+
export { lookupPricing, checkAccess, meter } from "./metering/pricing-guard.js";
|
|
32
|
+
export { VirtualFileBackend } from "./registry/virtual-files.js";
|
|
33
|
+
|
|
34
|
+
export type { StoredCredential, CredentialVault } from "./credential/types.js";
|
|
35
|
+
export { MemoryCredentialVault } from "./credential/memory-vault.js";
|
|
36
|
+
export { D1CredentialVault } from "./credential/d1-vault.js";
|
|
37
|
+
export { credentialRoutes } from "./http/routes/credentials.js";
|
|
38
|
+
|
|
39
|
+
export { queryDnsTxt } from "./http/lib/dns.js";
|
|
40
|
+
export { Context7Client, type Context7Options, type LibrarySearchResult } from "./registry/context7.js";
|
|
41
|
+
export { Context7Backend, type Context7BackendOptions } from "./registry/context7-backend.js";
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
OnboardPipeline,
|
|
45
|
+
discoverFromApisGuru,
|
|
46
|
+
type PipelineOptions,
|
|
47
|
+
type ApisGuruOptions,
|
|
48
|
+
type ManifestEntry,
|
|
49
|
+
type ManifestAuth,
|
|
50
|
+
type OnboardResult,
|
|
51
|
+
type OnboardReport,
|
|
52
|
+
} from "./onboard/index.js";
|
|
53
|
+
|
|
54
|
+
export type { D1Database, D1PreparedStatement, D1Result, D1RunResult } from "./d1/types.js";
|
|
55
|
+
export { createSqliteD1 } from "./d1/sqlite-adapter.js";
|
|
56
|
+
|
|
57
|
+
export type { PeerGateway, LendingRule, PeerStore } from "./federation/types.js";
|
|
58
|
+
export { D1PeerStore } from "./federation/d1-peer-store.js";
|
|
59
|
+
|
|
60
|
+
export type { TunnelRecord, TunnelStore, TunnelProvider } from "./tunnel/types.js";
|
|
61
|
+
export { MemoryTunnelStore } from "./tunnel/memory-store.js";
|
|
62
|
+
export { CloudflareTunnelProvider } from "./tunnel/cloudflare-provider.js";
|
|
63
|
+
export { tunnelRoutes, type TunnelRouteOptions } from "./http/routes/tunnels.js";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { D1Database } from "../d1/types.js";
|
|
2
|
+
import type { MeterRecord, MeterQuery, MeterStore } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const CREATE_METER_RECORDS = `
|
|
5
|
+
CREATE TABLE IF NOT EXISTS meter_records (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
timestamp INTEGER NOT NULL,
|
|
8
|
+
domain TEXT NOT NULL,
|
|
9
|
+
version TEXT NOT NULL,
|
|
10
|
+
endpoint TEXT NOT NULL,
|
|
11
|
+
agent_id TEXT NOT NULL,
|
|
12
|
+
developer_id TEXT,
|
|
13
|
+
cost REAL NOT NULL,
|
|
14
|
+
currency TEXT NOT NULL DEFAULT 'USDC'
|
|
15
|
+
)`;
|
|
16
|
+
|
|
17
|
+
const CREATE_METER_INDEX_DOMAIN = `
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_meter_domain ON meter_records(domain, timestamp)`;
|
|
19
|
+
|
|
20
|
+
const CREATE_METER_INDEX_AGENT = `
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_meter_agent ON meter_records(agent_id, timestamp)`;
|
|
22
|
+
|
|
23
|
+
interface MeterRow {
|
|
24
|
+
id: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
domain: string;
|
|
27
|
+
version: string;
|
|
28
|
+
endpoint: string;
|
|
29
|
+
agent_id: string;
|
|
30
|
+
developer_id: string | null;
|
|
31
|
+
cost: number;
|
|
32
|
+
currency: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class D1MeterStore implements MeterStore {
|
|
36
|
+
constructor(private db: D1Database) {}
|
|
37
|
+
|
|
38
|
+
async initSchema(): Promise<void> {
|
|
39
|
+
await this.db.exec(CREATE_METER_RECORDS);
|
|
40
|
+
await this.db.exec(CREATE_METER_INDEX_DOMAIN);
|
|
41
|
+
await this.db.exec(CREATE_METER_INDEX_AGENT);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async record(entry: MeterRecord): Promise<void> {
|
|
45
|
+
await this.db
|
|
46
|
+
.prepare(
|
|
47
|
+
`INSERT INTO meter_records (id, timestamp, domain, version, endpoint, agent_id, developer_id, cost, currency)
|
|
48
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
49
|
+
)
|
|
50
|
+
.bind(
|
|
51
|
+
entry.id,
|
|
52
|
+
entry.timestamp,
|
|
53
|
+
entry.domain,
|
|
54
|
+
entry.version,
|
|
55
|
+
entry.endpoint,
|
|
56
|
+
entry.agentId,
|
|
57
|
+
entry.developerId ?? null,
|
|
58
|
+
entry.cost,
|
|
59
|
+
entry.currency,
|
|
60
|
+
)
|
|
61
|
+
.run();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async query(filter: MeterQuery): Promise<MeterRecord[]> {
|
|
65
|
+
const { sql, bindings } = this.buildQuery("SELECT *", filter);
|
|
66
|
+
const { results } = await this.db.prepare(sql).bind(...bindings).all<MeterRow>();
|
|
67
|
+
return results.map(rowToRecord);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async sum(filter: MeterQuery): Promise<{ total: number; currency: string }> {
|
|
71
|
+
const { sql, bindings } = this.buildQuery("SELECT COALESCE(SUM(cost), 0) as total, COALESCE(MIN(currency), 'USDC') as currency", filter);
|
|
72
|
+
const row = await this.db.prepare(sql).bind(...bindings).first<{ total: number; currency: string }>();
|
|
73
|
+
return { total: row?.total ?? 0, currency: row?.currency ?? "USDC" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private buildQuery(select: string, filter: MeterQuery): { sql: string; bindings: unknown[] } {
|
|
77
|
+
const conditions: string[] = [];
|
|
78
|
+
const bindings: unknown[] = [];
|
|
79
|
+
|
|
80
|
+
if (filter.domain) {
|
|
81
|
+
conditions.push("domain = ?");
|
|
82
|
+
bindings.push(filter.domain);
|
|
83
|
+
}
|
|
84
|
+
if (filter.agentId) {
|
|
85
|
+
conditions.push("agent_id = ?");
|
|
86
|
+
bindings.push(filter.agentId);
|
|
87
|
+
}
|
|
88
|
+
if (filter.developerId) {
|
|
89
|
+
conditions.push("developer_id = ?");
|
|
90
|
+
bindings.push(filter.developerId);
|
|
91
|
+
}
|
|
92
|
+
if (filter.from) {
|
|
93
|
+
conditions.push("timestamp >= ?");
|
|
94
|
+
bindings.push(filter.from);
|
|
95
|
+
}
|
|
96
|
+
if (filter.to) {
|
|
97
|
+
conditions.push("timestamp <= ?");
|
|
98
|
+
bindings.push(filter.to);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let sql = `${select} FROM meter_records`;
|
|
102
|
+
if (conditions.length > 0) {
|
|
103
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
104
|
+
}
|
|
105
|
+
sql += " ORDER BY timestamp DESC";
|
|
106
|
+
|
|
107
|
+
return { sql, bindings };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function rowToRecord(row: MeterRow): MeterRecord {
|
|
112
|
+
return {
|
|
113
|
+
id: row.id,
|
|
114
|
+
timestamp: row.timestamp,
|
|
115
|
+
domain: row.domain,
|
|
116
|
+
version: row.version,
|
|
117
|
+
endpoint: row.endpoint,
|
|
118
|
+
agentId: row.agent_id,
|
|
119
|
+
...(row.developer_id ? { developerId: row.developer_id } : {}),
|
|
120
|
+
cost: row.cost,
|
|
121
|
+
currency: row.currency,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MeterRecord, MeterQuery, MeterStore } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export class MemoryMeterStore implements MeterStore {
|
|
4
|
+
private records: MeterRecord[] = [];
|
|
5
|
+
|
|
6
|
+
async record(entry: MeterRecord): Promise<void> {
|
|
7
|
+
this.records.push(entry);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async query(filter: MeterQuery): Promise<MeterRecord[]> {
|
|
11
|
+
return this.records.filter((r) => this.matches(r, filter));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async sum(filter: MeterQuery): Promise<{ total: number; currency: string }> {
|
|
15
|
+
const matched = this.records.filter((r) => this.matches(r, filter));
|
|
16
|
+
const total = matched.reduce((acc, r) => acc + r.cost, 0);
|
|
17
|
+
const currency = matched[0]?.currency ?? "USDC";
|
|
18
|
+
return { total, currency };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private matches(record: MeterRecord, filter: MeterQuery): boolean {
|
|
22
|
+
if (filter.domain && record.domain !== filter.domain) return false;
|
|
23
|
+
if (filter.agentId && record.agentId !== filter.agentId) return false;
|
|
24
|
+
if (filter.developerId && record.developerId !== filter.developerId) return false;
|
|
25
|
+
if (filter.from && record.timestamp < filter.from) return false;
|
|
26
|
+
if (filter.to && record.timestamp > filter.to) return false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { EndpointPricing, ServiceRecord } from "../registry/types.js";
|
|
2
|
+
import type { MeterStore, MeterRecord } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function lookupPricing(
|
|
5
|
+
record: ServiceRecord,
|
|
6
|
+
method: string,
|
|
7
|
+
path: string,
|
|
8
|
+
): EndpointPricing | null {
|
|
9
|
+
// Find matching endpoint with pricing
|
|
10
|
+
for (const ep of record.endpoints) {
|
|
11
|
+
if (ep.method.toUpperCase() !== method.toUpperCase()) continue;
|
|
12
|
+
if (matchPath(ep.path, path)) {
|
|
13
|
+
return ep.pricing ?? null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function checkAccess(record: ServiceRecord): { allowed: boolean; reason?: string } {
|
|
20
|
+
if (record.status === "sunset") {
|
|
21
|
+
return { allowed: false, reason: "Service has been sunset" };
|
|
22
|
+
}
|
|
23
|
+
if (record.sunsetDate && record.sunsetDate < Date.now()) {
|
|
24
|
+
return { allowed: false, reason: "Service sunset date has passed" };
|
|
25
|
+
}
|
|
26
|
+
return { allowed: true };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function meter(
|
|
30
|
+
store: MeterStore,
|
|
31
|
+
opts: {
|
|
32
|
+
domain: string;
|
|
33
|
+
version: string;
|
|
34
|
+
endpoint: string;
|
|
35
|
+
agentId: string;
|
|
36
|
+
developerId?: string;
|
|
37
|
+
pricing: EndpointPricing;
|
|
38
|
+
},
|
|
39
|
+
): Promise<MeterRecord> {
|
|
40
|
+
const entry: MeterRecord = {
|
|
41
|
+
id: `m_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
domain: opts.domain,
|
|
44
|
+
version: opts.version,
|
|
45
|
+
endpoint: opts.endpoint,
|
|
46
|
+
agentId: opts.agentId,
|
|
47
|
+
developerId: opts.developerId,
|
|
48
|
+
cost: opts.pricing.cost,
|
|
49
|
+
currency: opts.pricing.currency,
|
|
50
|
+
};
|
|
51
|
+
await store.record(entry);
|
|
52
|
+
return entry;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function matchPath(pattern: string, actual: string): boolean {
|
|
56
|
+
// Support :param pattern matching
|
|
57
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
58
|
+
const actualParts = actual.split("/").filter(Boolean);
|
|
59
|
+
|
|
60
|
+
if (patternParts.length !== actualParts.length) return false;
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
63
|
+
if (patternParts[i].startsWith(":")) continue; // wildcard
|
|
64
|
+
if (patternParts[i] !== actualParts[i]) return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface MeterRecord {
|
|
2
|
+
id: string;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
domain: string;
|
|
5
|
+
version: string;
|
|
6
|
+
endpoint: string;
|
|
7
|
+
agentId: string;
|
|
8
|
+
developerId?: string;
|
|
9
|
+
cost: number;
|
|
10
|
+
currency: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MeterQuery {
|
|
14
|
+
domain?: string;
|
|
15
|
+
agentId?: string;
|
|
16
|
+
developerId?: string;
|
|
17
|
+
from?: number;
|
|
18
|
+
to?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MeterStore {
|
|
22
|
+
record(entry: MeterRecord): Promise<void>;
|
|
23
|
+
query(filter: MeterQuery): Promise<MeterRecord[]>;
|
|
24
|
+
sum(filter: MeterQuery): Promise<{ total: number; currency: string }>;
|
|
25
|
+
}
|