@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.
Files changed (130) hide show
  1. package/dist/chunk-56RA53VS.js +37 -0
  2. package/dist/chunk-CZJ75YTV.js +969 -0
  3. package/dist/chunk-QGM4M3NI.js +37 -0
  4. package/dist/http.cjs +1772 -0
  5. package/dist/http.d.cts +49 -0
  6. package/dist/http.d.ts +49 -0
  7. package/dist/http.js +748 -0
  8. package/dist/index.cjs +2436 -0
  9. package/dist/index.d.cts +436 -0
  10. package/dist/index.d.ts +436 -0
  11. package/dist/index.js +1434 -0
  12. package/dist/proxy-ClPcDgsO.d.cts +283 -0
  13. package/dist/proxy-qpda1ANS.d.ts +283 -0
  14. package/dist/proxy.cjs +148 -0
  15. package/dist/proxy.d.cts +6 -0
  16. package/dist/proxy.d.ts +6 -0
  17. package/dist/proxy.js +90 -0
  18. package/dist/testing.cjs +865 -0
  19. package/dist/testing.d.cts +12 -0
  20. package/dist/testing.d.ts +12 -0
  21. package/dist/testing.js +831 -0
  22. package/dist/tunnels-BviBEaih.d.cts +12 -0
  23. package/dist/tunnels-DFHNgmN7.d.ts +12 -0
  24. package/dist/types-C6JC9oTm.d.cts +21 -0
  25. package/dist/types-C6JC9oTm.d.ts +21 -0
  26. package/package.json +47 -0
  27. package/src/__tests__/sqlite-integration.test.ts +384 -0
  28. package/src/credential/d1-vault.ts +134 -0
  29. package/src/credential/memory-vault.ts +50 -0
  30. package/src/credential/types.ts +16 -0
  31. package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
  32. package/src/d1/sqlite-adapter.ts +59 -0
  33. package/src/d1/types.ts +22 -0
  34. package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
  35. package/src/federation/__tests__/peer-client.test.ts +205 -0
  36. package/src/federation/__tests__/peer-store.test.ts +114 -0
  37. package/src/federation/d1-peer-store.ts +164 -0
  38. package/src/federation/peer-backend.ts +60 -0
  39. package/src/federation/peer-client.ts +122 -0
  40. package/src/federation/peer-store.ts +45 -0
  41. package/src/federation/types.ts +39 -0
  42. package/src/http/app.ts +152 -0
  43. package/src/http/lib/dns.ts +30 -0
  44. package/src/http/middleware/admin-auth.ts +18 -0
  45. package/src/http/middleware/agent-auth.ts +27 -0
  46. package/src/http/middleware/publish-auth.ts +39 -0
  47. package/src/http/routes/__tests__/federation.test.ts +364 -0
  48. package/src/http/routes/__tests__/peers.test.ts +290 -0
  49. package/src/http/routes/__tests__/proxy.test.ts +159 -0
  50. package/src/http/routes/auth.ts +39 -0
  51. package/src/http/routes/byok.ts +62 -0
  52. package/src/http/routes/credentials.ts +40 -0
  53. package/src/http/routes/domains.ts +174 -0
  54. package/src/http/routes/federation.ts +170 -0
  55. package/src/http/routes/fs.ts +89 -0
  56. package/src/http/routes/peers.ts +103 -0
  57. package/src/http/routes/proxy.ts +57 -0
  58. package/src/http/routes/registry.ts +222 -0
  59. package/src/http/routes/tunnels.ts +124 -0
  60. package/src/http.ts +9 -0
  61. package/src/index.ts +63 -0
  62. package/src/metering/d1-store.ts +123 -0
  63. package/src/metering/memory-store.ts +29 -0
  64. package/src/metering/pricing-guard.ts +68 -0
  65. package/src/metering/types.ts +25 -0
  66. package/src/onboard/apis-guru.ts +64 -0
  67. package/src/onboard/index.ts +4 -0
  68. package/src/onboard/manifest.ts +362 -0
  69. package/src/onboard/pipeline.ts +214 -0
  70. package/src/onboard/types.ts +72 -0
  71. package/src/proxy/__tests__/tool-registry.test.ts +93 -0
  72. package/src/proxy/tool-registry.ts +122 -0
  73. package/src/proxy.ts +12 -0
  74. package/src/registry/context7-backend.ts +93 -0
  75. package/src/registry/context7.ts +54 -0
  76. package/src/registry/d1-store.ts +242 -0
  77. package/src/registry/memory-store.ts +101 -0
  78. package/src/registry/openapi-compiler.ts +284 -0
  79. package/src/registry/resolver.ts +196 -0
  80. package/src/registry/rpc-compiler.ts +142 -0
  81. package/src/registry/skill-parser.ts +119 -0
  82. package/src/registry/skill-to-config.ts +239 -0
  83. package/src/registry/source-refresher.ts +83 -0
  84. package/src/registry/types.ts +129 -0
  85. package/src/registry/virtual-files.ts +76 -0
  86. package/src/testing/sqlite-d1.ts +64 -0
  87. package/src/testing.ts +2 -0
  88. package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
  89. package/src/tunnel/__tests__/tunnel.test.ts +542 -0
  90. package/src/tunnel/cloudflare-provider.ts +121 -0
  91. package/src/tunnel/memory-store.ts +30 -0
  92. package/src/tunnel/types.ts +28 -0
  93. package/test/credential/d1-vault.test.ts +127 -0
  94. package/test/credential/injection.test.ts +67 -0
  95. package/test/credential/memory-vault.test.ts +63 -0
  96. package/test/http/app.test.ts +300 -0
  97. package/test/http/byok-e2e.test.ts +240 -0
  98. package/test/http/byok.test.ts +115 -0
  99. package/test/http/credentials.test.ts +57 -0
  100. package/test/http/e2e.test.ts +260 -0
  101. package/test/integration/authenticated-apis.test.ts +185 -0
  102. package/test/integration/free-apis-e2e.test.ts +222 -0
  103. package/test/metering/d1-store.test.ts +82 -0
  104. package/test/metering/memory-store.test.ts +76 -0
  105. package/test/metering/pricing-guard.test.ts +108 -0
  106. package/test/onboard/apis-guru.test.ts +57 -0
  107. package/test/onboard/e2e.test.ts +70 -0
  108. package/test/onboard/pipeline.test.ts +318 -0
  109. package/test/onboard/real-apis.test.ts +483 -0
  110. package/test/registry/compilation-correctness.test.ts +132 -0
  111. package/test/registry/context7-backend.test.ts +88 -0
  112. package/test/registry/context7-e2e.test.ts +92 -0
  113. package/test/registry/context7.test.ts +73 -0
  114. package/test/registry/d1-store.test.ts +184 -0
  115. package/test/registry/integration.test.ts +129 -0
  116. package/test/registry/lazy-mount.test.ts +138 -0
  117. package/test/registry/memory-store.test.ts +171 -0
  118. package/test/registry/openapi-compiler.test.ts +267 -0
  119. package/test/registry/openapi-e2e.test.ts +154 -0
  120. package/test/registry/passthrough-e2e.test.ts +109 -0
  121. package/test/registry/resolver-peer.test.ts +299 -0
  122. package/test/registry/resolver.test.ts +228 -0
  123. package/test/registry/rpc-compiler.test.ts +112 -0
  124. package/test/registry/skill-parser.test.ts +151 -0
  125. package/test/registry/skill-to-config.test.ts +151 -0
  126. package/test/registry/skill-to-rpc-config.test.ts +142 -0
  127. package/test/registry/source-refresher.test.ts +90 -0
  128. package/test/registry/virtual-files.test.ts +96 -0
  129. package/tsconfig.json +4 -0
  130. package/tsup.config.ts +8 -0
