@nkmc/gateway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-56RA53VS.js +37 -0
- package/dist/chunk-CZJ75YTV.js +969 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/http.cjs +1772 -0
- package/dist/http.d.cts +49 -0
- package/dist/http.d.ts +49 -0
- package/dist/http.js +748 -0
- package/dist/index.cjs +2436 -0
- package/dist/index.d.cts +436 -0
- package/dist/index.d.ts +436 -0
- package/dist/index.js +1434 -0
- package/dist/proxy-ClPcDgsO.d.cts +283 -0
- package/dist/proxy-qpda1ANS.d.ts +283 -0
- package/dist/proxy.cjs +148 -0
- package/dist/proxy.d.cts +6 -0
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +90 -0
- package/dist/testing.cjs +865 -0
- package/dist/testing.d.cts +12 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +831 -0
- package/dist/tunnels-BviBEaih.d.cts +12 -0
- package/dist/tunnels-DFHNgmN7.d.ts +12 -0
- package/dist/types-C6JC9oTm.d.cts +21 -0
- package/dist/types-C6JC9oTm.d.ts +21 -0
- package/package.json +47 -0
- package/src/__tests__/sqlite-integration.test.ts +384 -0
- package/src/credential/d1-vault.ts +134 -0
- package/src/credential/memory-vault.ts +50 -0
- package/src/credential/types.ts +16 -0
- package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
- package/src/d1/sqlite-adapter.ts +59 -0
- package/src/d1/types.ts +22 -0
- package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
- package/src/federation/__tests__/peer-client.test.ts +205 -0
- package/src/federation/__tests__/peer-store.test.ts +114 -0
- package/src/federation/d1-peer-store.ts +164 -0
- package/src/federation/peer-backend.ts +60 -0
- package/src/federation/peer-client.ts +122 -0
- package/src/federation/peer-store.ts +45 -0
- package/src/federation/types.ts +39 -0
- package/src/http/app.ts +152 -0
- package/src/http/lib/dns.ts +30 -0
- package/src/http/middleware/admin-auth.ts +18 -0
- package/src/http/middleware/agent-auth.ts +27 -0
- package/src/http/middleware/publish-auth.ts +39 -0
- package/src/http/routes/__tests__/federation.test.ts +364 -0
- package/src/http/routes/__tests__/peers.test.ts +290 -0
- package/src/http/routes/__tests__/proxy.test.ts +159 -0
- package/src/http/routes/auth.ts +39 -0
- package/src/http/routes/byok.ts +62 -0
- package/src/http/routes/credentials.ts +40 -0
- package/src/http/routes/domains.ts +174 -0
- package/src/http/routes/federation.ts +170 -0
- package/src/http/routes/fs.ts +89 -0
- package/src/http/routes/peers.ts +103 -0
- package/src/http/routes/proxy.ts +57 -0
- package/src/http/routes/registry.ts +222 -0
- package/src/http/routes/tunnels.ts +124 -0
- package/src/http.ts +9 -0
- package/src/index.ts +63 -0
- package/src/metering/d1-store.ts +123 -0
- package/src/metering/memory-store.ts +29 -0
- package/src/metering/pricing-guard.ts +68 -0
- package/src/metering/types.ts +25 -0
- package/src/onboard/apis-guru.ts +64 -0
- package/src/onboard/index.ts +4 -0
- package/src/onboard/manifest.ts +362 -0
- package/src/onboard/pipeline.ts +214 -0
- package/src/onboard/types.ts +72 -0
- package/src/proxy/__tests__/tool-registry.test.ts +93 -0
- package/src/proxy/tool-registry.ts +122 -0
- package/src/proxy.ts +12 -0
- package/src/registry/context7-backend.ts +93 -0
- package/src/registry/context7.ts +54 -0
- package/src/registry/d1-store.ts +242 -0
- package/src/registry/memory-store.ts +101 -0
- package/src/registry/openapi-compiler.ts +284 -0
- package/src/registry/resolver.ts +196 -0
- package/src/registry/rpc-compiler.ts +142 -0
- package/src/registry/skill-parser.ts +119 -0
- package/src/registry/skill-to-config.ts +239 -0
- package/src/registry/source-refresher.ts +83 -0
- package/src/registry/types.ts +129 -0
- package/src/registry/virtual-files.ts +76 -0
- package/src/testing/sqlite-d1.ts +64 -0
- package/src/testing.ts +2 -0
- package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
- package/src/tunnel/__tests__/tunnel.test.ts +542 -0
- package/src/tunnel/cloudflare-provider.ts +121 -0
- package/src/tunnel/memory-store.ts +30 -0
- package/src/tunnel/types.ts +28 -0
- package/test/credential/d1-vault.test.ts +127 -0
- package/test/credential/injection.test.ts +67 -0
- package/test/credential/memory-vault.test.ts +63 -0
- package/test/http/app.test.ts +300 -0
- package/test/http/byok-e2e.test.ts +240 -0
- package/test/http/byok.test.ts +115 -0
- package/test/http/credentials.test.ts +57 -0
- package/test/http/e2e.test.ts +260 -0
- package/test/integration/authenticated-apis.test.ts +185 -0
- package/test/integration/free-apis-e2e.test.ts +222 -0
- package/test/metering/d1-store.test.ts +82 -0
- package/test/metering/memory-store.test.ts +76 -0
- package/test/metering/pricing-guard.test.ts +108 -0
- package/test/onboard/apis-guru.test.ts +57 -0
- package/test/onboard/e2e.test.ts +70 -0
- package/test/onboard/pipeline.test.ts +318 -0
- package/test/onboard/real-apis.test.ts +483 -0
- package/test/registry/compilation-correctness.test.ts +132 -0
- package/test/registry/context7-backend.test.ts +88 -0
- package/test/registry/context7-e2e.test.ts +92 -0
- package/test/registry/context7.test.ts +73 -0
- package/test/registry/d1-store.test.ts +184 -0
- package/test/registry/integration.test.ts +129 -0
- package/test/registry/lazy-mount.test.ts +138 -0
- package/test/registry/memory-store.test.ts +171 -0
- package/test/registry/openapi-compiler.test.ts +267 -0
- package/test/registry/openapi-e2e.test.ts +154 -0
- package/test/registry/passthrough-e2e.test.ts +109 -0
- package/test/registry/resolver-peer.test.ts +299 -0
- package/test/registry/resolver.test.ts +228 -0
- package/test/registry/rpc-compiler.test.ts +112 -0
- package/test/registry/skill-parser.test.ts +151 -0
- package/test/registry/skill-to-config.test.ts +151 -0
- package/test/registry/skill-to-rpc-config.test.ts +142 -0
- package/test/registry/source-refresher.test.ts +90 -0
- package/test/registry/virtual-files.test.ts +96 -0
- package/tsconfig.json +4 -0
- package/tsup.config.ts +8 -0
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
|
+
});
|