@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.cjs ADDED
@@ -0,0 +1,1772 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/http.ts
31
+ var http_exports = {};
32
+ __export(http_exports, {
33
+ adminAuth: () => adminAuth,
34
+ agentAuth: () => agentAuth,
35
+ authRoutes: () => authRoutes,
36
+ createGateway: () => createGateway,
37
+ domainRoutes: () => domainRoutes,
38
+ fsRoutes: () => fsRoutes,
39
+ publishOrAdminAuth: () => publishOrAdminAuth,
40
+ registryRoutes: () => registryRoutes,
41
+ tunnelRoutes: () => tunnelRoutes
42
+ });
43
+ module.exports = __toCommonJS(http_exports);
44
+
45
+ // src/http/app.ts
46
+ var import_hono11 = require("hono");
47
+ var import_agent_fs2 = require("@nkmc/agent-fs");
48
+
49
+ // src/registry/resolver.ts
50
+ var import_agent_fs = require("@nkmc/agent-fs");
51
+ var import_core = require("@nkmc/core");
52
+
53
+ // src/registry/skill-to-config.ts
54
+ function skillToHttpConfig(record) {
55
+ let baseUrl = `https://${record.domain}`;
56
+ if (record.source?.basePath) {
57
+ baseUrl += record.source.basePath;
58
+ }
59
+ const resources = extractResources(record.skillMd);
60
+ const endpoints = extractHttpEndpoints(record.skillMd);
61
+ return {
62
+ baseUrl,
63
+ resources,
64
+ endpoints
65
+ };
66
+ }
67
+ function extractResources(skillMd) {
68
+ const resources = [];
69
+ const lines = skillMd.split("\n");
70
+ let inSchema = false;
71
+ let current = null;
72
+ for (const line of lines) {
73
+ if (line.startsWith("## Schema")) {
74
+ inSchema = true;
75
+ continue;
76
+ }
77
+ if (inSchema && line.startsWith("## ") && !line.startsWith("## Schema")) {
78
+ break;
79
+ }
80
+ if (!inSchema) continue;
81
+ const tableMatch = line.match(/^### (\w+)\s/);
82
+ if (tableMatch) {
83
+ if (current) resources.push(toHttpResource(current));
84
+ current = { name: tableMatch[1], fields: [] };
85
+ continue;
86
+ }
87
+ if (current && line.startsWith("|") && !line.startsWith("|--") && !line.startsWith("| field")) {
88
+ const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
89
+ if (cells.length >= 3) {
90
+ current.fields.push({
91
+ name: cells[0],
92
+ type: cells[1],
93
+ description: cells[2]
94
+ });
95
+ }
96
+ }
97
+ }
98
+ if (current) resources.push(toHttpResource(current));
99
+ return resources;
100
+ }
101
+ function toHttpResource(parsed) {
102
+ return {
103
+ name: parsed.name,
104
+ apiPath: `/${parsed.name}`,
105
+ fields: parsed.fields
106
+ };
107
+ }
108
+ function extractHttpEndpoints(skillMd) {
109
+ const endpoints = [];
110
+ const lines = skillMd.split("\n");
111
+ let inApi = false;
112
+ let currentHeading = null;
113
+ for (const line of lines) {
114
+ if (line.startsWith("## API")) {
115
+ inApi = true;
116
+ continue;
117
+ }
118
+ if (inApi && line.startsWith("## ") && !line.startsWith("## API")) {
119
+ break;
120
+ }
121
+ if (!inApi) continue;
122
+ if (line.startsWith("### ")) {
123
+ currentHeading = line.slice(4).trim();
124
+ continue;
125
+ }
126
+ const match = line.match(/^`(GET|POST|PUT|PATCH|DELETE)\s+(\S+)`/);
127
+ if (match && currentHeading) {
128
+ const slug = currentHeading.toLowerCase().replace(/\s+/g, "-");
129
+ endpoints.push({
130
+ name: slug,
131
+ method: match[1],
132
+ apiPath: match[2],
133
+ description: currentHeading
134
+ });
135
+ currentHeading = null;
136
+ }
137
+ }
138
+ return endpoints;
139
+ }
140
+ function skillToRpcConfig(meta) {
141
+ const resources = meta.resources.map((r) => {
142
+ const methods = {};
143
+ const builder = getParamsBuilder(meta.convention);
144
+ for (const [fsOp, rpcMethod] of Object.entries(r.methods)) {
145
+ const key = fsOp;
146
+ methods[key] = {
147
+ method: rpcMethod,
148
+ params: builder(key)
149
+ };
150
+ }
151
+ const resource = {
152
+ name: r.name,
153
+ ...r.idField ? { idField: r.idField } : {},
154
+ methods
155
+ };
156
+ if (meta.convention === "evm") {
157
+ resource.transform = buildEvmTransforms(r.name);
158
+ }
159
+ return resource;
160
+ });
161
+ return { resources };
162
+ }
163
+ function getParamsBuilder(convention) {
164
+ switch (convention) {
165
+ case "crud":
166
+ return crudParamsBuilder;
167
+ case "evm":
168
+ return evmParamsBuilder;
169
+ case "raw":
170
+ default:
171
+ return rawParamsBuilder;
172
+ }
173
+ }
174
+ function rawParamsBuilder(_fsOp) {
175
+ return (ctx) => {
176
+ if (ctx.data !== void 0) return [ctx.data];
177
+ if (ctx.id !== void 0) return [ctx.id];
178
+ return [];
179
+ };
180
+ }
181
+ function crudParamsBuilder(fsOp) {
182
+ switch (fsOp) {
183
+ case "list":
184
+ return () => [];
185
+ case "read":
186
+ return (ctx) => [ctx.id];
187
+ case "write":
188
+ return (ctx) => [ctx.id, ctx.data];
189
+ case "create":
190
+ return (ctx) => [ctx.data];
191
+ case "remove":
192
+ return (ctx) => [ctx.id];
193
+ case "search":
194
+ return (ctx) => [ctx.pattern];
195
+ default:
196
+ return () => [];
197
+ }
198
+ }
199
+ function evmParamsBuilder(fsOp) {
200
+ switch (fsOp) {
201
+ case "list":
202
+ return () => [];
203
+ case "read":
204
+ return (ctx) => {
205
+ const id = ctx.id;
206
+ const hexId = /^\d+$/.test(id) ? "0x" + Number(id).toString(16) : id;
207
+ return [hexId, "latest"];
208
+ };
209
+ default:
210
+ return rawParamsBuilder(fsOp);
211
+ }
212
+ }
213
+ function buildEvmTransforms(resourceName) {
214
+ const transform = {};
215
+ if (resourceName === "blocks") {
216
+ transform.list = (data) => {
217
+ const hex = String(data);
218
+ const latest = parseInt(hex, 16);
219
+ if (isNaN(latest)) return [];
220
+ return Array.from({ length: 10 }, (_, i) => `${latest - i}.json`);
221
+ };
222
+ }
223
+ if (resourceName === "balances") {
224
+ transform.read = (data) => {
225
+ const hex = String(data);
226
+ return { wei: hex, raw: hex };
227
+ };
228
+ }
229
+ return Object.keys(transform).length > 0 ? transform : void 0;
230
+ }
231
+
232
+ // src/registry/virtual-files.ts
233
+ var VIRTUAL_FILES = ["_pricing.json", "_versions.json", "skill.md"];
234
+ var VirtualFileBackend = class {
235
+ inner;
236
+ domain;
237
+ store;
238
+ constructor(options) {
239
+ this.inner = options.inner;
240
+ this.domain = options.domain;
241
+ this.store = options.store;
242
+ }
243
+ async list(path) {
244
+ const entries = await this.inner.list(path);
245
+ if (path === "/" || path === "" || path === ".") {
246
+ return [...entries, ...VIRTUAL_FILES];
247
+ }
248
+ return entries;
249
+ }
250
+ async read(path) {
251
+ const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
252
+ if (cleaned === "_pricing.json") {
253
+ const record = await this.store.get(this.domain);
254
+ if (!record) return { endpoints: [] };
255
+ return {
256
+ domain: this.domain,
257
+ endpoints: record.endpoints.filter((ep) => ep.pricing).map((ep) => ({
258
+ method: ep.method,
259
+ path: ep.path,
260
+ description: ep.description,
261
+ pricing: ep.pricing
262
+ }))
263
+ };
264
+ }
265
+ if (cleaned === "_versions.json") {
266
+ const versions = await this.store.listVersions(this.domain);
267
+ return { domain: this.domain, versions };
268
+ }
269
+ if (cleaned === "skill.md") {
270
+ const record = await this.store.get(this.domain);
271
+ if (!record) return "# Not found\n";
272
+ return record.skillMd;
273
+ }
274
+ return this.inner.read(path);
275
+ }
276
+ async write(path, data) {
277
+ return this.inner.write(path, data);
278
+ }
279
+ async remove(path) {
280
+ return this.inner.remove(path);
281
+ }
282
+ async search(path, pattern) {
283
+ return this.inner.search(path, pattern);
284
+ }
285
+ };
286
+
287
+ // src/federation/peer-backend.ts
288
+ var PeerBackend = class {
289
+ constructor(client, peer, agentId) {
290
+ this.client = client;
291
+ this.peer = peer;
292
+ this.agentId = agentId;
293
+ }
294
+ async list(path) {
295
+ const result = await this.execOnPeer(`ls ${path}`);
296
+ return result.data ?? [];
297
+ }
298
+ async read(path) {
299
+ const result = await this.execOnPeer(`cat ${path}`);
300
+ return result.data;
301
+ }
302
+ async write(path, data) {
303
+ const result = await this.execOnPeer(
304
+ `write ${path} ${JSON.stringify(data)}`
305
+ );
306
+ return result.data ?? { id: "" };
307
+ }
308
+ async remove(path) {
309
+ await this.execOnPeer(`rm ${path}`);
310
+ }
311
+ async search(path, pattern) {
312
+ const result = await this.execOnPeer(`grep ${pattern} ${path}`);
313
+ return result.data ?? [];
314
+ }
315
+ async execOnPeer(command) {
316
+ const result = await this.client.exec(this.peer, {
317
+ command,
318
+ agentId: this.agentId
319
+ });
320
+ if (!result.ok) {
321
+ if (result.paymentRequired) {
322
+ throw new Error(
323
+ `Payment required: ${result.paymentRequired.price} ${result.paymentRequired.currency}`
324
+ );
325
+ }
326
+ throw new Error(result.error ?? "Peer execution failed");
327
+ }
328
+ return result;
329
+ }
330
+ };
331
+
332
+ // src/registry/resolver.ts
333
+ function createRegistryResolver(storeOrOptions) {
334
+ const options = "get" in storeOrOptions && "put" in storeOrOptions ? { store: storeOrOptions } : storeOrOptions;
335
+ const { store, vault, gatewayPrivateKey } = options;
336
+ const loaded = /* @__PURE__ */ new Set();
337
+ async function tryPeerFallback(domain, version, addMount, agent) {
338
+ if (!options.peerClient || !options.peerStore) return false;
339
+ const peers = await options.peerStore.listPeers();
340
+ for (const peer of peers) {
341
+ if (peer.advertisedDomains.length > 0 && !peer.advertisedDomains.includes(domain)) {
342
+ continue;
343
+ }
344
+ const result = await options.peerClient.query(peer, domain);
345
+ if (result.available) {
346
+ const peerBackend = new PeerBackend(options.peerClient, peer, agent?.id ?? "anonymous");
347
+ const mountPath = version ? `/${domain}@${version}` : `/${domain}`;
348
+ addMount({ path: mountPath, backend: peerBackend });
349
+ return true;
350
+ }
351
+ }
352
+ return false;
353
+ }
354
+ async function onMiss(path, addMount, agent) {
355
+ const { domain, version } = extractDomainPath(path);
356
+ if (!domain) return false;
357
+ const cacheKey = version ? `${domain}@${version}` : domain;
358
+ const record = version ? await store.getVersion(domain, version) : await store.get(domain);
359
+ if (!record) {
360
+ return tryPeerFallback(domain, version, addMount, agent);
361
+ }
362
+ const isNkmcJwt = record.authMode === "nkmc-jwt";
363
+ if (!isNkmcJwt && loaded.has(cacheKey)) return false;
364
+ if (record.status === "sunset") return false;
365
+ let auth;
366
+ if (isNkmcJwt && gatewayPrivateKey && agent) {
367
+ const token = await (0, import_core.signJwt)(gatewayPrivateKey, {
368
+ sub: agent.id,
369
+ roles: agent.roles,
370
+ svc: domain
371
+ }, { expiresIn: "5m" });
372
+ auth = { type: "bearer", token };
373
+ } else if (vault) {
374
+ const cred = await vault.get(domain, agent?.id);
375
+ if (cred) {
376
+ auth = cred.auth;
377
+ }
378
+ }
379
+ if (!auth && !isNkmcJwt) {
380
+ const peerMounted = await tryPeerFallback(domain, version, addMount, agent);
381
+ if (peerMounted) return true;
382
+ }
383
+ let backend;
384
+ if (record.source?.type === "jsonrpc" && record.source.rpc) {
385
+ const { resources } = skillToRpcConfig(record.source.rpc);
386
+ const headers = {};
387
+ if (auth) {
388
+ if (auth.type === "bearer") {
389
+ headers["Authorization"] = `${auth.prefix ?? "Bearer"} ${auth.token}`;
390
+ } else if (auth.type === "api-key") {
391
+ headers[auth.header] = auth.key;
392
+ }
393
+ }
394
+ const transport = new import_agent_fs.JsonRpcTransport({ url: record.source.rpc.rpcUrl, headers });
395
+ backend = new import_agent_fs.RpcBackend({ transport, resources });
396
+ } else {
397
+ const config = skillToHttpConfig(record);
398
+ config.auth = auth;
399
+ backend = new import_agent_fs.HttpBackend(config);
400
+ }
401
+ let finalBackend = backend;
402
+ if (options.wrapVirtualFiles !== false) {
403
+ finalBackend = new VirtualFileBackend({ inner: backend, domain, store: options.store });
404
+ }
405
+ const mountPath = version ? `/${domain}@${version}` : `/${domain}`;
406
+ addMount({ path: mountPath, backend: finalBackend });
407
+ if (!isNkmcJwt) {
408
+ loaded.add(cacheKey);
409
+ }
410
+ return true;
411
+ }
412
+ async function listDomains() {
413
+ const summaries = await store.list();
414
+ return summaries.map((s) => s.domain);
415
+ }
416
+ async function searchDomains(query) {
417
+ return store.search(query);
418
+ }
419
+ async function searchEndpoints(domain, query) {
420
+ const record = await store.get(domain);
421
+ if (!record) return [];
422
+ const q = query.toLowerCase();
423
+ return record.endpoints.filter(
424
+ (e) => e.description.toLowerCase().includes(q) || e.method.toLowerCase().includes(q) || e.path.toLowerCase().includes(q)
425
+ ).map((e) => ({ method: e.method, path: e.path, description: e.description }));
426
+ }
427
+ return { onMiss, listDomains, searchDomains, searchEndpoints };
428
+ }
429
+ function extractDomainPath(path) {
430
+ const segments = path.split("/").filter(Boolean);
431
+ if (segments.length === 0) return { domain: null, version: null };
432
+ const first = segments[0];
433
+ const atIndex = first.indexOf("@");
434
+ if (atIndex > 0) {
435
+ return {
436
+ domain: first.slice(0, atIndex),
437
+ version: first.slice(atIndex + 1)
438
+ };
439
+ }
440
+ return { domain: first, version: null };
441
+ }
442
+
443
+ // src/registry/context7.ts
444
+ var Context7Client = class {
445
+ apiKey;
446
+ baseUrl;
447
+ fetchFn;
448
+ constructor(options) {
449
+ this.apiKey = options?.apiKey;
450
+ this.baseUrl = options?.baseUrl ?? "https://context7.com/api/v2";
451
+ this.fetchFn = options?.fetchFn ?? globalThis.fetch.bind(globalThis);
452
+ }
453
+ /** Search for a library by name. Returns matching library entries. */
454
+ async searchLibraries(libraryName, query) {
455
+ const params = new URLSearchParams({ libraryName });
456
+ if (query) params.set("query", query);
457
+ const resp = await this.fetchFn(`${this.baseUrl}/libs/search?${params}`, {
458
+ headers: this.headers()
459
+ });
460
+ if (!resp.ok) throw new Error(`Context7 search failed: ${resp.status}`);
461
+ return resp.json();
462
+ }
463
+ /** Query documentation for a specific library. Returns documentation text. */
464
+ async queryDocs(libraryId, query) {
465
+ const params = new URLSearchParams({ libraryId, query, type: "txt" });
466
+ const resp = await this.fetchFn(`${this.baseUrl}/context?${params}`, {
467
+ headers: this.headers()
468
+ });
469
+ if (!resp.ok) throw new Error(`Context7 query failed: ${resp.status}`);
470
+ return resp.text();
471
+ }
472
+ headers() {
473
+ const h = {};
474
+ if (this.apiKey) h["Authorization"] = `Bearer ${this.apiKey}`;
475
+ return h;
476
+ }
477
+ };
478
+
479
+ // src/registry/context7-backend.ts
480
+ var Context7Backend = class {
481
+ client;
482
+ constructor(options) {
483
+ this.client = new Context7Client(options);
484
+ }
485
+ async list(path) {
486
+ const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
487
+ if (!cleaned) {
488
+ return [
489
+ 'grep "<\u5173\u952E\u8BCD>" /context7/ \u2014 \u641C\u7D22\u5E93',
490
+ 'grep "<\u95EE\u9898>" /context7/{id} \u2014 \u67E5\u8BE2\u6587\u6863',
491
+ "cat /context7/{owner}/{repo} \u2014 \u5E93\u6982\u89C8"
492
+ ];
493
+ }
494
+ return ['grep "<\u95EE\u9898>" /context7/' + cleaned + " \u2014 \u67E5\u8BE2\u6B64\u5E93\u6587\u6863"];
495
+ }
496
+ async read(path) {
497
+ const libraryId = parseLibraryId(path);
498
+ if (!libraryId) {
499
+ return { usage: 'grep "<\u5173\u952E\u8BCD>" /context7/ \u2014 \u641C\u7D22\u5E93' };
500
+ }
501
+ const name = libraryId.split("/").pop() ?? libraryId;
502
+ const docs = await this.client.queryDocs(libraryId, `${name} overview getting started`);
503
+ return { libraryId, docs };
504
+ }
505
+ async write(_path, _data) {
506
+ throw new Error("context7 is read-only");
507
+ }
508
+ async remove(_path) {
509
+ throw new Error("context7 is read-only");
510
+ }
511
+ async search(path, pattern) {
512
+ const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
513
+ if (!cleaned) {
514
+ const results = await this.client.searchLibraries(pattern);
515
+ return results.map(formatSearchResult);
516
+ }
517
+ const libraryId = parseLibraryId(path);
518
+ if (!libraryId) return [];
519
+ const docs = await this.client.queryDocs(libraryId, pattern);
520
+ if (!docs) return [];
521
+ return [{ libraryId, query: pattern, docs }];
522
+ }
523
+ };
524
+ function parseLibraryId(path) {
525
+ const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
526
+ if (!cleaned) return null;
527
+ const parts = cleaned.split("/");
528
+ if (parts.length < 2) return null;
529
+ return "/" + parts.slice(0, 2).join("/");
530
+ }
531
+ function formatSearchResult(r) {
532
+ return {
533
+ id: r.id,
534
+ name: r.name,
535
+ description: r.description ?? "",
536
+ snippets: r.totalSnippets ?? 0
537
+ };
538
+ }
539
+
540
+ // src/http/middleware/admin-auth.ts
541
+ var import_factory = require("hono/factory");
542
+ function adminAuth(adminToken) {
543
+ return (0, import_factory.createMiddleware)(async (c, next) => {
544
+ const auth = c.req.header("Authorization");
545
+ if (!auth || !auth.startsWith("Bearer ")) {
546
+ return c.json({ error: "Missing Authorization header" }, 401);
547
+ }
548
+ const token = auth.slice(7);
549
+ if (token !== adminToken) {
550
+ return c.json({ error: "Invalid admin token" }, 403);
551
+ }
552
+ await next();
553
+ });
554
+ }
555
+
556
+ // src/http/middleware/publish-auth.ts
557
+ var import_factory2 = require("hono/factory");
558
+ var import_core2 = require("@nkmc/core");
559
+ function publishOrAdminAuth(adminToken, publicKey) {
560
+ return (0, import_factory2.createMiddleware)(async (c, next) => {
561
+ const auth = c.req.header("Authorization");
562
+ if (!auth || !auth.startsWith("Bearer ")) {
563
+ return c.json({ error: "Missing Authorization header" }, 401);
564
+ }
565
+ const token = auth.slice(7);
566
+ if (token === adminToken) {
567
+ c.set("publishAuth", { type: "admin" });
568
+ return next();
569
+ }
570
+ try {
571
+ const payload = await (0, import_core2.verifyPublishToken)(token, publicKey);
572
+ c.set("publishAuth", { type: "publish", domain: payload.sub });
573
+ return next();
574
+ } catch {
575
+ return c.json({ error: "Invalid token" }, 403);
576
+ }
577
+ });
578
+ }
579
+
580
+ // src/http/middleware/agent-auth.ts
581
+ var import_factory3 = require("hono/factory");
582
+ var import_core3 = require("@nkmc/core");
583
+ function agentAuth(publicKey) {
584
+ return (0, import_factory3.createMiddleware)(async (c, next) => {
585
+ const auth = c.req.header("Authorization");
586
+ if (!auth || !auth.startsWith("Bearer ")) {
587
+ return c.json({ error: "Missing Authorization header" }, 401);
588
+ }
589
+ const token = auth.slice(7);
590
+ try {
591
+ const payload = await (0, import_core3.verifyJwt)(token, publicKey);
592
+ c.set("agent", { id: payload.sub, roles: payload.roles });
593
+ } catch (err) {
594
+ const message = err instanceof Error ? err.message : "Invalid token";
595
+ if (message.includes("exp") || message.includes("expired")) {
596
+ return c.json({ error: "Token has expired" }, 401);
597
+ }
598
+ return c.json({ error: "Invalid token" }, 401);
599
+ }
600
+ await next();
601
+ });
602
+ }
603
+
604
+ // src/http/routes/auth.ts
605
+ var import_hono = require("hono");
606
+ var import_core4 = require("@nkmc/core");
607
+ function authRoutes(options) {
608
+ const app = new import_hono.Hono();
609
+ app.post("/token", async (c) => {
610
+ const body = await c.req.json();
611
+ if (!body.sub || !body.svc) {
612
+ return c.json({ error: "Missing required fields: sub, svc" }, 400);
613
+ }
614
+ const token = await (0, import_core4.signJwt)(
615
+ options.privateKey,
616
+ {
617
+ sub: body.sub,
618
+ roles: body.roles ?? ["agent"],
619
+ svc: body.svc
620
+ },
621
+ body.expiresIn ? { expiresIn: body.expiresIn } : void 0
622
+ );
623
+ return c.json({ token });
624
+ });
625
+ return app;
626
+ }
627
+
628
+ // src/http/routes/registry.ts
629
+ var import_hono2 = require("hono");
630
+
631
+ // src/registry/skill-parser.ts
632
+ var import_yaml = require("yaml");
633
+ function parseSkillMd(domain, raw, options) {
634
+ const { frontmatter, body } = extractFrontmatter(raw);
635
+ const parsed = (0, import_yaml.parse)(frontmatter) ?? {};
636
+ const description = extractDescription(body);
637
+ const endpoints = extractEndpoints(body);
638
+ const now = Date.now();
639
+ return {
640
+ domain,
641
+ name: parsed.name ?? domain,
642
+ description,
643
+ version: parsed.version ?? "0.0",
644
+ roles: parsed.roles ?? ["agent"],
645
+ skillMd: raw,
646
+ endpoints,
647
+ isFirstParty: options?.isFirstParty ?? false,
648
+ createdAt: now,
649
+ updatedAt: now,
650
+ status: "active",
651
+ isDefault: true
652
+ };
653
+ }
654
+ function parsePricingAnnotation(text) {
655
+ const match = text.match(
656
+ /(\d+(?:\.\d+)?)\s+(\w+)\s*\/\s*(call|byte|minute|次)/i
657
+ );
658
+ if (!match) return void 0;
659
+ return {
660
+ cost: parseFloat(match[1]),
661
+ currency: match[2].toUpperCase(),
662
+ per: match[3] === "\u6B21" ? "call" : match[3].toLowerCase()
663
+ };
664
+ }
665
+ function extractFrontmatter(raw) {
666
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
667
+ if (!match) return { frontmatter: "", body: raw };
668
+ return { frontmatter: match[1], body: match[2] };
669
+ }
670
+ function extractDescription(body) {
671
+ const lines = body.split("\n");
672
+ let foundTitle = false;
673
+ const descLines = [];
674
+ for (const line of lines) {
675
+ if (!foundTitle) {
676
+ if (line.startsWith("# ")) foundTitle = true;
677
+ continue;
678
+ }
679
+ if (line.startsWith("## ")) break;
680
+ const trimmed = line.trim();
681
+ if (trimmed === "" && descLines.length > 0) break;
682
+ if (trimmed !== "") descLines.push(trimmed);
683
+ }
684
+ return descLines.join(" ");
685
+ }
686
+ function extractEndpoints(body) {
687
+ const endpoints = [];
688
+ const lines = body.split("\n");
689
+ let inApiSection = false;
690
+ let currentHeading = null;
691
+ for (const line of lines) {
692
+ if (line.startsWith("## API")) {
693
+ inApiSection = true;
694
+ continue;
695
+ }
696
+ if (inApiSection && line.startsWith("## ") && !line.startsWith("## API")) {
697
+ break;
698
+ }
699
+ if (!inApiSection) continue;
700
+ if (line.startsWith("### ")) {
701
+ currentHeading = line.slice(4).trim();
702
+ continue;
703
+ }
704
+ const endpointMatch = line.match(/^`(GET|POST|PUT|PATCH|DELETE)\s+(\S+)`/);
705
+ if (endpointMatch && currentHeading) {
706
+ const afterBacktick = line.slice(line.indexOf("`", 1) + 1).trim();
707
+ const pricing = afterBacktick.startsWith("\u2014") ? parsePricingAnnotation(afterBacktick.slice(1).trim()) : void 0;
708
+ endpoints.push({
709
+ method: endpointMatch[1],
710
+ path: endpointMatch[2],
711
+ description: currentHeading,
712
+ ...pricing ? { pricing } : {}
713
+ });
714
+ currentHeading = null;
715
+ }
716
+ }
717
+ return endpoints;
718
+ }
719
+
720
+ // src/registry/openapi-compiler.ts
721
+ var import_yaml2 = __toESM(require("yaml"), 1);
722
+ function extractBasePath(spec) {
723
+ const servers = spec.servers;
724
+ if (!Array.isArray(servers) || servers.length === 0) return "";
725
+ const serverUrl = servers[0]?.url;
726
+ if (!serverUrl || typeof serverUrl !== "string") return "";
727
+ try {
728
+ if (serverUrl.startsWith("/")) {
729
+ return serverUrl.replace(/\/+$/, "");
730
+ }
731
+ const parsed = new URL(serverUrl);
732
+ const pathname = parsed.pathname.replace(/\/+$/, "");
733
+ return pathname || "";
734
+ } catch {
735
+ return "";
736
+ }
737
+ }
738
+ function resolveRef(spec, ref) {
739
+ if (!ref.startsWith("#/")) return void 0;
740
+ const parts = ref.slice(2).split("/");
741
+ let current = spec;
742
+ for (const part of parts) {
743
+ if (current == null || typeof current !== "object") return void 0;
744
+ current = current[part];
745
+ }
746
+ return current;
747
+ }
748
+ function resolveSchema(spec, schema) {
749
+ if (!schema) return void 0;
750
+ if (schema.$ref) return resolveRef(spec, schema.$ref);
751
+ return schema;
752
+ }
753
+ function extractProperties(spec, schema) {
754
+ const resolved = resolveSchema(spec, schema);
755
+ if (!resolved || resolved.type !== "object" || !resolved.properties) return [];
756
+ const requiredSet = new Set(resolved.required ?? []);
757
+ const props = [];
758
+ for (const [name, prop] of Object.entries(resolved.properties)) {
759
+ const p = resolveSchema(spec, prop) ?? prop;
760
+ props.push({
761
+ name,
762
+ type: p.type ?? (p.enum ? "enum" : "unknown"),
763
+ required: requiredSet.has(name),
764
+ ...p.description ? { description: p.description } : {}
765
+ });
766
+ }
767
+ return props;
768
+ }
769
+ function extractParams(spec, operation) {
770
+ const params = operation.parameters;
771
+ if (!Array.isArray(params) || params.length === 0) return void 0;
772
+ const result = [];
773
+ for (const raw of params) {
774
+ const p = resolveSchema(spec, raw) ?? raw;
775
+ if (!p.name || !p.in) continue;
776
+ if (!["path", "query", "header"].includes(p.in)) continue;
777
+ const schema = resolveSchema(spec, p.schema) ?? p.schema;
778
+ result.push({
779
+ name: p.name,
780
+ in: p.in,
781
+ required: !!p.required,
782
+ type: schema?.type ?? "string",
783
+ ...p.description ? { description: p.description } : {}
784
+ });
785
+ }
786
+ return result.length > 0 ? result : void 0;
787
+ }
788
+ function extractRequestBody(spec, operation) {
789
+ const body = resolveSchema(spec, operation.requestBody);
790
+ if (!body?.content) return void 0;
791
+ const jsonContent = body.content["application/json"];
792
+ if (!jsonContent?.schema) return void 0;
793
+ const properties = extractProperties(spec, jsonContent.schema);
794
+ if (properties.length === 0) return void 0;
795
+ return {
796
+ contentType: "application/json",
797
+ required: !!body.required,
798
+ properties
799
+ };
800
+ }
801
+ function extractResponses(spec, operation) {
802
+ const responses = operation.responses;
803
+ if (!responses || typeof responses !== "object") return void 0;
804
+ const result = [];
805
+ for (const [code, raw] of Object.entries(responses)) {
806
+ const status = parseInt(code, 10);
807
+ if (isNaN(status) || status < 200 || status >= 300) continue;
808
+ const resp = resolveSchema(spec, raw) ?? raw;
809
+ const jsonContent = resp?.content?.["application/json"];
810
+ const properties = jsonContent?.schema ? extractProperties(spec, jsonContent.schema) : void 0;
811
+ result.push({
812
+ status,
813
+ description: resp?.description ?? "",
814
+ ...properties && properties.length > 0 ? { properties } : {}
815
+ });
816
+ }
817
+ return result.length > 0 ? result : void 0;
818
+ }
819
+ function compileOpenApiSpec(spec, options) {
820
+ const info = spec.info ?? {};
821
+ const name = info.title ?? options.domain;
822
+ const description = info.description ?? "";
823
+ const version = options.version ?? info.version ?? "1.0";
824
+ const basePath = extractBasePath(spec);
825
+ const endpoints = [];
826
+ const resources = [];
827
+ const resourcePaths = /* @__PURE__ */ new Map();
828
+ const paths = spec.paths ?? {};
829
+ for (const [path, methods] of Object.entries(paths)) {
830
+ if (typeof methods !== "object" || methods === null) continue;
831
+ for (const [method, op] of Object.entries(methods)) {
832
+ if (!["get", "post", "put", "patch", "delete"].includes(method)) continue;
833
+ const operation = op;
834
+ const parameters = extractParams(spec, operation);
835
+ const requestBody = extractRequestBody(spec, operation);
836
+ const responses = extractResponses(spec, operation);
837
+ endpoints.push({
838
+ method: method.toUpperCase(),
839
+ path,
840
+ description: operation.summary ?? operation.operationId ?? `${method.toUpperCase()} ${path}`,
841
+ ...parameters ? { parameters } : {},
842
+ ...requestBody ? { requestBody } : {},
843
+ ...responses ? { responses } : {}
844
+ });
845
+ }
846
+ const segments = path.split("/").filter(Boolean);
847
+ if (segments.length >= 1) {
848
+ const resourceName = segments[0];
849
+ if (!resourcePaths.has(resourceName) && !resourceName.startsWith("{")) {
850
+ resourcePaths.set(resourceName, {
851
+ name: resourceName,
852
+ apiPath: `/${resourceName}`
853
+ });
854
+ }
855
+ }
856
+ }
857
+ for (const r of resourcePaths.values()) {
858
+ resources.push(r);
859
+ }
860
+ const skillMd = generateSkillMd(name, version, description, endpoints, resources);
861
+ const now = Date.now();
862
+ const record = {
863
+ domain: options.domain,
864
+ name,
865
+ description,
866
+ version,
867
+ roles: ["agent"],
868
+ skillMd,
869
+ endpoints,
870
+ isFirstParty: options.isFirstParty ?? false,
871
+ createdAt: now,
872
+ updatedAt: now,
873
+ status: "active",
874
+ isDefault: true,
875
+ source: { type: "openapi", ...basePath ? { basePath } : {} }
876
+ };
877
+ return { record, resources, skillMd };
878
+ }
879
+ async function fetchAndCompile(specUrl, options, fetchFn = globalThis.fetch.bind(globalThis)) {
880
+ const resp = await fetchFn(specUrl);
881
+ if (!resp.ok) throw new Error(`Failed to fetch spec: ${resp.status} ${resp.statusText}`);
882
+ const text = await resp.text();
883
+ const spec = parseSpec(specUrl, resp.headers.get("content-type") ?? "", text);
884
+ const result = compileOpenApiSpec(spec, options);
885
+ const basePath = result.record.source?.basePath;
886
+ result.record.source = { type: "openapi", url: specUrl, ...basePath ? { basePath } : {} };
887
+ return result;
888
+ }
889
+ function parseSpec(url, contentType, text) {
890
+ const isJson = contentType.includes("json") || url.endsWith(".json") || text.trimStart().startsWith("{");
891
+ if (isJson) return JSON.parse(text);
892
+ return import_yaml2.default.parse(text);
893
+ }
894
+ function propsTable(props) {
895
+ let t = "| name | type | required |\n|------|------|----------|\n";
896
+ for (const p of props) {
897
+ t += `| ${p.name} | ${p.type} | ${p.required ? "*" : ""} |
898
+ `;
899
+ }
900
+ return t;
901
+ }
902
+ function generateSkillMd(name, version, description, endpoints, resources) {
903
+ let md = `---
904
+ name: "${name}"
905
+ gateway: nkmc
906
+ version: "${version}"
907
+ roles: [agent]
908
+ ---
909
+
910
+ `;
911
+ md += `# ${name}
912
+
913
+ ${description}
914
+
915
+ `;
916
+ if (resources.length > 0) {
917
+ md += `## Schema
918
+
919
+ `;
920
+ for (const r of resources) {
921
+ md += `### ${r.name} (public)
922
+
923
+ `;
924
+ }
925
+ }
926
+ if (endpoints.length > 0) {
927
+ md += `## API
928
+
929
+ `;
930
+ for (const ep of endpoints) {
931
+ md += `### ${ep.description}
932
+
933
+ `;
934
+ md += `\`${ep.method} ${ep.path}\`
935
+
936
+ `;
937
+ if (ep.parameters && ep.parameters.length > 0) {
938
+ md += "**Parameters:**\n\n";
939
+ md += "| name | in | type | required |\n|------|-----|------|----------|\n";
940
+ for (const p of ep.parameters) {
941
+ md += `| ${p.name} | ${p.in} | ${p.type} | ${p.required ? "*" : ""} |
942
+ `;
943
+ }
944
+ md += "\n";
945
+ }
946
+ if (ep.requestBody) {
947
+ const req = ep.requestBody;
948
+ md += `**Body** (${req.contentType}${req.required ? ", required" : ""}):
949
+
950
+ `;
951
+ md += propsTable(req.properties);
952
+ md += "\n";
953
+ }
954
+ if (ep.responses && ep.responses.length > 0) {
955
+ for (const r of ep.responses) {
956
+ md += `**Response ${r.status}**${r.description ? `: ${r.description}` : ""}
957
+
958
+ `;
959
+ if (r.properties && r.properties.length > 0) {
960
+ md += propsTable(r.properties);
961
+ md += "\n";
962
+ }
963
+ }
964
+ }
965
+ }
966
+ }
967
+ return md;
968
+ }
969
+
970
+ // src/http/routes/registry.ts
971
+ var OPENAPI_PATHS = [
972
+ "/openapi.json",
973
+ "/openapi.yaml",
974
+ "/swagger.json",
975
+ "/swagger.yaml",
976
+ "/docs/openapi.json",
977
+ "/api-docs",
978
+ "/api/openapi.json",
979
+ "/.well-known/openapi.json",
980
+ "/.well-known/openapi.yaml"
981
+ ];
982
+ function registryRoutes(options) {
983
+ const { store } = options;
984
+ const app = new import_hono2.Hono();
985
+ app.post("/services", async (c) => {
986
+ const domain = c.req.query("domain");
987
+ if (!domain) {
988
+ return c.json({ error: "Missing ?domain= query parameter" }, 400);
989
+ }
990
+ const auth = c.get("publishAuth");
991
+ if (auth?.type === "publish" && auth.domain !== domain) {
992
+ return c.json(
993
+ { error: `Token is scoped to "${auth.domain}", cannot register "${domain}"` },
994
+ 403
995
+ );
996
+ }
997
+ const contentType = c.req.header("Content-Type") ?? "";
998
+ let skillMd;
999
+ if (contentType.includes("text/markdown") || contentType.includes("text/plain")) {
1000
+ skillMd = await c.req.text();
1001
+ } else {
1002
+ const body = await c.req.json().catch(() => null);
1003
+ if (!body?.skillMd) {
1004
+ return c.json({ error: "Body must be skill.md text or JSON with skillMd field" }, 400);
1005
+ }
1006
+ skillMd = body.skillMd;
1007
+ if (Array.isArray(body.endpoints) && body.endpoints.length > 0) {
1008
+ if (!skillMd.trim()) {
1009
+ return c.json({ error: "Empty skill.md content" }, 400);
1010
+ }
1011
+ const isFirstParty2 = c.req.query("first_party") === "true";
1012
+ const authMode2 = c.req.query("auth_mode");
1013
+ const record2 = parseSkillMd(domain, skillMd, { isFirstParty: isFirstParty2 });
1014
+ record2.endpoints = body.endpoints;
1015
+ if (body.source) {
1016
+ record2.source = body.source;
1017
+ }
1018
+ if (authMode2 === "nkmc-jwt") {
1019
+ record2.authMode = "nkmc-jwt";
1020
+ }
1021
+ await store.put(domain, record2);
1022
+ return c.json({ ok: true, domain, name: record2.name }, 201);
1023
+ }
1024
+ }
1025
+ if (!skillMd.trim()) {
1026
+ return c.json({ error: "Empty skill.md content" }, 400);
1027
+ }
1028
+ const isFirstParty = c.req.query("first_party") === "true";
1029
+ const authMode = c.req.query("auth_mode");
1030
+ const record = parseSkillMd(domain, skillMd, { isFirstParty });
1031
+ if (authMode === "nkmc-jwt") {
1032
+ record.authMode = "nkmc-jwt";
1033
+ }
1034
+ await store.put(domain, record);
1035
+ return c.json({ ok: true, domain, name: record.name }, 201);
1036
+ });
1037
+ app.post("/services/discover", async (c) => {
1038
+ const body = await c.req.json().catch(() => null);
1039
+ if (!body?.url) {
1040
+ return c.json({ error: "Missing 'url' field (base URL of the service)" }, 400);
1041
+ }
1042
+ const baseUrl = body.url.replace(/\/+$/, "");
1043
+ let domain;
1044
+ try {
1045
+ domain = body.domain ?? new URL(baseUrl).hostname;
1046
+ } catch {
1047
+ return c.json({ error: "Invalid URL" }, 400);
1048
+ }
1049
+ const auth = c.get("publishAuth");
1050
+ if (auth?.type === "publish" && auth.domain !== domain) {
1051
+ return c.json(
1052
+ { error: `Token is scoped to "${auth.domain}", cannot register "${domain}"` },
1053
+ 403
1054
+ );
1055
+ }
1056
+ if (body.specUrl) {
1057
+ try {
1058
+ const result = await fetchAndCompile(body.specUrl, { domain });
1059
+ await store.put(domain, result.record);
1060
+ return c.json({
1061
+ ok: true,
1062
+ domain,
1063
+ name: result.record.name,
1064
+ endpoints: result.record.endpoints.length,
1065
+ source: body.specUrl
1066
+ }, 201);
1067
+ } catch (err) {
1068
+ return c.json({ error: `Failed to compile spec: ${err instanceof Error ? err.message : err}` }, 400);
1069
+ }
1070
+ }
1071
+ for (const path of OPENAPI_PATHS) {
1072
+ const specUrl = `${baseUrl}${path}`;
1073
+ try {
1074
+ const resp = await fetch(specUrl, { method: "GET", headers: { Accept: "application/json, application/yaml" } });
1075
+ if (!resp.ok) continue;
1076
+ const text = await resp.text();
1077
+ if (!text.trim() || text.length < 20) continue;
1078
+ try {
1079
+ const result = await fetchAndCompile(specUrl, { domain });
1080
+ await store.put(domain, result.record);
1081
+ return c.json({
1082
+ ok: true,
1083
+ domain,
1084
+ name: result.record.name,
1085
+ endpoints: result.record.endpoints.length,
1086
+ source: specUrl
1087
+ }, 201);
1088
+ } catch {
1089
+ continue;
1090
+ }
1091
+ } catch {
1092
+ continue;
1093
+ }
1094
+ }
1095
+ return c.json({
1096
+ error: "Could not find OpenAPI spec",
1097
+ probed: OPENAPI_PATHS.map((p) => `${baseUrl}${p}`),
1098
+ hint: "Use --spec-url to provide the spec location directly"
1099
+ }, 404);
1100
+ });
1101
+ app.get("/services", async (c) => {
1102
+ const query = c.req.query("q");
1103
+ if (query) {
1104
+ const results = await store.search(query);
1105
+ return c.json(results);
1106
+ }
1107
+ const list = await store.list();
1108
+ return c.json(list);
1109
+ });
1110
+ app.get("/services/:domain", async (c) => {
1111
+ const domain = c.req.param("domain");
1112
+ const record = await store.get(domain);
1113
+ if (!record) {
1114
+ return c.json({ error: "Service not found" }, 404);
1115
+ }
1116
+ return c.json(record);
1117
+ });
1118
+ app.get("/services/:domain/versions", async (c) => {
1119
+ const domain = c.req.param("domain");
1120
+ const versions = await store.listVersions(domain);
1121
+ return c.json({ domain, versions });
1122
+ });
1123
+ app.get("/services/:domain/versions/:version", async (c) => {
1124
+ const domain = c.req.param("domain");
1125
+ const version = c.req.param("version");
1126
+ const record = await store.getVersion(domain, version);
1127
+ if (!record) {
1128
+ return c.json({ error: "Version not found" }, 404);
1129
+ }
1130
+ return c.json(record);
1131
+ });
1132
+ app.delete("/services/:domain", async (c) => {
1133
+ const domain = c.req.param("domain");
1134
+ const existing = await store.get(domain);
1135
+ if (!existing) {
1136
+ return c.json({ error: "Service not found" }, 404);
1137
+ }
1138
+ await store.delete(domain);
1139
+ return c.json({ ok: true, domain });
1140
+ });
1141
+ return app;
1142
+ }
1143
+
1144
+ // src/http/routes/domains.ts
1145
+ var import_hono3 = require("hono");
1146
+ var import_nanoid = require("nanoid");
1147
+ var import_core5 = require("@nkmc/core");
1148
+
1149
+ // src/http/lib/dns.ts
1150
+ async function queryDnsTxt(domain) {
1151
+ const url = new URL("https://cloudflare-dns.com/dns-query");
1152
+ url.searchParams.set("name", domain);
1153
+ url.searchParams.set("type", "TXT");
1154
+ const res = await fetch(url.toString(), {
1155
+ headers: { Accept: "application/dns-json" }
1156
+ });
1157
+ if (!res.ok) {
1158
+ throw new Error(`DNS query failed: ${res.status}`);
1159
+ }
1160
+ const data = await res.json();
1161
+ return (data.Answer ?? []).filter((a) => a.type === 16).map((a) => a.data.replace(/^"|"$/g, ""));
1162
+ }
1163
+
1164
+ // src/http/routes/domains.ts
1165
+ var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
1166
+ var ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1e3;
1167
+ function isValidDomain(domain) {
1168
+ 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);
1169
+ }
1170
+ function domainRoutes(options) {
1171
+ const { db, gatewayPrivateKey } = options;
1172
+ const app = new import_hono3.Hono();
1173
+ app.post("/challenge", async (c) => {
1174
+ const body = await c.req.json().catch(() => null);
1175
+ const domain = body?.domain;
1176
+ if (!domain || !isValidDomain(domain)) {
1177
+ return c.json({ error: "Invalid or missing domain" }, 400);
1178
+ }
1179
+ const now = Date.now();
1180
+ const verified = await db.prepare(
1181
+ "SELECT * FROM domain_challenges WHERE domain = ? AND status = 'verified' AND expires_at > ?"
1182
+ ).bind(domain, now).first();
1183
+ if (verified) {
1184
+ return c.json(
1185
+ {
1186
+ error: "Domain already verified. Use `nkmc claim <domain> --verify` to renew your token.",
1187
+ expiresAt: verified.expires_at
1188
+ },
1189
+ 409
1190
+ );
1191
+ }
1192
+ const existing = await db.prepare(
1193
+ "SELECT * FROM domain_challenges WHERE domain = ? AND status = 'pending' AND expires_at > ?"
1194
+ ).bind(domain, now).first();
1195
+ if (existing) {
1196
+ return c.json({
1197
+ domain,
1198
+ txtRecord: `_nkmc.${domain}`,
1199
+ txtValue: `nkmc-verify=${existing.challenge_code}`,
1200
+ expiresAt: existing.expires_at
1201
+ });
1202
+ }
1203
+ const challengeCode = (0, import_nanoid.nanoid)(32);
1204
+ const expiresAt = now + SEVEN_DAYS_MS;
1205
+ await db.prepare(
1206
+ `INSERT OR REPLACE INTO domain_challenges (domain, challenge_code, status, created_at, expires_at)
1207
+ VALUES (?, ?, 'pending', ?, ?)`
1208
+ ).bind(domain, challengeCode, now, expiresAt).run();
1209
+ return c.json({
1210
+ domain,
1211
+ txtRecord: `_nkmc.${domain}`,
1212
+ txtValue: `nkmc-verify=${challengeCode}`,
1213
+ expiresAt
1214
+ });
1215
+ });
1216
+ app.post("/verify", async (c) => {
1217
+ const body = await c.req.json().catch(() => null);
1218
+ const domain = body?.domain;
1219
+ if (!domain || !isValidDomain(domain)) {
1220
+ return c.json({ error: "Invalid or missing domain" }, 400);
1221
+ }
1222
+ const now = Date.now();
1223
+ const verified = await db.prepare(
1224
+ "SELECT * FROM domain_challenges WHERE domain = ? AND status = 'verified' AND expires_at > ?"
1225
+ ).bind(domain, now).first();
1226
+ if (verified) {
1227
+ const publishToken2 = await (0, import_core5.signPublishToken)(gatewayPrivateKey, domain);
1228
+ return c.json({ ok: true, domain, publishToken: publishToken2 });
1229
+ }
1230
+ const challenge = await db.prepare(
1231
+ "SELECT * FROM domain_challenges WHERE domain = ? AND status = 'pending' AND expires_at > ?"
1232
+ ).bind(domain, now).first();
1233
+ if (!challenge) {
1234
+ return c.json(
1235
+ { error: "No pending challenge found. Run `nkmc claim <domain>` first." },
1236
+ 404
1237
+ );
1238
+ }
1239
+ const expectedValue = `nkmc-verify=${challenge.challenge_code}`;
1240
+ let txtRecords;
1241
+ try {
1242
+ txtRecords = await queryDnsTxt(`_nkmc.${domain}`);
1243
+ } catch {
1244
+ return c.json(
1245
+ { error: "Failed to query DNS. Please try again later." },
1246
+ 502
1247
+ );
1248
+ }
1249
+ if (!txtRecords.includes(expectedValue)) {
1250
+ return c.json(
1251
+ {
1252
+ error: `DNS TXT record not found. Expected TXT record on _nkmc.${domain} with value "${expectedValue}". DNS propagation can take up to 5 minutes.`
1253
+ },
1254
+ 422
1255
+ );
1256
+ }
1257
+ await db.prepare(
1258
+ "UPDATE domain_challenges SET status = 'verified', verified_at = ?, expires_at = ? WHERE domain = ?"
1259
+ ).bind(now, now + ONE_YEAR_MS, domain).run();
1260
+ const publishToken = await (0, import_core5.signPublishToken)(gatewayPrivateKey, domain);
1261
+ return c.json({ ok: true, domain, publishToken });
1262
+ });
1263
+ return app;
1264
+ }
1265
+
1266
+ // src/http/routes/credentials.ts
1267
+ var import_hono4 = require("hono");
1268
+ function credentialRoutes(options) {
1269
+ const { vault } = options;
1270
+ const app = new import_hono4.Hono();
1271
+ app.put("/:domain", async (c) => {
1272
+ const domain = c.req.param("domain");
1273
+ const body = await c.req.json();
1274
+ if (!body.auth?.type) {
1275
+ return c.json({ error: "Missing auth.type" }, 400);
1276
+ }
1277
+ await vault.putPool(domain, body.auth);
1278
+ return c.json({ ok: true, domain });
1279
+ });
1280
+ app.get("/", async (c) => {
1281
+ const domains = await vault.listDomains();
1282
+ return c.json({ domains });
1283
+ });
1284
+ app.delete("/:domain", async (c) => {
1285
+ const domain = c.req.param("domain");
1286
+ await vault.delete(domain);
1287
+ return c.json({ ok: true, domain });
1288
+ });
1289
+ return app;
1290
+ }
1291
+
1292
+ // src/http/routes/byok.ts
1293
+ var import_hono5 = require("hono");
1294
+ function byokRoutes(options) {
1295
+ const { vault } = options;
1296
+ const app = new import_hono5.Hono();
1297
+ app.put("/:domain", async (c) => {
1298
+ const domain = c.req.param("domain");
1299
+ const agent = c.get("agent");
1300
+ const body = await c.req.json();
1301
+ if (!body.auth?.type) {
1302
+ return c.json({ error: "Missing auth.type" }, 400);
1303
+ }
1304
+ await vault.putByok(domain, agent.id, body.auth);
1305
+ return c.json({ ok: true, domain });
1306
+ });
1307
+ app.get("/", async (c) => {
1308
+ const agent = c.get("agent");
1309
+ const allDomains = await vault.listDomains();
1310
+ const byokDomains = [];
1311
+ for (const domain of allDomains) {
1312
+ const cred = await vault.get(domain, agent.id);
1313
+ if (cred && cred.scope === "byok" && cred.developerId === agent.id) {
1314
+ byokDomains.push(domain);
1315
+ }
1316
+ }
1317
+ return c.json({ domains: byokDomains });
1318
+ });
1319
+ app.delete("/:domain", async (c) => {
1320
+ const domain = c.req.param("domain");
1321
+ const agent = c.get("agent");
1322
+ await vault.delete(domain, agent.id);
1323
+ return c.json({ ok: true, domain });
1324
+ });
1325
+ return app;
1326
+ }
1327
+
1328
+ // src/http/routes/fs.ts
1329
+ var import_hono6 = require("hono");
1330
+ function errorToStatus(code) {
1331
+ switch (code) {
1332
+ case "PARSE_ERROR":
1333
+ case "INVALID_PATH":
1334
+ return 400;
1335
+ case "PERMISSION_DENIED":
1336
+ return 403;
1337
+ case "NOT_FOUND":
1338
+ case "NO_MOUNT":
1339
+ return 404;
1340
+ default:
1341
+ return 500;
1342
+ }
1343
+ }
1344
+ function fsRoutes(options) {
1345
+ const { agentFs } = options;
1346
+ const app = new import_hono6.Hono();
1347
+ app.post("/execute", async (c) => {
1348
+ const body = await c.req.json();
1349
+ if (!body.command || typeof body.command !== "string") {
1350
+ return c.json({ error: "Missing 'command' field" }, 400);
1351
+ }
1352
+ const agent = c.get("agent");
1353
+ const roles = body.roles ?? agent?.roles;
1354
+ const result = await agentFs.execute(body.command, roles, agent);
1355
+ const status = result.ok ? 200 : errorToStatus(result.error.code);
1356
+ return c.json(result, status);
1357
+ });
1358
+ app.all("/fs/*", async (c) => {
1359
+ const fullPath = c.req.path;
1360
+ const virtualPath = fullPath.slice(fullPath.indexOf("/fs") + 3) || "/";
1361
+ const query = c.req.query("q");
1362
+ const agent = c.get("agent");
1363
+ const roles = agent?.roles;
1364
+ let op;
1365
+ let data;
1366
+ let pattern;
1367
+ switch (c.req.method) {
1368
+ case "GET":
1369
+ if (query) {
1370
+ op = "grep";
1371
+ pattern = query;
1372
+ } else if (virtualPath.endsWith("/")) {
1373
+ op = "ls";
1374
+ } else {
1375
+ op = "cat";
1376
+ }
1377
+ break;
1378
+ case "POST":
1379
+ case "PUT":
1380
+ op = "write";
1381
+ data = await c.req.json();
1382
+ break;
1383
+ case "DELETE":
1384
+ op = "rm";
1385
+ break;
1386
+ default:
1387
+ return c.json({ error: "Method not allowed" }, 405);
1388
+ }
1389
+ const result = await agentFs.executeCommand(
1390
+ { op, path: virtualPath, data, pattern },
1391
+ roles,
1392
+ agent
1393
+ );
1394
+ const status = result.ok ? 200 : errorToStatus(result.error.code);
1395
+ return c.json(result, status);
1396
+ });
1397
+ return app;
1398
+ }
1399
+
1400
+ // src/http/routes/proxy.ts
1401
+ var import_hono7 = require("hono");
1402
+ function proxyRoutes(options) {
1403
+ const { vault, toolRegistry, exec } = options;
1404
+ const app = new import_hono7.Hono();
1405
+ app.post("/exec", async (c) => {
1406
+ const body = await c.req.json();
1407
+ if (!body.tool || typeof body.tool !== "string") {
1408
+ return c.json({ error: "Missing 'tool' field" }, 400);
1409
+ }
1410
+ const toolDef = toolRegistry.get(body.tool);
1411
+ if (!toolDef) {
1412
+ return c.json({ error: `Unknown tool: ${body.tool}` }, 404);
1413
+ }
1414
+ const agent = c.get("agent");
1415
+ const credential = await vault.get(toolDef.credentialDomain, agent.id);
1416
+ if (!credential) {
1417
+ return c.json({ error: `No credential for domain: ${toolDef.credentialDomain}` }, 401);
1418
+ }
1419
+ const env = toolRegistry.buildEnv(toolDef, credential.auth);
1420
+ const args = body.args ?? [];
1421
+ const result = await exec(body.tool, args, env);
1422
+ return c.json(result);
1423
+ });
1424
+ app.get("/tools", (c) => {
1425
+ const tools = toolRegistry.list().map((t) => ({
1426
+ name: t.name,
1427
+ credentialDomain: t.credentialDomain
1428
+ }));
1429
+ return c.json({ tools });
1430
+ });
1431
+ return app;
1432
+ }
1433
+
1434
+ // src/http/routes/peers.ts
1435
+ var import_hono8 = require("hono");
1436
+ function peerRoutes(options) {
1437
+ const { peerStore } = options;
1438
+ const app = new import_hono8.Hono();
1439
+ app.get("/peers", async (c) => {
1440
+ const peers = await peerStore.listPeers();
1441
+ const safe = peers.map(({ sharedSecret: _, ...rest }) => rest);
1442
+ return c.json({ peers: safe });
1443
+ });
1444
+ app.put("/peers/:id", async (c) => {
1445
+ const id = c.req.param("id");
1446
+ const body = await c.req.json();
1447
+ if (!body.name || !body.url || !body.sharedSecret) {
1448
+ return c.json({ error: "Missing required fields: name, url, sharedSecret" }, 400);
1449
+ }
1450
+ const existing = await peerStore.getPeer(id);
1451
+ const now = Date.now();
1452
+ const peer = {
1453
+ id,
1454
+ name: body.name,
1455
+ url: body.url,
1456
+ sharedSecret: body.sharedSecret,
1457
+ status: "active",
1458
+ advertisedDomains: existing?.advertisedDomains ?? [],
1459
+ lastSeen: existing?.lastSeen ?? 0,
1460
+ createdAt: existing?.createdAt ?? now
1461
+ };
1462
+ await peerStore.putPeer(peer);
1463
+ return c.json({ ok: true, id });
1464
+ });
1465
+ app.delete("/peers/:id", async (c) => {
1466
+ const id = c.req.param("id");
1467
+ await peerStore.deletePeer(id);
1468
+ return c.json({ ok: true, id });
1469
+ });
1470
+ app.get("/rules", async (c) => {
1471
+ const rules = await peerStore.listRules();
1472
+ return c.json({ rules });
1473
+ });
1474
+ app.put("/rules/:domain", async (c) => {
1475
+ const domain = c.req.param("domain");
1476
+ const body = await c.req.json();
1477
+ if (body.allow === void 0) {
1478
+ return c.json({ error: "Missing required field: allow" }, 400);
1479
+ }
1480
+ const existing = await peerStore.getRule(domain);
1481
+ const now = Date.now();
1482
+ const rule = {
1483
+ domain,
1484
+ allow: body.allow,
1485
+ peers: body.peers ?? existing?.peers ?? "*",
1486
+ pricing: body.pricing ?? existing?.pricing ?? { mode: "free" },
1487
+ rateLimit: body.rateLimit,
1488
+ createdAt: existing?.createdAt ?? now,
1489
+ updatedAt: now
1490
+ };
1491
+ await peerStore.putRule(rule);
1492
+ return c.json({ ok: true, domain });
1493
+ });
1494
+ app.delete("/rules/:domain", async (c) => {
1495
+ const domain = c.req.param("domain");
1496
+ await peerStore.deleteRule(domain);
1497
+ return c.json({ ok: true, domain });
1498
+ });
1499
+ return app;
1500
+ }
1501
+
1502
+ // src/http/routes/federation.ts
1503
+ var import_hono9 = require("hono");
1504
+ async function authenticatePeer(peerStore, peerId, authHeader) {
1505
+ if (!peerId || !authHeader) return null;
1506
+ const peer = await peerStore.getPeer(peerId);
1507
+ if (!peer || peer.status !== "active") return null;
1508
+ const token = authHeader.replace(/^Bearer\s+/i, "");
1509
+ if (token !== peer.sharedSecret) return null;
1510
+ return peer;
1511
+ }
1512
+ function extractDomainFromCommand(command) {
1513
+ const parts = command.trim().split(/\s+/);
1514
+ if (parts.length < 2) return null;
1515
+ const path = parts[1];
1516
+ if (!path.startsWith("/")) return null;
1517
+ const segments = path.slice(1).split("/");
1518
+ return segments[0] || null;
1519
+ }
1520
+ function federationRoutes(options) {
1521
+ const { peerStore, vault, agentFs } = options;
1522
+ const app = new import_hono9.Hono();
1523
+ app.post("/query", async (c) => {
1524
+ const peerId = c.req.header("X-Peer-Id");
1525
+ const authHeader = c.req.header("Authorization");
1526
+ const peer = await authenticatePeer(peerStore, peerId, authHeader);
1527
+ if (!peer) {
1528
+ return c.json({ error: "Unauthorized peer" }, 403);
1529
+ }
1530
+ const body = await c.req.json();
1531
+ if (!body.domain) {
1532
+ return c.json({ error: "Missing 'domain' field" }, 400);
1533
+ }
1534
+ await peerStore.updateLastSeen(peer.id, Date.now());
1535
+ const credential = await vault.get(body.domain);
1536
+ if (!credential) {
1537
+ return c.json({ available: false });
1538
+ }
1539
+ const rule = await peerStore.getRule(body.domain);
1540
+ if (!rule || !rule.allow) {
1541
+ return c.json({ available: false });
1542
+ }
1543
+ if (rule.peers !== "*" && !rule.peers.includes(peer.id)) {
1544
+ return c.json({ available: false });
1545
+ }
1546
+ return c.json({
1547
+ available: true,
1548
+ pricing: rule.pricing
1549
+ });
1550
+ });
1551
+ app.post("/exec", async (c) => {
1552
+ const peerId = c.req.header("X-Peer-Id");
1553
+ const authHeader = c.req.header("Authorization");
1554
+ const peer = await authenticatePeer(peerStore, peerId, authHeader);
1555
+ if (!peer) {
1556
+ return c.json({ error: "Unauthorized peer" }, 403);
1557
+ }
1558
+ const body = await c.req.json();
1559
+ if (!body.command) {
1560
+ return c.json({ error: "Missing 'command' field" }, 400);
1561
+ }
1562
+ await peerStore.updateLastSeen(peer.id, Date.now());
1563
+ const domain = extractDomainFromCommand(body.command);
1564
+ if (domain) {
1565
+ const rule = await peerStore.getRule(domain);
1566
+ if (!rule || !rule.allow) {
1567
+ return c.json({ error: "Domain not available for lending" }, 403);
1568
+ }
1569
+ if (rule.peers !== "*" && !rule.peers.includes(peer.id)) {
1570
+ return c.json({ error: "Peer not in allowed list" }, 403);
1571
+ }
1572
+ if (rule.pricing.mode !== "free") {
1573
+ const paymentHeader = c.req.header("X-402-Payment");
1574
+ if (!paymentHeader) {
1575
+ return c.json({ error: "Payment required" }, 402);
1576
+ }
1577
+ }
1578
+ }
1579
+ const syntheticAgentId = `peer:${peer.id}:${body.agentId}`;
1580
+ const result = await agentFs.execute(body.command, ["agent"], {
1581
+ id: syntheticAgentId,
1582
+ roles: ["agent"]
1583
+ });
1584
+ if (!result.ok) {
1585
+ return c.json({ ok: false, error: result.error.message }, 500);
1586
+ }
1587
+ return c.json({ ok: true, data: result.data });
1588
+ });
1589
+ app.post("/announce", async (c) => {
1590
+ const peerId = c.req.header("X-Peer-Id");
1591
+ const authHeader = c.req.header("Authorization");
1592
+ const peer = await authenticatePeer(peerStore, peerId, authHeader);
1593
+ if (!peer) {
1594
+ return c.json({ error: "Unauthorized peer" }, 403);
1595
+ }
1596
+ const body = await c.req.json();
1597
+ if (!Array.isArray(body.domains)) {
1598
+ return c.json({ error: "Missing 'domains' field" }, 400);
1599
+ }
1600
+ peer.advertisedDomains = body.domains;
1601
+ await peerStore.putPeer(peer);
1602
+ await peerStore.updateLastSeen(peer.id, Date.now());
1603
+ return c.json({ ok: true });
1604
+ });
1605
+ return app;
1606
+ }
1607
+
1608
+ // src/http/routes/tunnels.ts
1609
+ var import_hono10 = require("hono");
1610
+ var import_nanoid2 = require("nanoid");
1611
+ function tunnelRoutes(options) {
1612
+ const { tunnelStore, tunnelProvider, tunnelDomain } = options;
1613
+ const app = new import_hono10.Hono();
1614
+ app.post("/create", async (c) => {
1615
+ const agent = c.get("agent");
1616
+ const body = await c.req.json().catch(() => ({}));
1617
+ const existing = await tunnelStore.getByAgent(agent.id);
1618
+ if (existing && existing.status === "active") {
1619
+ return c.json({
1620
+ tunnelId: existing.id,
1621
+ publicUrl: existing.publicUrl,
1622
+ message: "Tunnel already exists"
1623
+ });
1624
+ }
1625
+ const id = (0, import_nanoid2.nanoid)(12);
1626
+ const hostname = `${id}.${tunnelDomain}`;
1627
+ const publicUrl = `https://${hostname}`;
1628
+ const { tunnelId, tunnelToken } = await tunnelProvider.create(
1629
+ `nkmc-${agent.id}-${id}`,
1630
+ hostname
1631
+ );
1632
+ const now = Date.now();
1633
+ await tunnelStore.put({
1634
+ id,
1635
+ agentId: agent.id,
1636
+ tunnelId,
1637
+ publicUrl,
1638
+ status: "active",
1639
+ createdAt: now,
1640
+ advertisedDomains: body.advertisedDomains ?? [],
1641
+ gatewayName: body.gatewayName,
1642
+ lastSeen: now
1643
+ });
1644
+ return c.json({ tunnelId: id, tunnelToken, publicUrl }, 201);
1645
+ });
1646
+ app.delete("/:id", async (c) => {
1647
+ const id = c.req.param("id");
1648
+ const agent = c.get("agent");
1649
+ const record = await tunnelStore.get(id);
1650
+ if (!record) return c.json({ error: "Tunnel not found" }, 404);
1651
+ if (record.agentId !== agent.id)
1652
+ return c.json({ error: "Not your tunnel" }, 403);
1653
+ await tunnelProvider.delete(record.tunnelId);
1654
+ await tunnelStore.delete(id);
1655
+ return c.json({ ok: true });
1656
+ });
1657
+ app.get("/", async (c) => {
1658
+ const agent = c.get("agent");
1659
+ const all = await tunnelStore.list();
1660
+ const mine = all.filter((t) => t.agentId === agent.id);
1661
+ return c.json({ tunnels: mine });
1662
+ });
1663
+ app.get("/discover", async (c) => {
1664
+ const domain = c.req.query("domain");
1665
+ const all = await tunnelStore.list();
1666
+ let results = all.filter((t) => t.status === "active");
1667
+ if (domain) {
1668
+ results = results.filter((t) => t.advertisedDomains.includes(domain));
1669
+ }
1670
+ return c.json({
1671
+ gateways: results.map((t) => ({
1672
+ id: t.id,
1673
+ name: t.gatewayName ?? `gateway-${t.id}`,
1674
+ publicUrl: t.publicUrl,
1675
+ advertisedDomains: t.advertisedDomains
1676
+ }))
1677
+ });
1678
+ });
1679
+ app.post("/heartbeat", async (c) => {
1680
+ const agent = c.get("agent");
1681
+ const body = await c.req.json();
1682
+ const record = await tunnelStore.getByAgent(agent.id);
1683
+ if (!record) return c.json({ error: "No active tunnel" }, 404);
1684
+ record.advertisedDomains = body.advertisedDomains ?? record.advertisedDomains;
1685
+ record.lastSeen = Date.now();
1686
+ await tunnelStore.put(record);
1687
+ return c.json({ ok: true });
1688
+ });
1689
+ return app;
1690
+ }
1691
+
1692
+ // src/http/app.ts
1693
+ function createGateway(options) {
1694
+ const { store, gatewayPrivateKey, gatewayPublicKey, adminToken } = options;
1695
+ const app = new import_hono11.Hono();
1696
+ const { onMiss, listDomains, searchDomains, searchEndpoints } = createRegistryResolver(
1697
+ options.vault ? { store, vault: options.vault, gatewayPrivateKey } : { store, gatewayPrivateKey }
1698
+ );
1699
+ const mounts = [];
1700
+ if (options.context7ApiKey) {
1701
+ mounts.push({ path: "/context7", backend: new Context7Backend({ apiKey: options.context7ApiKey }) });
1702
+ }
1703
+ const agentFs = new import_agent_fs2.AgentFs({
1704
+ mounts,
1705
+ onMiss,
1706
+ listDomains,
1707
+ searchDomains,
1708
+ searchEndpoints
1709
+ });
1710
+ app.get("/.well-known/jwks.json", (c) => {
1711
+ return c.json({ keys: [gatewayPublicKey] });
1712
+ });
1713
+ app.route("/auth", authRoutes({ privateKey: gatewayPrivateKey }));
1714
+ if (options.db) {
1715
+ app.route("/domains", domainRoutes({ db: options.db, gatewayPrivateKey }));
1716
+ }
1717
+ app.use("/registry/*", publishOrAdminAuth(adminToken, gatewayPublicKey));
1718
+ app.route("/registry", registryRoutes({ store }));
1719
+ if (options.vault) {
1720
+ app.use("/credentials/*", adminAuth(adminToken));
1721
+ app.route("/credentials", credentialRoutes({ vault: options.vault }));
1722
+ app.use("/byok/*", agentAuth(gatewayPublicKey));
1723
+ app.route("/byok", byokRoutes({ vault: options.vault }));
1724
+ }
1725
+ app.use("/execute", agentAuth(gatewayPublicKey));
1726
+ app.use("/fs/*", agentAuth(gatewayPublicKey));
1727
+ app.route("/", fsRoutes({ agentFs }));
1728
+ if (options.proxy && options.vault) {
1729
+ app.use("/proxy/*", agentAuth(gatewayPublicKey));
1730
+ app.route(
1731
+ "/proxy",
1732
+ proxyRoutes({
1733
+ vault: options.vault,
1734
+ toolRegistry: options.proxy.toolRegistry,
1735
+ exec: options.proxy.exec
1736
+ })
1737
+ );
1738
+ }
1739
+ if (options.peerStore && options.vault) {
1740
+ app.use("/admin/federation/*", adminAuth(adminToken));
1741
+ app.route("/admin/federation", peerRoutes({ peerStore: options.peerStore }));
1742
+ app.route("/federation", federationRoutes({
1743
+ peerStore: options.peerStore,
1744
+ vault: options.vault,
1745
+ agentFs
1746
+ }));
1747
+ }
1748
+ if (options.tunnel) {
1749
+ app.use("/tunnels/*", agentAuth(gatewayPublicKey));
1750
+ app.route(
1751
+ "/tunnels",
1752
+ tunnelRoutes({
1753
+ tunnelStore: options.tunnel.store,
1754
+ tunnelProvider: options.tunnel.provider,
1755
+ tunnelDomain: options.tunnel.domain
1756
+ })
1757
+ );
1758
+ }
1759
+ return app;
1760
+ }
1761
+ // Annotate the CommonJS export names for ESM import in node:
1762
+ 0 && (module.exports = {
1763
+ adminAuth,
1764
+ agentAuth,
1765
+ authRoutes,
1766
+ createGateway,
1767
+ domainRoutes,
1768
+ fsRoutes,
1769
+ publishOrAdminAuth,
1770
+ registryRoutes,
1771
+ tunnelRoutes
1772
+ });