package/dist/http.js ADDED
@@ -0,0 +1,748 @@
1
+ import {
2
+ Context7Backend,
3
+ createRegistryResolver,
4
+ credentialRoutes,
5
+ fetchAndCompile,
6
+ parseSkillMd,
7
+ queryDnsTxt,
8
+ tunnelRoutes
9
+ } from "./chunk-CZJ75YTV.js";
10
+ import {
11
+ proxyRoutes
12
+ } from "./chunk-56RA53VS.js";
13
+ import "./chunk-QGM4M3NI.js";
14
+
15
+ // src/http/app.ts
16
+ import { Hono as Hono8 } from "hono";
17
+ import { AgentFs } from "@nkmc/agent-fs";
18
+
19
+ // src/http/middleware/admin-auth.ts
20
+ import { createMiddleware } from "hono/factory";
21
+ function adminAuth(adminToken) {
22
+ return createMiddleware(async (c, next) => {
23
+ const auth = c.req.header("Authorization");
24
+ if (!auth || !auth.startsWith("Bearer ")) {
25
+ return c.json({ error: "Missing Authorization header" }, 401);
26
+ }
27
+ const token = auth.slice(7);
28
+ if (token !== adminToken) {
29
+ return c.json({ error: "Invalid admin token" }, 403);
30
+ }
31
+ await next();
32
+ });
33
+ }
34
+
35
+ // src/http/middleware/publish-auth.ts
36
+ import { createMiddleware as createMiddleware2 } from "hono/factory";
37
+ import { verifyPublishToken } from "@nkmc/core";
38
+ function publishOrAdminAuth(adminToken, publicKey) {
39
+ return createMiddleware2(async (c, next) => {
40
+ const auth = c.req.header("Authorization");
41
+ if (!auth || !auth.startsWith("Bearer ")) {
42
+ return c.json({ error: "Missing Authorization header" }, 401);
43
+ }
44
+ const token = auth.slice(7);
45
+ if (token === adminToken) {
46
+ c.set("publishAuth", { type: "admin" });
47
+ return next();
48
+ }
49
+ try {
50
+ const payload = await verifyPublishToken(token, publicKey);
51
+ c.set("publishAuth", { type: "publish", domain: payload.sub });
52
+ return next();
53
+ } catch {
54
+ return c.json({ error: "Invalid token" }, 403);
55
+ }
56
+ });
57
+ }
58
+
59
+ // src/http/middleware/agent-auth.ts
60
+ import { createMiddleware as createMiddleware3 } from "hono/factory";
61
+ import { verifyJwt } from "@nkmc/core";
62
+ function agentAuth(publicKey) {
63
+ return createMiddleware3(async (c, next) => {
64
+ const auth = c.req.header("Authorization");
65
+ if (!auth || !auth.startsWith("Bearer ")) {
66
+ return c.json({ error: "Missing Authorization header" }, 401);
67
+ }
68
+ const token = auth.slice(7);
69
+ try {
70
+ const payload = await verifyJwt(token, publicKey);
71
+ c.set("agent", { id: payload.sub, roles: payload.roles });
72
+ } catch (err) {
73
+ const message = err instanceof Error ? err.message : "Invalid token";
74
+ if (message.includes("exp") || message.includes("expired")) {
75
+ return c.json({ error: "Token has expired" }, 401);
76
+ }
77
+ return c.json({ error: "Invalid token" }, 401);
78
+ }
79
+ await next();
80
+ });
81
+ }
82
+
83
+ // src/http/routes/auth.ts
84
+ import { Hono } from "hono";
85
+ import { signJwt } from "@nkmc/core";
86
+ function authRoutes(options) {
87
+ const app = new Hono();
88
+ app.post("/token", async (c) => {
89
+ const body = await c.req.json();
90
+ if (!body.sub || !body.svc) {
91
+ return c.json({ error: "Missing required fields: sub, svc" }, 400);
92
+ }
93
+ const token = await signJwt(
94
+ options.privateKey,
95
+ {
96
+ sub: body.sub,
97
+ roles: body.roles ?? ["agent"],
98
+ svc: body.svc
99
+ },
100
+ body.expiresIn ? { expiresIn: body.expiresIn } : void 0
101
+ );
102
+ return c.json({ token });
103
+ });
104
+ return app;
105
+ }
106
+
107
+ // src/http/routes/registry.ts
108
+ import { Hono as Hono2 } from "hono";
109
+ var OPENAPI_PATHS = [
110
+ "/openapi.json",
111
+ "/openapi.yaml",
112
+ "/swagger.json",
113
+ "/swagger.yaml",
114
+ "/docs/openapi.json",
115
+ "/api-docs",
116
+ "/api/openapi.json",
117
+ "/.well-known/openapi.json",
118
+ "/.well-known/openapi.yaml"
119
+ ];
120
+ function registryRoutes(options) {
121
+ const { store } = options;
122
+ const app = new Hono2();
123
+ app.post("/services", async (c) => {
124
+ const domain = c.req.query("domain");
125
+ if (!domain) {
126
+ return c.json({ error: "Missing ?domain= query parameter" }, 400);
127
+ }
128
+ const auth = c.get("publishAuth");
129
+ if (auth?.type === "publish" && auth.domain !== domain) {
130
+ return c.json(
131
+ { error: `Token is scoped to "${auth.domain}", cannot register "${domain}"` },
132
+ 403
133
+ );
134
+ }
135
+ const contentType = c.req.header("Content-Type") ?? "";
136
+ let skillMd;
137
+ if (contentType.includes("text/markdown") || contentType.includes("text/plain")) {
138
+ skillMd = await c.req.text();
139
+ } else {
140
+ const body = await c.req.json().catch(() => null);
141
+ if (!body?.skillMd) {
142
+ return c.json({ error: "Body must be skill.md text or JSON with skillMd field" }, 400);
143
+ }
144
+ skillMd = body.skillMd;
145
+ if (Array.isArray(body.endpoints) && body.endpoints.length > 0) {
146
+ if (!skillMd.trim()) {
147
+ return c.json({ error: "Empty skill.md content" }, 400);
148
+ }
149
+ const isFirstParty2 = c.req.query("first_party") === "true";
150
+ const authMode2 = c.req.query("auth_mode");
151
+ const record2 = parseSkillMd(domain, skillMd, { isFirstParty: isFirstParty2 });
152
+ record2.endpoints = body.endpoints;
153
+ if (body.source) {
154
+ record2.source = body.source;
155
+ }
156
+ if (authMode2 === "nkmc-jwt") {
157
+ record2.authMode = "nkmc-jwt";
158
+ }
159
+ await store.put(domain, record2);
160
+ return c.json({ ok: true, domain, name: record2.name }, 201);
161
+ }
162
+ }
163
+ if (!skillMd.trim()) {
164
+ return c.json({ error: "Empty skill.md content" }, 400);
165
+ }
166
+ const isFirstParty = c.req.query("first_party") === "true";
167
+ const authMode = c.req.query("auth_mode");
168
+ const record = parseSkillMd(domain, skillMd, { isFirstParty });
169
+ if (authMode === "nkmc-jwt") {
170
+ record.authMode = "nkmc-jwt";
171
+ }
172
+ await store.put(domain, record);
173
+ return c.json({ ok: true, domain, name: record.name }, 201);
174
+ });
175
+ app.post("/services/discover", async (c) => {
176
+ const body = await c.req.json().catch(() => null);
177
+ if (!body?.url) {
178
+ return c.json({ error: "Missing 'url' field (base URL of the service)" }, 400);
179
+ }
180
+ const baseUrl = body.url.replace(/\/+$/, "");
181
+ let domain;
182
+ try {
183
+ domain = body.domain ?? new URL(baseUrl).hostname;
184
+ } catch {
185
+ return c.json({ error: "Invalid URL" }, 400);
186
+ }
187
+ const auth = c.get("publishAuth");
188
+ if (auth?.type === "publish" && auth.domain !== domain) {
189
+ return c.json(
190
+ { error: `Token is scoped to "${auth.domain}", cannot register "${domain}"` },
191
+ 403
192
+ );
193
+ }
194
+ if (body.specUrl) {
195
+ try {
196
+ const result = await fetchAndCompile(body.specUrl, { domain });
197
+ await store.put(domain, result.record);
198
+ return c.json({
199
+ ok: true,
200
+ domain,
201
+ name: result.record.name,
202
+ endpoints: result.record.endpoints.length,
203
+ source: body.specUrl
204
+ }, 201);
205
+ } catch (err) {
206
+ return c.json({ error: `Failed to compile spec: ${err instanceof Error ? err.message : err}` }, 400);
207
+ }
208
+ }
209
+ for (const path of OPENAPI_PATHS) {
210
+ const specUrl = `${baseUrl}${path}`;
211
+ try {
212
+ const resp = await fetch(specUrl, { method: "GET", headers: { Accept: "application/json, application/yaml" } });
213
+ if (!resp.ok) continue;
214
+ const text = await resp.text();
215
+ if (!text.trim() || text.length < 20) continue;
216
+ try {
217
+ const result = await fetchAndCompile(specUrl, { domain });
218
+ await store.put(domain, result.record);
219
+ return c.json({
220
+ ok: true,
221
+ domain,
222
+ name: result.record.name,
223
+ endpoints: result.record.endpoints.length,
224
+ source: specUrl
225
+ }, 201);
226
+ } catch {
227
+ continue;
228
+ }
229
+ } catch {
230
+ continue;
231
+ }
232
+ }
233
+ return c.json({
234
+ error: "Could not find OpenAPI spec",
235
+ probed: OPENAPI_PATHS.map((p) => `${baseUrl}${p}`),
236
+ hint: "Use --spec-url to provide the spec location directly"
237
+ }, 404);
238
+ });
239
+ app.get("/services", async (c) => {
240
+ const query = c.req.query("q");
241
+ if (query) {
242
+ const results = await store.search(query);
243
+ return c.json(results);
244
+ }
245
+ const list = await store.list();
246
+ return c.json(list);
247
+ });
248
+ app.get("/services/:domain", async (c) => {
249
+ const domain = c.req.param("domain");
250
+ const record = await store.get(domain);
251
+ if (!record) {
252
+ return c.json({ error: "Service not found" }, 404);
253
+ }
254
+ return c.json(record);
255
+ });
256
+ app.get("/services/:domain/versions", async (c) => {
257
+ const domain = c.req.param("domain");
258
+ const versions = await store.listVersions(domain);
259
+ return c.json({ domain, versions });
260
+ });
261
+ app.get("/services/:domain/versions/:version", async (c) => {
262
+ const domain = c.req.param("domain");
263
+ const version = c.req.param("version");
264
+ const record = await store.getVersion(domain, version);
265
+ if (!record) {
266
+ return c.json({ error: "Version not found" }, 404);
267
+ }
268
+ return c.json(record);
269
+ });
270
+ app.delete("/services/:domain", async (c) => {
271
+ const domain = c.req.param("domain");
272
+ const existing = await store.get(domain);
273
+ if (!existing) {
274
+ return c.json({ error: "Service not found" }, 404);
275
+ }
276
+ await store.delete(domain);
277
+ return c.json({ ok: true, domain });
278
+ });
279
+ return app;
280
+ }
281
+
282
+ // src/http/routes/domains.ts
283
+ import { Hono as Hono3 } from "hono";
284
+ import { nanoid } from "nanoid";
285
+ import { signPublishToken } from "@nkmc/core";
286
+ var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
287
+ var ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
288
+ function isValidDomain(domain) {
289
+ return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+$/.test(domain);
290
+ }
291
+ function domainRoutes(options) {
292
+ const { db, gatewayPrivateKey } = options;
293
+ const app = new Hono3();
294
+ app.post("/challenge", async (c) => {
295
+ const body = await c.req.json().catch(() => null);
296
+ const domain = body?.domain;
297
+ if (!domain || !isValidDomain(domain)) {
298
+ return c.json({ error: "Invalid or missing domain" }, 400);
299
+ }
300
+ const now = Date.now();
301
+ const verified = await db.prepare(
302
+ "SELECT * FROM domain_challenges WHERE domain = ? AND status = 'verified' AND expires_at > ?"
303
+ ).bind(domain, now).first();
304
+ if (verified) {
305
+ return c.json(
306
+ {
307
+ error: "Domain already verified. Use `nkmc claim <domain> --verify` to renew your token.",
308
+ expiresAt: verified.expires_at
309
+ },
310
+ 409
311
+ );
312
+ }
313
+ const existing = await db.prepare(
314
+ "SELECT * FROM domain_challenges WHERE domain = ? AND status = 'pending' AND expires_at > ?"
315
+ ).bind(domain, now).first();
316
+ if (existing) {
317
+ return c.json({
318
+ domain,
319
+ txtRecord: `_nkmc.${domain}`,
320
+ txtValue: `nkmc-verify=${existing.challenge_code}`,
321
+ expiresAt: existing.expires_at
322
+ });
323
+ }
324
+ const challengeCode = nanoid(32);
325
+ const expiresAt = now + SEVEN_DAYS_MS;
326
+ await db.prepare(
327
+ `INSERT OR REPLACE INTO domain_challenges (domain, challenge_code, status, created_at, expires_at)
328
+ VALUES (?, ?, 'pending', ?, ?)`
329
+ ).bind(domain, challengeCode, now, expiresAt).run();
330
+ return c.json({
331
+ domain,
332
+ txtRecord: `_nkmc.${domain}`,
333
+ txtValue: `nkmc-verify=${challengeCode}`,
334
+ expiresAt
335
+ });
336
+ });
337
+ app.post("/verify", async (c) => {
338
+ const body = await c.req.json().catch(() => null);
339
+ const domain = body?.domain;
340
+ if (!domain || !isValidDomain(domain)) {
341
+ return c.json({ error: "Invalid or missing domain" }, 400);
342
+ }
343
+ const now = Date.now();
344
+ const verified = await db.prepare(
345
+ "SELECT * FROM domain_challenges WHERE domain = ? AND status = 'verified' AND expires_at > ?"
346
+ ).bind(domain, now).first();
347
+ if (verified) {
348
+ const publishToken2 = await signPublishToken(gatewayPrivateKey, domain);
349
+ return c.json({ ok: true, domain, publishToken: publishToken2 });
350
+ }
351
+ const challenge = await db.prepare(
352
+ "SELECT * FROM domain_challenges WHERE domain = ? AND status = 'pending' AND expires_at > ?"
353
+ ).bind(domain, now).first();
354
+ if (!challenge) {
355
+ return c.json(
356
+ { error: "No pending challenge found. Run `nkmc claim <domain>` first." },
357
+ 404
358
+ );
359
+ }
360
+ const expectedValue = `nkmc-verify=${challenge.challenge_code}`;
361
+ let txtRecords;
362
+ try {
363
+ txtRecords = await queryDnsTxt(`_nkmc.${domain}`);
364
+ } catch {
365
+ return c.json(
366
+ { error: "Failed to query DNS. Please try again later." },
367
+ 502
368
+ );
369
+ }
370
+ if (!txtRecords.includes(expectedValue)) {
371
+ return c.json(
372
+ {
373
+ error: `DNS TXT record not found. Expected TXT record on _nkmc.${domain} with value "${expectedValue}". DNS propagation can take up to 5 minutes.`
374
+ },
375
+ 422
376
+ );
377
+ }
378
+ await db.prepare(
379
+ "UPDATE domain_challenges SET status = 'verified', verified_at = ?, expires_at = ? WHERE domain = ?"
380
+ ).bind(now, now + ONE_YEAR_MS, domain).run();
381
+ const publishToken = await signPublishToken(gatewayPrivateKey, domain);
382
+ return c.json({ ok: true, domain, publishToken });
383
+ });
384
+ return app;
385
+ }
386
+
387
+ // src/http/routes/byok.ts
388
+ import { Hono as Hono4 } from "hono";
389
+ function byokRoutes(options) {
390
+ const { vault } = options;
391
+ const app = new Hono4();
392
+ app.put("/:domain", async (c) => {
393
+ const domain = c.req.param("domain");
394
+ const agent = c.get("agent");
395
+ const body = await c.req.json();
396
+ if (!body.auth?.type) {
397
+ return c.json({ error: "Missing auth.type" }, 400);
398
+ }
399
+ await vault.putByok(domain, agent.id, body.auth);
400
+ return c.json({ ok: true, domain });
401
+ });
402
+ app.get("/", async (c) => {
403
+ const agent = c.get("agent");
404
+ const allDomains = await vault.listDomains();
405
+ const byokDomains = [];
406
+ for (const domain of allDomains) {
407
+ const cred = await vault.get(domain, agent.id);
408
+ if (cred && cred.scope === "byok" && cred.developerId === agent.id) {
409
+ byokDomains.push(domain);
410
+ }
411
+ }
412
+ return c.json({ domains: byokDomains });
413
+ });
414
+ app.delete("/:domain", async (c) => {
415
+ const domain = c.req.param("domain");
416
+ const agent = c.get("agent");
417
+ await vault.delete(domain, agent.id);
418
+ return c.json({ ok: true, domain });
419
+ });
420
+ return app;
421
+ }
422
+
423
+ // src/http/routes/fs.ts
424
+ import { Hono as Hono5 } from "hono";
425
+ function errorToStatus(code) {
426
+ switch (code) {
427
+ case "PARSE_ERROR":
428
+ case "INVALID_PATH":
429
+ return 400;
430
+ case "PERMISSION_DENIED":
431
+ return 403;
432
+ case "NOT_FOUND":
433
+ case "NO_MOUNT":
434
+ return 404;
435
+ default:
436
+ return 500;
437
+ }
438
+ }
439
+ function fsRoutes(options) {
440
+ const { agentFs } = options;
441
+ const app = new Hono5();
442
+ app.post("/execute", async (c) => {
443
+ const body = await c.req.json();
444
+ if (!body.command || typeof body.command !== "string") {
445
+ return c.json({ error: "Missing 'command' field" }, 400);
446
+ }
447
+ const agent = c.get("agent");
448
+ const roles = body.roles ?? agent?.roles;
449
+ const result = await agentFs.execute(body.command, roles, agent);
450
+ const status = result.ok ? 200 : errorToStatus(result.error.code);
451
+ return c.json(result, status);
452
+ });
453
+ app.all("/fs/*", async (c) => {
454
+ const fullPath = c.req.path;
455
+ const virtualPath = fullPath.slice(fullPath.indexOf("/fs") + 3) || "/";
456
+ const query = c.req.query("q");
457
+ const agent = c.get("agent");
458
+ const roles = agent?.roles;
459
+ let op;
460
+ let data;
461
+ let pattern;
462
+ switch (c.req.method) {
463
+ case "GET":
464
+ if (query) {
465
+ op = "grep";
466
+ pattern = query;
467
+ } else if (virtualPath.endsWith("/")) {
468
+ op = "ls";
469
+ } else {
470
+ op = "cat";
471
+ }
472
+ break;
473
+ case "POST":
474
+ case "PUT":
475
+ op = "write";
476
+ data = await c.req.json();
477
+ break;
478
+ case "DELETE":
479
+ op = "rm";
480
+ break;
481
+ default:
482
+ return c.json({ error: "Method not allowed" }, 405);
483
+ }
484
+ const result = await agentFs.executeCommand(
485
+ { op, path: virtualPath, data, pattern },
486
+ roles,
487
+ agent
488
+ );
489
+ const status = result.ok ? 200 : errorToStatus(result.error.code);
490
+ return c.json(result, status);
491
+ });
492
+ return app;
493
+ }
494
+
495
+ // src/http/routes/peers.ts
496
+ import { Hono as Hono6 } from "hono";
497
+ function peerRoutes(options) {
498
+ const { peerStore } = options;
499
+ const app = new Hono6();
500
+ app.get("/peers", async (c) => {
501
+ const peers = await peerStore.listPeers();
502
+ const safe = peers.map(({ sharedSecret: _, ...rest }) => rest);
503
+ return c.json({ peers: safe });
504
+ });
505
+ app.put("/peers/:id", async (c) => {
506
+ const id = c.req.param("id");
507
+ const body = await c.req.json();
508
+ if (!body.name || !body.url || !body.sharedSecret) {
509
+ return c.json({ error: "Missing required fields: name, url, sharedSecret" }, 400);
510
+ }
511
+ const existing = await peerStore.getPeer(id);
512
+ const now = Date.now();
513
+ const peer = {
514
+ id,
515
+ name: body.name,
516
+ url: body.url,
517
+ sharedSecret: body.sharedSecret,
518
+ status: "active",
519
+ advertisedDomains: existing?.advertisedDomains ?? [],
520
+ lastSeen: existing?.lastSeen ?? 0,
521
+ createdAt: existing?.createdAt ?? now
522
+ };
523
+ await peerStore.putPeer(peer);
524
+ return c.json({ ok: true, id });
525
+ });
526
+ app.delete("/peers/:id", async (c) => {
527
+ const id = c.req.param("id");
528
+ await peerStore.deletePeer(id);
529
+ return c.json({ ok: true, id });
530
+ });
531
+ app.get("/rules", async (c) => {
532
+ const rules = await peerStore.listRules();
533
+ return c.json({ rules });
534
+ });
535
+ app.put("/rules/:domain", async (c) => {
536
+ const domain = c.req.param("domain");
537
+ const body = await c.req.json();
538
+ if (body.allow === void 0) {
539
+ return c.json({ error: "Missing required field: allow" }, 400);
540
+ }
541
+ const existing = await peerStore.getRule(domain);
542
+ const now = Date.now();
543
+ const rule = {
544
+ domain,
545
+ allow: body.allow,
546
+ peers: body.peers ?? existing?.peers ?? "*",
547
+ pricing: body.pricing ?? existing?.pricing ?? { mode: "free" },
548
+ rateLimit: body.rateLimit,
549
+ createdAt: existing?.createdAt ?? now,
550
+ updatedAt: now
551
+ };
552
+ await peerStore.putRule(rule);
553
+ return c.json({ ok: true, domain });
554
+ });
555
+ app.delete("/rules/:domain", async (c) => {
556
+ const domain = c.req.param("domain");
557
+ await peerStore.deleteRule(domain);
558
+ return c.json({ ok: true, domain });
559
+ });
560
+ return app;
561
+ }
562
+
563
+ // src/http/routes/federation.ts
564
+ import { Hono as Hono7 } from "hono";
565
+ async function authenticatePeer(peerStore, peerId, authHeader) {
566
+ if (!peerId || !authHeader) return null;
567
+ const peer = await peerStore.getPeer(peerId);
568
+ if (!peer || peer.status !== "active") return null;
569
+ const token = authHeader.replace(/^Bearer\s+/i, "");
570
+ if (token !== peer.sharedSecret) return null;
571
+ return peer;
572
+ }
573
+ function extractDomainFromCommand(command) {
574
+ const parts = command.trim().split(/\s+/);
575
+ if (parts.length < 2) return null;
576
+ const path = parts[1];
577
+ if (!path.startsWith("/")) return null;
578
+ const segments = path.slice(1).split("/");
579
+ return segments[0] || null;
580
+ }
581
+ function federationRoutes(options) {
582
+ const { peerStore, vault, agentFs } = options;
583
+ const app = new Hono7();
584
+ app.post("/query", async (c) => {
585
+ const peerId = c.req.header("X-Peer-Id");
586
+ const authHeader = c.req.header("Authorization");
587
+ const peer = await authenticatePeer(peerStore, peerId, authHeader);
588
+ if (!peer) {
589
+ return c.json({ error: "Unauthorized peer" }, 403);
590
+ }
591
+ const body = await c.req.json();
592
+ if (!body.domain) {
593
+ return c.json({ error: "Missing 'domain' field" }, 400);
594
+ }
595
+ await peerStore.updateLastSeen(peer.id, Date.now());
596
+ const credential = await vault.get(body.domain);
597
+ if (!credential) {
598
+ return c.json({ available: false });
599
+ }
600
+ const rule = await peerStore.getRule(body.domain);
601
+ if (!rule || !rule.allow) {
602
+ return c.json({ available: false });
603
+ }
604
+ if (rule.peers !== "*" && !rule.peers.includes(peer.id)) {
605
+ return c.json({ available: false });
606
+ }
607
+ return c.json({
608
+ available: true,
609
+ pricing: rule.pricing
610
+ });
611
+ });
612
+ app.post("/exec", async (c) => {
613
+ const peerId = c.req.header("X-Peer-Id");
614
+ const authHeader = c.req.header("Authorization");
615
+ const peer = await authenticatePeer(peerStore, peerId, authHeader);
616
+ if (!peer) {
617
+ return c.json({ error: "Unauthorized peer" }, 403);
618
+ }
619
+ const body = await c.req.json();
620
+ if (!body.command) {
621
+ return c.json({ error: "Missing 'command' field" }, 400);
622
+ }
623
+ await peerStore.updateLastSeen(peer.id, Date.now());
624
+ const domain = extractDomainFromCommand(body.command);
625
+ if (domain) {
626
+ const rule = await peerStore.getRule(domain);
627
+ if (!rule || !rule.allow) {
628
+ return c.json({ error: "Domain not available for lending" }, 403);
629
+ }
630
+ if (rule.peers !== "*" && !rule.peers.includes(peer.id)) {
631
+ return c.json({ error: "Peer not in allowed list" }, 403);
632
+ }
633
+ if (rule.pricing.mode !== "free") {
634
+ const paymentHeader = c.req.header("X-402-Payment");
635
+ if (!paymentHeader) {
636
+ return c.json({ error: "Payment required" }, 402);
637
+ }
638
+ }
639
+ }
640
+ const syntheticAgentId = `peer:${peer.id}:${body.agentId}`;
641
+ const result = await agentFs.execute(body.command, ["agent"], {
642
+ id: syntheticAgentId,
643
+ roles: ["agent"]
644
+ });
645
+ if (!result.ok) {
646
+ return c.json({ ok: false, error: result.error.message }, 500);
647
+ }
648
+ return c.json({ ok: true, data: result.data });
649
+ });
650
+ app.post("/announce", async (c) => {
651
+ const peerId = c.req.header("X-Peer-Id");
652
+ const authHeader = c.req.header("Authorization");
653
+ const peer = await authenticatePeer(peerStore, peerId, authHeader);
654
+ if (!peer) {
655
+ return c.json({ error: "Unauthorized peer" }, 403);
656
+ }
657
+ const body = await c.req.json();
658
+ if (!Array.isArray(body.domains)) {
659
+ return c.json({ error: "Missing 'domains' field" }, 400);
660
+ }
661
+ peer.advertisedDomains = body.domains;
662
+ await peerStore.putPeer(peer);
663
+ await peerStore.updateLastSeen(peer.id, Date.now());
664
+ return c.json({ ok: true });
665
+ });
666
+ return app;
667
+ }
668
+
669
+ // src/http/app.ts
670
+ function createGateway(options) {
671
+ const { store, gatewayPrivateKey, gatewayPublicKey, adminToken } = options;
672
+ const app = new Hono8();
673
+ const { onMiss, listDomains, searchDomains, searchEndpoints } = createRegistryResolver(
674
+ options.vault ? { store, vault: options.vault, gatewayPrivateKey } : { store, gatewayPrivateKey }
675
+ );
676
+ const mounts = [];
677
+ if (options.context7ApiKey) {
678
+ mounts.push({ path: "/context7", backend: new Context7Backend({ apiKey: options.context7ApiKey }) });
679
+ }
680
+ const agentFs = new AgentFs({
681
+ mounts,
682
+ onMiss,
683
+ listDomains,
684
+ searchDomains,
685
+ searchEndpoints
686
+ });
687
+ app.get("/.well-known/jwks.json", (c) => {
688
+ return c.json({ keys: [gatewayPublicKey] });
689
+ });
690
+ app.route("/auth", authRoutes({ privateKey: gatewayPrivateKey }));
691
+ if (options.db) {
692
+ app.route("/domains", domainRoutes({ db: options.db, gatewayPrivateKey }));
693
+ }
694
+ app.use("/registry/*", publishOrAdminAuth(adminToken, gatewayPublicKey));
695
+ app.route("/registry", registryRoutes({ store }));
696
+ if (options.vault) {
697
+ app.use("/credentials/*", adminAuth(adminToken));
698
+ app.route("/credentials", credentialRoutes({ vault: options.vault }));
699
+ app.use("/byok/*", agentAuth(gatewayPublicKey));
700
+ app.route("/byok", byokRoutes({ vault: options.vault }));
701
+ }
702
+ app.use("/execute", agentAuth(gatewayPublicKey));
703
+ app.use("/fs/*", agentAuth(gatewayPublicKey));
704
+ app.route("/", fsRoutes({ agentFs }));
705
+ if (options.proxy && options.vault) {
706
+ app.use("/proxy/*", agentAuth(gatewayPublicKey));
707
+ app.route(
708
+ "/proxy",
709
+ proxyRoutes({
710
+ vault: options.vault,
711
+ toolRegistry: options.proxy.toolRegistry,
712
+ exec: options.proxy.exec
713
+ })
714
+ );
715
+ }
716
+ if (options.peerStore && options.vault) {
717
+ app.use("/admin/federation/*", adminAuth(adminToken));
718
+ app.route("/admin/federation", peerRoutes({ peerStore: options.peerStore }));
719
+ app.route("/federation", federationRoutes({
720
+ peerStore: options.peerStore,
721
+ vault: options.vault,
722
+ agentFs
723
+ }));
724
+ }
725
+ if (options.tunnel) {
726
+ app.use("/tunnels/*", agentAuth(gatewayPublicKey));
727
+ app.route(
728
+ "/tunnels",
729
+ tunnelRoutes({
730
+ tunnelStore: options.tunnel.store,
731
+ tunnelProvider: options.tunnel.provider,
732
+ tunnelDomain: options.tunnel.domain
733
+ })
734
+ );
735
+ }
736
+ return app;
737
+ }
738
+ export {
739
+ adminAuth,
740
+ agentAuth,
741
+ authRoutes,
742
+ createGateway,
743
+ domainRoutes,
744
+ fsRoutes,
745
+ publishOrAdminAuth,
746
+ registryRoutes,
747
+ tunnelRoutes
748
+ };