@nkmc/gateway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-56RA53VS.js +37 -0
- package/dist/chunk-CZJ75YTV.js +969 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/http.cjs +1772 -0
- package/dist/http.d.cts +49 -0
- package/dist/http.d.ts +49 -0
- package/dist/http.js +748 -0
- package/dist/index.cjs +2436 -0
- package/dist/index.d.cts +436 -0
- package/dist/index.d.ts +436 -0
- package/dist/index.js +1434 -0
- package/dist/proxy-ClPcDgsO.d.cts +283 -0
- package/dist/proxy-qpda1ANS.d.ts +283 -0
- package/dist/proxy.cjs +148 -0
- package/dist/proxy.d.cts +6 -0
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +90 -0
- package/dist/testing.cjs +865 -0
- package/dist/testing.d.cts +12 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +831 -0
- package/dist/tunnels-BviBEaih.d.cts +12 -0
- package/dist/tunnels-DFHNgmN7.d.ts +12 -0
- package/dist/types-C6JC9oTm.d.cts +21 -0
- package/dist/types-C6JC9oTm.d.ts +21 -0
- package/package.json +47 -0
- package/src/__tests__/sqlite-integration.test.ts +384 -0
- package/src/credential/d1-vault.ts +134 -0
- package/src/credential/memory-vault.ts +50 -0
- package/src/credential/types.ts +16 -0
- package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
- package/src/d1/sqlite-adapter.ts +59 -0
- package/src/d1/types.ts +22 -0
- package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
- package/src/federation/__tests__/peer-client.test.ts +205 -0
- package/src/federation/__tests__/peer-store.test.ts +114 -0
- package/src/federation/d1-peer-store.ts +164 -0
- package/src/federation/peer-backend.ts +60 -0
- package/src/federation/peer-client.ts +122 -0
- package/src/federation/peer-store.ts +45 -0
- package/src/federation/types.ts +39 -0
- package/src/http/app.ts +152 -0
- package/src/http/lib/dns.ts +30 -0
- package/src/http/middleware/admin-auth.ts +18 -0
- package/src/http/middleware/agent-auth.ts +27 -0
- package/src/http/middleware/publish-auth.ts +39 -0
- package/src/http/routes/__tests__/federation.test.ts +364 -0
- package/src/http/routes/__tests__/peers.test.ts +290 -0
- package/src/http/routes/__tests__/proxy.test.ts +159 -0
- package/src/http/routes/auth.ts +39 -0
- package/src/http/routes/byok.ts +62 -0
- package/src/http/routes/credentials.ts +40 -0
- package/src/http/routes/domains.ts +174 -0
- package/src/http/routes/federation.ts +170 -0
- package/src/http/routes/fs.ts +89 -0
- package/src/http/routes/peers.ts +103 -0
- package/src/http/routes/proxy.ts +57 -0
- package/src/http/routes/registry.ts +222 -0
- package/src/http/routes/tunnels.ts +124 -0
- package/src/http.ts +9 -0
- package/src/index.ts +63 -0
- package/src/metering/d1-store.ts +123 -0
- package/src/metering/memory-store.ts +29 -0
- package/src/metering/pricing-guard.ts +68 -0
- package/src/metering/types.ts +25 -0
- package/src/onboard/apis-guru.ts +64 -0
- package/src/onboard/index.ts +4 -0
- package/src/onboard/manifest.ts +362 -0
- package/src/onboard/pipeline.ts +214 -0
- package/src/onboard/types.ts +72 -0
- package/src/proxy/__tests__/tool-registry.test.ts +93 -0
- package/src/proxy/tool-registry.ts +122 -0
- package/src/proxy.ts +12 -0
- package/src/registry/context7-backend.ts +93 -0
- package/src/registry/context7.ts +54 -0
- package/src/registry/d1-store.ts +242 -0
- package/src/registry/memory-store.ts +101 -0
- package/src/registry/openapi-compiler.ts +284 -0
- package/src/registry/resolver.ts +196 -0
- package/src/registry/rpc-compiler.ts +142 -0
- package/src/registry/skill-parser.ts +119 -0
- package/src/registry/skill-to-config.ts +239 -0
- package/src/registry/source-refresher.ts +83 -0
- package/src/registry/types.ts +129 -0
- package/src/registry/virtual-files.ts +76 -0
- package/src/testing/sqlite-d1.ts +64 -0
- package/src/testing.ts +2 -0
- package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
- package/src/tunnel/__tests__/tunnel.test.ts +542 -0
- package/src/tunnel/cloudflare-provider.ts +121 -0
- package/src/tunnel/memory-store.ts +30 -0
- package/src/tunnel/types.ts +28 -0
- package/test/credential/d1-vault.test.ts +127 -0
- package/test/credential/injection.test.ts +67 -0
- package/test/credential/memory-vault.test.ts +63 -0
- package/test/http/app.test.ts +300 -0
- package/test/http/byok-e2e.test.ts +240 -0
- package/test/http/byok.test.ts +115 -0
- package/test/http/credentials.test.ts +57 -0
- package/test/http/e2e.test.ts +260 -0
- package/test/integration/authenticated-apis.test.ts +185 -0
- package/test/integration/free-apis-e2e.test.ts +222 -0
- package/test/metering/d1-store.test.ts +82 -0
- package/test/metering/memory-store.test.ts +76 -0
- package/test/metering/pricing-guard.test.ts +108 -0
- package/test/onboard/apis-guru.test.ts +57 -0
- package/test/onboard/e2e.test.ts +70 -0
- package/test/onboard/pipeline.test.ts +318 -0
- package/test/onboard/real-apis.test.ts +483 -0
- package/test/registry/compilation-correctness.test.ts +132 -0
- package/test/registry/context7-backend.test.ts +88 -0
- package/test/registry/context7-e2e.test.ts +92 -0
- package/test/registry/context7.test.ts +73 -0
- package/test/registry/d1-store.test.ts +184 -0
- package/test/registry/integration.test.ts +129 -0
- package/test/registry/lazy-mount.test.ts +138 -0
- package/test/registry/memory-store.test.ts +171 -0
- package/test/registry/openapi-compiler.test.ts +267 -0
- package/test/registry/openapi-e2e.test.ts +154 -0
- package/test/registry/passthrough-e2e.test.ts +109 -0
- package/test/registry/resolver-peer.test.ts +299 -0
- package/test/registry/resolver.test.ts +228 -0
- package/test/registry/rpc-compiler.test.ts +112 -0
- package/test/registry/skill-parser.test.ts +151 -0
- package/test/registry/skill-to-config.test.ts +151 -0
- package/test/registry/skill-to-rpc-config.test.ts +142 -0
- package/test/registry/source-refresher.test.ts +90 -0
- package/test/registry/virtual-files.test.ts +96 -0
- package/tsconfig.json +4 -0
- package/tsup.config.ts +8 -0
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
// src/registry/skill-to-config.ts
|
|
2
|
+
function skillToHttpConfig(record) {
|
|
3
|
+
let baseUrl = `https://${record.domain}`;
|
|
4
|
+
if (record.source?.basePath) {
|
|
5
|
+
baseUrl += record.source.basePath;
|
|
6
|
+
}
|
|
7
|
+
const resources = extractResources(record.skillMd);
|
|
8
|
+
const endpoints = extractHttpEndpoints(record.skillMd);
|
|
9
|
+
return {
|
|
10
|
+
baseUrl,
|
|
11
|
+
resources,
|
|
12
|
+
endpoints
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function extractResources(skillMd) {
|
|
16
|
+
const resources = [];
|
|
17
|
+
const lines = skillMd.split("\n");
|
|
18
|
+
let inSchema = false;
|
|
19
|
+
let current = null;
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
if (line.startsWith("## Schema")) {
|
|
22
|
+
inSchema = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (inSchema && line.startsWith("## ") && !line.startsWith("## Schema")) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
if (!inSchema) continue;
|
|
29
|
+
const tableMatch = line.match(/^### (\w+)\s/);
|
|
30
|
+
if (tableMatch) {
|
|
31
|
+
if (current) resources.push(toHttpResource(current));
|
|
32
|
+
current = { name: tableMatch[1], fields: [] };
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (current && line.startsWith("|") && !line.startsWith("|--") && !line.startsWith("| field")) {
|
|
36
|
+
const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
|
|
37
|
+
if (cells.length >= 3) {
|
|
38
|
+
current.fields.push({
|
|
39
|
+
name: cells[0],
|
|
40
|
+
type: cells[1],
|
|
41
|
+
description: cells[2]
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (current) resources.push(toHttpResource(current));
|
|
47
|
+
return resources;
|
|
48
|
+
}
|
|
49
|
+
function toHttpResource(parsed) {
|
|
50
|
+
return {
|
|
51
|
+
name: parsed.name,
|
|
52
|
+
apiPath: `/${parsed.name}`,
|
|
53
|
+
fields: parsed.fields
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function extractHttpEndpoints(skillMd) {
|
|
57
|
+
const endpoints = [];
|
|
58
|
+
const lines = skillMd.split("\n");
|
|
59
|
+
let inApi = false;
|
|
60
|
+
let currentHeading = null;
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
if (line.startsWith("## API")) {
|
|
63
|
+
inApi = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (inApi && line.startsWith("## ") && !line.startsWith("## API")) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (!inApi) continue;
|
|
70
|
+
if (line.startsWith("### ")) {
|
|
71
|
+
currentHeading = line.slice(4).trim();
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const match = line.match(/^`(GET|POST|PUT|PATCH|DELETE)\s+(\S+)`/);
|
|
75
|
+
if (match && currentHeading) {
|
|
76
|
+
const slug = currentHeading.toLowerCase().replace(/\s+/g, "-");
|
|
77
|
+
endpoints.push({
|
|
78
|
+
name: slug,
|
|
79
|
+
method: match[1],
|
|
80
|
+
apiPath: match[2],
|
|
81
|
+
description: currentHeading
|
|
82
|
+
});
|
|
83
|
+
currentHeading = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return endpoints;
|
|
87
|
+
}
|
|
88
|
+
function skillToRpcConfig(meta) {
|
|
89
|
+
const resources = meta.resources.map((r) => {
|
|
90
|
+
const methods = {};
|
|
91
|
+
const builder = getParamsBuilder(meta.convention);
|
|
92
|
+
for (const [fsOp, rpcMethod] of Object.entries(r.methods)) {
|
|
93
|
+
const key = fsOp;
|
|
94
|
+
methods[key] = {
|
|
95
|
+
method: rpcMethod,
|
|
96
|
+
params: builder(key)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const resource = {
|
|
100
|
+
name: r.name,
|
|
101
|
+
...r.idField ? { idField: r.idField } : {},
|
|
102
|
+
methods
|
|
103
|
+
};
|
|
104
|
+
if (meta.convention === "evm") {
|
|
105
|
+
resource.transform = buildEvmTransforms(r.name);
|
|
106
|
+
}
|
|
107
|
+
return resource;
|
|
108
|
+
});
|
|
109
|
+
return { resources };
|
|
110
|
+
}
|
|
111
|
+
function getParamsBuilder(convention) {
|
|
112
|
+
switch (convention) {
|
|
113
|
+
case "crud":
|
|
114
|
+
return crudParamsBuilder;
|
|
115
|
+
case "evm":
|
|
116
|
+
return evmParamsBuilder;
|
|
117
|
+
case "raw":
|
|
118
|
+
default:
|
|
119
|
+
return rawParamsBuilder;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function rawParamsBuilder(_fsOp) {
|
|
123
|
+
return (ctx) => {
|
|
124
|
+
if (ctx.data !== void 0) return [ctx.data];
|
|
125
|
+
if (ctx.id !== void 0) return [ctx.id];
|
|
126
|
+
return [];
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function crudParamsBuilder(fsOp) {
|
|
130
|
+
switch (fsOp) {
|
|
131
|
+
case "list":
|
|
132
|
+
return () => [];
|
|
133
|
+
case "read":
|
|
134
|
+
return (ctx) => [ctx.id];
|
|
135
|
+
case "write":
|
|
136
|
+
return (ctx) => [ctx.id, ctx.data];
|
|
137
|
+
case "create":
|
|
138
|
+
return (ctx) => [ctx.data];
|
|
139
|
+
case "remove":
|
|
140
|
+
return (ctx) => [ctx.id];
|
|
141
|
+
case "search":
|
|
142
|
+
return (ctx) => [ctx.pattern];
|
|
143
|
+
default:
|
|
144
|
+
return () => [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function evmParamsBuilder(fsOp) {
|
|
148
|
+
switch (fsOp) {
|
|
149
|
+
case "list":
|
|
150
|
+
return () => [];
|
|
151
|
+
case "read":
|
|
152
|
+
return (ctx) => {
|
|
153
|
+
const id = ctx.id;
|
|
154
|
+
const hexId = /^\d+$/.test(id) ? "0x" + Number(id).toString(16) : id;
|
|
155
|
+
return [hexId, "latest"];
|
|
156
|
+
};
|
|
157
|
+
default:
|
|
158
|
+
return rawParamsBuilder(fsOp);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function buildEvmTransforms(resourceName) {
|
|
162
|
+
const transform = {};
|
|
163
|
+
if (resourceName === "blocks") {
|
|
164
|
+
transform.list = (data) => {
|
|
165
|
+
const hex = String(data);
|
|
166
|
+
const latest = parseInt(hex, 16);
|
|
167
|
+
if (isNaN(latest)) return [];
|
|
168
|
+
return Array.from({ length: 10 }, (_, i) => `${latest - i}.json`);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (resourceName === "balances") {
|
|
172
|
+
transform.read = (data) => {
|
|
173
|
+
const hex = String(data);
|
|
174
|
+
return { wei: hex, raw: hex };
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return Object.keys(transform).length > 0 ? transform : void 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/registry/virtual-files.ts
|
|
181
|
+
var VIRTUAL_FILES = ["_pricing.json", "_versions.json", "skill.md"];
|
|
182
|
+
var VirtualFileBackend = class {
|
|
183
|
+
inner;
|
|
184
|
+
domain;
|
|
185
|
+
store;
|
|
186
|
+
constructor(options) {
|
|
187
|
+
this.inner = options.inner;
|
|
188
|
+
this.domain = options.domain;
|
|
189
|
+
this.store = options.store;
|
|
190
|
+
}
|
|
191
|
+
async list(path) {
|
|
192
|
+
const entries = await this.inner.list(path);
|
|
193
|
+
if (path === "/" || path === "" || path === ".") {
|
|
194
|
+
return [...entries, ...VIRTUAL_FILES];
|
|
195
|
+
}
|
|
196
|
+
return entries;
|
|
197
|
+
}
|
|
198
|
+
async read(path) {
|
|
199
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
200
|
+
if (cleaned === "_pricing.json") {
|
|
201
|
+
const record = await this.store.get(this.domain);
|
|
202
|
+
if (!record) return { endpoints: [] };
|
|
203
|
+
return {
|
|
204
|
+
domain: this.domain,
|
|
205
|
+
endpoints: record.endpoints.filter((ep) => ep.pricing).map((ep) => ({
|
|
206
|
+
method: ep.method,
|
|
207
|
+
path: ep.path,
|
|
208
|
+
description: ep.description,
|
|
209
|
+
pricing: ep.pricing
|
|
210
|
+
}))
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (cleaned === "_versions.json") {
|
|
214
|
+
const versions = await this.store.listVersions(this.domain);
|
|
215
|
+
return { domain: this.domain, versions };
|
|
216
|
+
}
|
|
217
|
+
if (cleaned === "skill.md") {
|
|
218
|
+
const record = await this.store.get(this.domain);
|
|
219
|
+
if (!record) return "# Not found\n";
|
|
220
|
+
return record.skillMd;
|
|
221
|
+
}
|
|
222
|
+
return this.inner.read(path);
|
|
223
|
+
}
|
|
224
|
+
async write(path, data) {
|
|
225
|
+
return this.inner.write(path, data);
|
|
226
|
+
}
|
|
227
|
+
async remove(path) {
|
|
228
|
+
return this.inner.remove(path);
|
|
229
|
+
}
|
|
230
|
+
async search(path, pattern) {
|
|
231
|
+
return this.inner.search(path, pattern);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// src/registry/resolver.ts
|
|
236
|
+
import { HttpBackend, RpcBackend, JsonRpcTransport } from "@nkmc/agent-fs";
|
|
237
|
+
import { signJwt } from "@nkmc/core";
|
|
238
|
+
|
|
239
|
+
// src/federation/peer-backend.ts
|
|
240
|
+
var PeerBackend = class {
|
|
241
|
+
constructor(client, peer, agentId) {
|
|
242
|
+
this.client = client;
|
|
243
|
+
this.peer = peer;
|
|
244
|
+
this.agentId = agentId;
|
|
245
|
+
}
|
|
246
|
+
async list(path) {
|
|
247
|
+
const result = await this.execOnPeer(`ls ${path}`);
|
|
248
|
+
return result.data ?? [];
|
|
249
|
+
}
|
|
250
|
+
async read(path) {
|
|
251
|
+
const result = await this.execOnPeer(`cat ${path}`);
|
|
252
|
+
return result.data;
|
|
253
|
+
}
|
|
254
|
+
async write(path, data) {
|
|
255
|
+
const result = await this.execOnPeer(
|
|
256
|
+
`write ${path} ${JSON.stringify(data)}`
|
|
257
|
+
);
|
|
258
|
+
return result.data ?? { id: "" };
|
|
259
|
+
}
|
|
260
|
+
async remove(path) {
|
|
261
|
+
await this.execOnPeer(`rm ${path}`);
|
|
262
|
+
}
|
|
263
|
+
async search(path, pattern) {
|
|
264
|
+
const result = await this.execOnPeer(`grep ${pattern} ${path}`);
|
|
265
|
+
return result.data ?? [];
|
|
266
|
+
}
|
|
267
|
+
async execOnPeer(command) {
|
|
268
|
+
const result = await this.client.exec(this.peer, {
|
|
269
|
+
command,
|
|
270
|
+
agentId: this.agentId
|
|
271
|
+
});
|
|
272
|
+
if (!result.ok) {
|
|
273
|
+
if (result.paymentRequired) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Payment required: ${result.paymentRequired.price} ${result.paymentRequired.currency}`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
throw new Error(result.error ?? "Peer execution failed");
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// src/registry/resolver.ts
|
|
285
|
+
function createRegistryResolver(storeOrOptions) {
|
|
286
|
+
const options = "get" in storeOrOptions && "put" in storeOrOptions ? { store: storeOrOptions } : storeOrOptions;
|
|
287
|
+
const { store, vault, gatewayPrivateKey } = options;
|
|
288
|
+
const loaded = /* @__PURE__ */ new Set();
|
|
289
|
+
async function tryPeerFallback(domain, version, addMount, agent) {
|
|
290
|
+
if (!options.peerClient || !options.peerStore) return false;
|
|
291
|
+
const peers = await options.peerStore.listPeers();
|
|
292
|
+
for (const peer of peers) {
|
|
293
|
+
if (peer.advertisedDomains.length > 0 && !peer.advertisedDomains.includes(domain)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const result = await options.peerClient.query(peer, domain);
|
|
297
|
+
if (result.available) {
|
|
298
|
+
const peerBackend = new PeerBackend(options.peerClient, peer, agent?.id ?? "anonymous");
|
|
299
|
+
const mountPath = version ? `/${domain}@${version}` : `/${domain}`;
|
|
300
|
+
addMount({ path: mountPath, backend: peerBackend });
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
async function onMiss(path, addMount, agent) {
|
|
307
|
+
const { domain, version } = extractDomainPath(path);
|
|
308
|
+
if (!domain) return false;
|
|
309
|
+
const cacheKey = version ? `${domain}@${version}` : domain;
|
|
310
|
+
const record = version ? await store.getVersion(domain, version) : await store.get(domain);
|
|
311
|
+
if (!record) {
|
|
312
|
+
return tryPeerFallback(domain, version, addMount, agent);
|
|
313
|
+
}
|
|
314
|
+
const isNkmcJwt = record.authMode === "nkmc-jwt";
|
|
315
|
+
if (!isNkmcJwt && loaded.has(cacheKey)) return false;
|
|
316
|
+
if (record.status === "sunset") return false;
|
|
317
|
+
let auth;
|
|
318
|
+
if (isNkmcJwt && gatewayPrivateKey && agent) {
|
|
319
|
+
const token = await signJwt(gatewayPrivateKey, {
|
|
320
|
+
sub: agent.id,
|
|
321
|
+
roles: agent.roles,
|
|
322
|
+
svc: domain
|
|
323
|
+
}, { expiresIn: "5m" });
|
|
324
|
+
auth = { type: "bearer", token };
|
|
325
|
+
} else if (vault) {
|
|
326
|
+
const cred = await vault.get(domain, agent?.id);
|
|
327
|
+
if (cred) {
|
|
328
|
+
auth = cred.auth;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (!auth && !isNkmcJwt) {
|
|
332
|
+
const peerMounted = await tryPeerFallback(domain, version, addMount, agent);
|
|
333
|
+
if (peerMounted) return true;
|
|
334
|
+
}
|
|
335
|
+
let backend;
|
|
336
|
+
if (record.source?.type === "jsonrpc" && record.source.rpc) {
|
|
337
|
+
const { resources } = skillToRpcConfig(record.source.rpc);
|
|
338
|
+
const headers = {};
|
|
339
|
+
if (auth) {
|
|
340
|
+
if (auth.type === "bearer") {
|
|
341
|
+
headers["Authorization"] = `${auth.prefix ?? "Bearer"} ${auth.token}`;
|
|
342
|
+
} else if (auth.type === "api-key") {
|
|
343
|
+
headers[auth.header] = auth.key;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const transport = new JsonRpcTransport({ url: record.source.rpc.rpcUrl, headers });
|
|
347
|
+
backend = new RpcBackend({ transport, resources });
|
|
348
|
+
} else {
|
|
349
|
+
const config = skillToHttpConfig(record);
|
|
350
|
+
config.auth = auth;
|
|
351
|
+
backend = new HttpBackend(config);
|
|
352
|
+
}
|
|
353
|
+
let finalBackend = backend;
|
|
354
|
+
if (options.wrapVirtualFiles !== false) {
|
|
355
|
+
finalBackend = new VirtualFileBackend({ inner: backend, domain, store: options.store });
|
|
356
|
+
}
|
|
357
|
+
const mountPath = version ? `/${domain}@${version}` : `/${domain}`;
|
|
358
|
+
addMount({ path: mountPath, backend: finalBackend });
|
|
359
|
+
if (!isNkmcJwt) {
|
|
360
|
+
loaded.add(cacheKey);
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
async function listDomains() {
|
|
365
|
+
const summaries = await store.list();
|
|
366
|
+
return summaries.map((s) => s.domain);
|
|
367
|
+
}
|
|
368
|
+
async function searchDomains(query) {
|
|
369
|
+
return store.search(query);
|
|
370
|
+
}
|
|
371
|
+
async function searchEndpoints(domain, query) {
|
|
372
|
+
const record = await store.get(domain);
|
|
373
|
+
if (!record) return [];
|
|
374
|
+
const q = query.toLowerCase();
|
|
375
|
+
return record.endpoints.filter(
|
|
376
|
+
(e) => e.description.toLowerCase().includes(q) || e.method.toLowerCase().includes(q) || e.path.toLowerCase().includes(q)
|
|
377
|
+
).map((e) => ({ method: e.method, path: e.path, description: e.description }));
|
|
378
|
+
}
|
|
379
|
+
return { onMiss, listDomains, searchDomains, searchEndpoints };
|
|
380
|
+
}
|
|
381
|
+
function extractDomainPath(path) {
|
|
382
|
+
const segments = path.split("/").filter(Boolean);
|
|
383
|
+
if (segments.length === 0) return { domain: null, version: null };
|
|
384
|
+
const first = segments[0];
|
|
385
|
+
const atIndex = first.indexOf("@");
|
|
386
|
+
if (atIndex > 0) {
|
|
387
|
+
return {
|
|
388
|
+
domain: first.slice(0, atIndex),
|
|
389
|
+
version: first.slice(atIndex + 1)
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return { domain: first, version: null };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/registry/context7.ts
|
|
396
|
+
var Context7Client = class {
|
|
397
|
+
apiKey;
|
|
398
|
+
baseUrl;
|
|
399
|
+
fetchFn;
|
|
400
|
+
constructor(options) {
|
|
401
|
+
this.apiKey = options?.apiKey;
|
|
402
|
+
this.baseUrl = options?.baseUrl ?? "https://context7.com/api/v2";
|
|
403
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
404
|
+
}
|
|
405
|
+
/** Search for a library by name. Returns matching library entries. */
|
|
406
|
+
async searchLibraries(libraryName, query) {
|
|
407
|
+
const params = new URLSearchParams({ libraryName });
|
|
408
|
+
if (query) params.set("query", query);
|
|
409
|
+
const resp = await this.fetchFn(`${this.baseUrl}/libs/search?${params}`, {
|
|
410
|
+
headers: this.headers()
|
|
411
|
+
});
|
|
412
|
+
if (!resp.ok) throw new Error(`Context7 search failed: ${resp.status}`);
|
|
413
|
+
return resp.json();
|
|
414
|
+
}
|
|
415
|
+
/** Query documentation for a specific library. Returns documentation text. */
|
|
416
|
+
async queryDocs(libraryId, query) {
|
|
417
|
+
const params = new URLSearchParams({ libraryId, query, type: "txt" });
|
|
418
|
+
const resp = await this.fetchFn(`${this.baseUrl}/context?${params}`, {
|
|
419
|
+
headers: this.headers()
|
|
420
|
+
});
|
|
421
|
+
if (!resp.ok) throw new Error(`Context7 query failed: ${resp.status}`);
|
|
422
|
+
return resp.text();
|
|
423
|
+
}
|
|
424
|
+
headers() {
|
|
425
|
+
const h = {};
|
|
426
|
+
if (this.apiKey) h["Authorization"] = `Bearer ${this.apiKey}`;
|
|
427
|
+
return h;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// src/registry/context7-backend.ts
|
|
432
|
+
var Context7Backend = class {
|
|
433
|
+
client;
|
|
434
|
+
constructor(options) {
|
|
435
|
+
this.client = new Context7Client(options);
|
|
436
|
+
}
|
|
437
|
+
async list(path) {
|
|
438
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
439
|
+
if (!cleaned) {
|
|
440
|
+
return [
|
|
441
|
+
'grep "<\u5173\u952E\u8BCD>" /context7/ \u2014 \u641C\u7D22\u5E93',
|
|
442
|
+
'grep "<\u95EE\u9898>" /context7/{id} \u2014 \u67E5\u8BE2\u6587\u6863',
|
|
443
|
+
"cat /context7/{owner}/{repo} \u2014 \u5E93\u6982\u89C8"
|
|
444
|
+
];
|
|
445
|
+
}
|
|
446
|
+
return ['grep "<\u95EE\u9898>" /context7/' + cleaned + " \u2014 \u67E5\u8BE2\u6B64\u5E93\u6587\u6863"];
|
|
447
|
+
}
|
|
448
|
+
async read(path) {
|
|
449
|
+
const libraryId = parseLibraryId(path);
|
|
450
|
+
if (!libraryId) {
|
|
451
|
+
return { usage: 'grep "<\u5173\u952E\u8BCD>" /context7/ \u2014 \u641C\u7D22\u5E93' };
|
|
452
|
+
}
|
|
453
|
+
const name = libraryId.split("/").pop() ?? libraryId;
|
|
454
|
+
const docs = await this.client.queryDocs(libraryId, `${name} overview getting started`);
|
|
455
|
+
return { libraryId, docs };
|
|
456
|
+
}
|
|
457
|
+
async write(_path, _data) {
|
|
458
|
+
throw new Error("context7 is read-only");
|
|
459
|
+
}
|
|
460
|
+
async remove(_path) {
|
|
461
|
+
throw new Error("context7 is read-only");
|
|
462
|
+
}
|
|
463
|
+
async search(path, pattern) {
|
|
464
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
465
|
+
if (!cleaned) {
|
|
466
|
+
const results = await this.client.searchLibraries(pattern);
|
|
467
|
+
return results.map(formatSearchResult);
|
|
468
|
+
}
|
|
469
|
+
const libraryId = parseLibraryId(path);
|
|
470
|
+
if (!libraryId) return [];
|
|
471
|
+
const docs = await this.client.queryDocs(libraryId, pattern);
|
|
472
|
+
if (!docs) return [];
|
|
473
|
+
return [{ libraryId, query: pattern, docs }];
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
function parseLibraryId(path) {
|
|
477
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
478
|
+
if (!cleaned) return null;
|
|
479
|
+
const parts = cleaned.split("/");
|
|
480
|
+
if (parts.length < 2) return null;
|
|
481
|
+
return "/" + parts.slice(0, 2).join("/");
|
|
482
|
+
}
|
|
483
|
+
function formatSearchResult(r) {
|
|
484
|
+
return {
|
|
485
|
+
id: r.id,
|
|
486
|
+
name: r.name,
|
|
487
|
+
description: r.description ?? "",
|
|
488
|
+
snippets: r.totalSnippets ?? 0
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/registry/skill-parser.ts
|
|
493
|
+
import { parse as parseYaml } from "yaml";
|
|
494
|
+
function parseSkillMd(domain, raw, options) {
|
|
495
|
+
const { frontmatter, body } = extractFrontmatter(raw);
|
|
496
|
+
const parsed = parseYaml(frontmatter) ?? {};
|
|
497
|
+
const description = extractDescription(body);
|
|
498
|
+
const endpoints = extractEndpoints(body);
|
|
499
|
+
const now = Date.now();
|
|
500
|
+
return {
|
|
501
|
+
domain,
|
|
502
|
+
name: parsed.name ?? domain,
|
|
503
|
+
description,
|
|
504
|
+
version: parsed.version ?? "0.0",
|
|
505
|
+
roles: parsed.roles ?? ["agent"],
|
|
506
|
+
skillMd: raw,
|
|
507
|
+
endpoints,
|
|
508
|
+
isFirstParty: options?.isFirstParty ?? false,
|
|
509
|
+
createdAt: now,
|
|
510
|
+
updatedAt: now,
|
|
511
|
+
status: "active",
|
|
512
|
+
isDefault: true
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function parsePricingAnnotation(text) {
|
|
516
|
+
const match = text.match(
|
|
517
|
+
/(\d+(?:\.\d+)?)\s+(\w+)\s*\/\s*(call|byte|minute|次)/i
|
|
518
|
+
);
|
|
519
|
+
if (!match) return void 0;
|
|
520
|
+
return {
|
|
521
|
+
cost: parseFloat(match[1]),
|
|
522
|
+
currency: match[2].toUpperCase(),
|
|
523
|
+
per: match[3] === "\u6B21" ? "call" : match[3].toLowerCase()
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function extractFrontmatter(raw) {
|
|
527
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
528
|
+
if (!match) return { frontmatter: "", body: raw };
|
|
529
|
+
return { frontmatter: match[1], body: match[2] };
|
|
530
|
+
}
|
|
531
|
+
function extractDescription(body) {
|
|
532
|
+
const lines = body.split("\n");
|
|
533
|
+
let foundTitle = false;
|
|
534
|
+
const descLines = [];
|
|
535
|
+
for (const line of lines) {
|
|
536
|
+
if (!foundTitle) {
|
|
537
|
+
if (line.startsWith("# ")) foundTitle = true;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (line.startsWith("## ")) break;
|
|
541
|
+
const trimmed = line.trim();
|
|
542
|
+
if (trimmed === "" && descLines.length > 0) break;
|
|
543
|
+
if (trimmed !== "") descLines.push(trimmed);
|
|
544
|
+
}
|
|
545
|
+
return descLines.join(" ");
|
|
546
|
+
}
|
|
547
|
+
function extractEndpoints(body) {
|
|
548
|
+
const endpoints = [];
|
|
549
|
+
const lines = body.split("\n");
|
|
550
|
+
let inApiSection = false;
|
|
551
|
+
let currentHeading = null;
|
|
552
|
+
for (const line of lines) {
|
|
553
|
+
if (line.startsWith("## API")) {
|
|
554
|
+
inApiSection = true;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (inApiSection && line.startsWith("## ") && !line.startsWith("## API")) {
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
if (!inApiSection) continue;
|
|
561
|
+
if (line.startsWith("### ")) {
|
|
562
|
+
currentHeading = line.slice(4).trim();
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const endpointMatch = line.match(/^`(GET|POST|PUT|PATCH|DELETE)\s+(\S+)`/);
|
|
566
|
+
if (endpointMatch && currentHeading) {
|
|
567
|
+
const afterBacktick = line.slice(line.indexOf("`", 1) + 1).trim();
|
|
568
|
+
const pricing = afterBacktick.startsWith("\u2014") ? parsePricingAnnotation(afterBacktick.slice(1).trim()) : void 0;
|
|
569
|
+
endpoints.push({
|
|
570
|
+
method: endpointMatch[1],
|
|
571
|
+
path: endpointMatch[2],
|
|
572
|
+
description: currentHeading,
|
|
573
|
+
...pricing ? { pricing } : {}
|
|
574
|
+
});
|
|
575
|
+
currentHeading = null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return endpoints;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// src/http/lib/dns.ts
|
|
582
|
+
async function queryDnsTxt(domain) {
|
|
583
|
+
const url = new URL("https://cloudflare-dns.com/dns-query");
|
|
584
|
+
url.searchParams.set("name", domain);
|
|
585
|
+
url.searchParams.set("type", "TXT");
|
|
586
|
+
const res = await fetch(url.toString(), {
|
|
587
|
+
headers: { Accept: "application/dns-json" }
|
|
588
|
+
});
|
|
589
|
+
if (!res.ok) {
|
|
590
|
+
throw new Error(`DNS query failed: ${res.status}`);
|
|
591
|
+
}
|
|
592
|
+
const data = await res.json();
|
|
593
|
+
return (data.Answer ?? []).filter((a) => a.type === 16).map((a) => a.data.replace(/^"|"$/g, ""));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/http/routes/credentials.ts
|
|
597
|
+
import { Hono } from "hono";
|
|
598
|
+
function credentialRoutes(options) {
|
|
599
|
+
const { vault } = options;
|
|
600
|
+
const app = new Hono();
|
|
601
|
+
app.put("/:domain", async (c) => {
|
|
602
|
+
const domain = c.req.param("domain");
|
|
603
|
+
const body = await c.req.json();
|
|
604
|
+
if (!body.auth?.type) {
|
|
605
|
+
return c.json({ error: "Missing auth.type" }, 400);
|
|
606
|
+
}
|
|
607
|
+
await vault.putPool(domain, body.auth);
|
|
608
|
+
return c.json({ ok: true, domain });
|
|
609
|
+
});
|
|
610
|
+
app.get("/", async (c) => {
|
|
611
|
+
const domains = await vault.listDomains();
|
|
612
|
+
return c.json({ domains });
|
|
613
|
+
});
|
|
614
|
+
app.delete("/:domain", async (c) => {
|
|
615
|
+
const domain = c.req.param("domain");
|
|
616
|
+
await vault.delete(domain);
|
|
617
|
+
return c.json({ ok: true, domain });
|
|
618
|
+
});
|
|
619
|
+
return app;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/http/routes/tunnels.ts
|
|
623
|
+
import { Hono as Hono2 } from "hono";
|
|
624
|
+
import { nanoid } from "nanoid";
|
|
625
|
+
function tunnelRoutes(options) {
|
|
626
|
+
const { tunnelStore, tunnelProvider, tunnelDomain } = options;
|
|
627
|
+
const app = new Hono2();
|
|
628
|
+
app.post("/create", async (c) => {
|
|
629
|
+
const agent = c.get("agent");
|
|
630
|
+
const body = await c.req.json().catch(() => ({}));
|
|
631
|
+
const existing = await tunnelStore.getByAgent(agent.id);
|
|
632
|
+
if (existing && existing.status === "active") {
|
|
633
|
+
return c.json({
|
|
634
|
+
tunnelId: existing.id,
|
|
635
|
+
publicUrl: existing.publicUrl,
|
|
636
|
+
message: "Tunnel already exists"
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
const id = nanoid(12);
|
|
640
|
+
const hostname = `${id}.${tunnelDomain}`;
|
|
641
|
+
const publicUrl = `https://${hostname}`;
|
|
642
|
+
const { tunnelId, tunnelToken } = await tunnelProvider.create(
|
|
643
|
+
`nkmc-${agent.id}-${id}`,
|
|
644
|
+
hostname
|
|
645
|
+
);
|
|
646
|
+
const now = Date.now();
|
|
647
|
+
await tunnelStore.put({
|
|
648
|
+
id,
|
|
649
|
+
agentId: agent.id,
|
|
650
|
+
tunnelId,
|
|
651
|
+
publicUrl,
|
|
652
|
+
status: "active",
|
|
653
|
+
createdAt: now,
|
|
654
|
+
advertisedDomains: body.advertisedDomains ?? [],
|
|
655
|
+
gatewayName: body.gatewayName,
|
|
656
|
+
lastSeen: now
|
|
657
|
+
});
|
|
658
|
+
return c.json({ tunnelId: id, tunnelToken, publicUrl }, 201);
|
|
659
|
+
});
|
|
660
|
+
app.delete("/:id", async (c) => {
|
|
661
|
+
const id = c.req.param("id");
|
|
662
|
+
const agent = c.get("agent");
|
|
663
|
+
const record = await tunnelStore.get(id);
|
|
664
|
+
if (!record) return c.json({ error: "Tunnel not found" }, 404);
|
|
665
|
+
if (record.agentId !== agent.id)
|
|
666
|
+
return c.json({ error: "Not your tunnel" }, 403);
|
|
667
|
+
await tunnelProvider.delete(record.tunnelId);
|
|
668
|
+
await tunnelStore.delete(id);
|
|
669
|
+
return c.json({ ok: true });
|
|
670
|
+
});
|
|
671
|
+
app.get("/", async (c) => {
|
|
672
|
+
const agent = c.get("agent");
|
|
673
|
+
const all = await tunnelStore.list();
|
|
674
|
+
const mine = all.filter((t) => t.agentId === agent.id);
|
|
675
|
+
return c.json({ tunnels: mine });
|
|
676
|
+
});
|
|
677
|
+
app.get("/discover", async (c) => {
|
|
678
|
+
const domain = c.req.query("domain");
|
|
679
|
+
const all = await tunnelStore.list();
|
|
680
|
+
let results = all.filter((t) => t.status === "active");
|
|
681
|
+
if (domain) {
|
|
682
|
+
results = results.filter((t) => t.advertisedDomains.includes(domain));
|
|
683
|
+
}
|
|
684
|
+
return c.json({
|
|
685
|
+
gateways: results.map((t) => ({
|
|
686
|
+
id: t.id,
|
|
687
|
+
name: t.gatewayName ?? `gateway-${t.id}`,
|
|
688
|
+
publicUrl: t.publicUrl,
|
|
689
|
+
advertisedDomains: t.advertisedDomains
|
|
690
|
+
}))
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
app.post("/heartbeat", async (c) => {
|
|
694
|
+
const agent = c.get("agent");
|
|
695
|
+
const body = await c.req.json();
|
|
696
|
+
const record = await tunnelStore.getByAgent(agent.id);
|
|
697
|
+
if (!record) return c.json({ error: "No active tunnel" }, 404);
|
|
698
|
+
record.advertisedDomains = body.advertisedDomains ?? record.advertisedDomains;
|
|
699
|
+
record.lastSeen = Date.now();
|
|
700
|
+
await tunnelStore.put(record);
|
|
701
|
+
return c.json({ ok: true });
|
|
702
|
+
});
|
|
703
|
+
return app;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// src/registry/openapi-compiler.ts
|
|
707
|
+
import YAML from "yaml";
|
|
708
|
+
function extractBasePath(spec) {
|
|
709
|
+
const servers = spec.servers;
|
|
710
|
+
if (!Array.isArray(servers) || servers.length === 0) return "";
|
|
711
|
+
const serverUrl = servers[0]?.url;
|
|
712
|
+
if (!serverUrl || typeof serverUrl !== "string") return "";
|
|
713
|
+
try {
|
|
714
|
+
if (serverUrl.startsWith("/")) {
|
|
715
|
+
return serverUrl.replace(/\/+$/, "");
|
|
716
|
+
}
|
|
717
|
+
const parsed = new URL(serverUrl);
|
|
718
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
719
|
+
return pathname || "";
|
|
720
|
+
} catch {
|
|
721
|
+
return "";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function resolveRef(spec, ref) {
|
|
725
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
726
|
+
const parts = ref.slice(2).split("/");
|
|
727
|
+
let current = spec;
|
|
728
|
+
for (const part of parts) {
|
|
729
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
730
|
+
current = current[part];
|
|
731
|
+
}
|
|
732
|
+
return current;
|
|
733
|
+
}
|
|
734
|
+
function resolveSchema(spec, schema) {
|
|
735
|
+
if (!schema) return void 0;
|
|
736
|
+
if (schema.$ref) return resolveRef(spec, schema.$ref);
|
|
737
|
+
return schema;
|
|
738
|
+
}
|
|
739
|
+
function extractProperties(spec, schema) {
|
|
740
|
+
const resolved = resolveSchema(spec, schema);
|
|
741
|
+
if (!resolved || resolved.type !== "object" || !resolved.properties) return [];
|
|
742
|
+
const requiredSet = new Set(resolved.required ?? []);
|
|
743
|
+
const props = [];
|
|
744
|
+
for (const [name, prop] of Object.entries(resolved.properties)) {
|
|
745
|
+
const p = resolveSchema(spec, prop) ?? prop;
|
|
746
|
+
props.push({
|
|
747
|
+
name,
|
|
748
|
+
type: p.type ?? (p.enum ? "enum" : "unknown"),
|
|
749
|
+
required: requiredSet.has(name),
|
|
750
|
+
...p.description ? { description: p.description } : {}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
return props;
|
|
754
|
+
}
|
|
755
|
+
function extractParams(spec, operation) {
|
|
756
|
+
const params = operation.parameters;
|
|
757
|
+
if (!Array.isArray(params) || params.length === 0) return void 0;
|
|
758
|
+
const result = [];
|
|
759
|
+
for (const raw of params) {
|
|
760
|
+
const p = resolveSchema(spec, raw) ?? raw;
|
|
761
|
+
if (!p.name || !p.in) continue;
|
|
762
|
+
if (!["path", "query", "header"].includes(p.in)) continue;
|
|
763
|
+
const schema = resolveSchema(spec, p.schema) ?? p.schema;
|
|
764
|
+
result.push({
|
|
765
|
+
name: p.name,
|
|
766
|
+
in: p.in,
|
|
767
|
+
required: !!p.required,
|
|
768
|
+
type: schema?.type ?? "string",
|
|
769
|
+
...p.description ? { description: p.description } : {}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return result.length > 0 ? result : void 0;
|
|
773
|
+
}
|
|
774
|
+
function extractRequestBody(spec, operation) {
|
|
775
|
+
const body = resolveSchema(spec, operation.requestBody);
|
|
776
|
+
if (!body?.content) return void 0;
|
|
777
|
+
const jsonContent = body.content["application/json"];
|
|
778
|
+
if (!jsonContent?.schema) return void 0;
|
|
779
|
+
const properties = extractProperties(spec, jsonContent.schema);
|
|
780
|
+
if (properties.length === 0) return void 0;
|
|
781
|
+
return {
|
|
782
|
+
contentType: "application/json",
|
|
783
|
+
required: !!body.required,
|
|
784
|
+
properties
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function extractResponses(spec, operation) {
|
|
788
|
+
const responses = operation.responses;
|
|
789
|
+
if (!responses || typeof responses !== "object") return void 0;
|
|
790
|
+
const result = [];
|
|
791
|
+
for (const [code, raw] of Object.entries(responses)) {
|
|
792
|
+
const status = parseInt(code, 10);
|
|
793
|
+
if (isNaN(status) || status < 200 || status >= 300) continue;
|
|
794
|
+
const resp = resolveSchema(spec, raw) ?? raw;
|
|
795
|
+
const jsonContent = resp?.content?.["application/json"];
|
|
796
|
+
const properties = jsonContent?.schema ? extractProperties(spec, jsonContent.schema) : void 0;
|
|
797
|
+
result.push({
|
|
798
|
+
status,
|
|
799
|
+
description: resp?.description ?? "",
|
|
800
|
+
...properties && properties.length > 0 ? { properties } : {}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return result.length > 0 ? result : void 0;
|
|
804
|
+
}
|
|
805
|
+
function compileOpenApiSpec(spec, options) {
|
|
806
|
+
const info = spec.info ?? {};
|
|
807
|
+
const name = info.title ?? options.domain;
|
|
808
|
+
const description = info.description ?? "";
|
|
809
|
+
const version = options.version ?? info.version ?? "1.0";
|
|
810
|
+
const basePath = extractBasePath(spec);
|
|
811
|
+
const endpoints = [];
|
|
812
|
+
const resources = [];
|
|
813
|
+
const resourcePaths = /* @__PURE__ */ new Map();
|
|
814
|
+
const paths = spec.paths ?? {};
|
|
815
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
816
|
+
if (typeof methods !== "object" || methods === null) continue;
|
|
817
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
818
|
+
if (!["get", "post", "put", "patch", "delete"].includes(method)) continue;
|
|
819
|
+
const operation = op;
|
|
820
|
+
const parameters = extractParams(spec, operation);
|
|
821
|
+
const requestBody = extractRequestBody(spec, operation);
|
|
822
|
+
const responses = extractResponses(spec, operation);
|
|
823
|
+
endpoints.push({
|
|
824
|
+
method: method.toUpperCase(),
|
|
825
|
+
path,
|
|
826
|
+
description: operation.summary ?? operation.operationId ?? `${method.toUpperCase()} ${path}`,
|
|
827
|
+
...parameters ? { parameters } : {},
|
|
828
|
+
...requestBody ? { requestBody } : {},
|
|
829
|
+
...responses ? { responses } : {}
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
const segments = path.split("/").filter(Boolean);
|
|
833
|
+
if (segments.length >= 1) {
|
|
834
|
+
const resourceName = segments[0];
|
|
835
|
+
if (!resourcePaths.has(resourceName) && !resourceName.startsWith("{")) {
|
|
836
|
+
resourcePaths.set(resourceName, {
|
|
837
|
+
name: resourceName,
|
|
838
|
+
apiPath: `/${resourceName}`
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
for (const r of resourcePaths.values()) {
|
|
844
|
+
resources.push(r);
|
|
845
|
+
}
|
|
846
|
+
const skillMd = generateSkillMd(name, version, description, endpoints, resources);
|
|
847
|
+
const now = Date.now();
|
|
848
|
+
const record = {
|
|
849
|
+
domain: options.domain,
|
|
850
|
+
name,
|
|
851
|
+
description,
|
|
852
|
+
version,
|
|
853
|
+
roles: ["agent"],
|
|
854
|
+
skillMd,
|
|
855
|
+
endpoints,
|
|
856
|
+
isFirstParty: options.isFirstParty ?? false,
|
|
857
|
+
createdAt: now,
|
|
858
|
+
updatedAt: now,
|
|
859
|
+
status: "active",
|
|
860
|
+
isDefault: true,
|
|
861
|
+
source: { type: "openapi", ...basePath ? { basePath } : {} }
|
|
862
|
+
};
|
|
863
|
+
return { record, resources, skillMd };
|
|
864
|
+
}
|
|
865
|
+
async function fetchAndCompile(specUrl, options, fetchFn = globalThis.fetch.bind(globalThis)) {
|
|
866
|
+
const resp = await fetchFn(specUrl);
|
|
867
|
+
if (!resp.ok) throw new Error(`Failed to fetch spec: ${resp.status} ${resp.statusText}`);
|
|
868
|
+
const text = await resp.text();
|
|
869
|
+
const spec = parseSpec(specUrl, resp.headers.get("content-type") ?? "", text);
|
|
870
|
+
const result = compileOpenApiSpec(spec, options);
|
|
871
|
+
const basePath = result.record.source?.basePath;
|
|
872
|
+
result.record.source = { type: "openapi", url: specUrl, ...basePath ? { basePath } : {} };
|
|
873
|
+
return result;
|
|
874
|
+
}
|
|
875
|
+
function parseSpec(url, contentType, text) {
|
|
876
|
+
const isJson = contentType.includes("json") || url.endsWith(".json") || text.trimStart().startsWith("{");
|
|
877
|
+
if (isJson) return JSON.parse(text);
|
|
878
|
+
return YAML.parse(text);
|
|
879
|
+
}
|
|
880
|
+
function propsTable(props) {
|
|
881
|
+
let t = "| name | type | required |\n|------|------|----------|\n";
|
|
882
|
+
for (const p of props) {
|
|
883
|
+
t += `| ${p.name} | ${p.type} | ${p.required ? "*" : ""} |
|
|
884
|
+
`;
|
|
885
|
+
}
|
|
886
|
+
return t;
|
|
887
|
+
}
|
|
888
|
+
function generateSkillMd(name, version, description, endpoints, resources) {
|
|
889
|
+
let md = `---
|
|
890
|
+
name: "${name}"
|
|
891
|
+
gateway: nkmc
|
|
892
|
+
version: "${version}"
|
|
893
|
+
roles: [agent]
|
|
894
|
+
---
|
|
895
|
+
|
|
896
|
+
`;
|
|
897
|
+
md += `# ${name}
|
|
898
|
+
|
|
899
|
+
${description}
|
|
900
|
+
|
|
901
|
+
`;
|
|
902
|
+
if (resources.length > 0) {
|
|
903
|
+
md += `## Schema
|
|
904
|
+
|
|
905
|
+
`;
|
|
906
|
+
for (const r of resources) {
|
|
907
|
+
md += `### ${r.name} (public)
|
|
908
|
+
|
|
909
|
+
`;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (endpoints.length > 0) {
|
|
913
|
+
md += `## API
|
|
914
|
+
|
|
915
|
+
`;
|
|
916
|
+
for (const ep of endpoints) {
|
|
917
|
+
md += `### ${ep.description}
|
|
918
|
+
|
|
919
|
+
`;
|
|
920
|
+
md += `\`${ep.method} ${ep.path}\`
|
|
921
|
+
|
|
922
|
+
`;
|
|
923
|
+
if (ep.parameters && ep.parameters.length > 0) {
|
|
924
|
+
md += "**Parameters:**\n\n";
|
|
925
|
+
md += "| name | in | type | required |\n|------|-----|------|----------|\n";
|
|
926
|
+
for (const p of ep.parameters) {
|
|
927
|
+
md += `| ${p.name} | ${p.in} | ${p.type} | ${p.required ? "*" : ""} |
|
|
928
|
+
`;
|
|
929
|
+
}
|
|
930
|
+
md += "\n";
|
|
931
|
+
}
|
|
932
|
+
if (ep.requestBody) {
|
|
933
|
+
const req = ep.requestBody;
|
|
934
|
+
md += `**Body** (${req.contentType}${req.required ? ", required" : ""}):
|
|
935
|
+
|
|
936
|
+
`;
|
|
937
|
+
md += propsTable(req.properties);
|
|
938
|
+
md += "\n";
|
|
939
|
+
}
|
|
940
|
+
if (ep.responses && ep.responses.length > 0) {
|
|
941
|
+
for (const r of ep.responses) {
|
|
942
|
+
md += `**Response ${r.status}**${r.description ? `: ${r.description}` : ""}
|
|
943
|
+
|
|
944
|
+
`;
|
|
945
|
+
if (r.properties && r.properties.length > 0) {
|
|
946
|
+
md += propsTable(r.properties);
|
|
947
|
+
md += "\n";
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return md;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
export {
|
|
957
|
+
skillToHttpConfig,
|
|
958
|
+
VirtualFileBackend,
|
|
959
|
+
createRegistryResolver,
|
|
960
|
+
extractDomainPath,
|
|
961
|
+
Context7Client,
|
|
962
|
+
Context7Backend,
|
|
963
|
+
parseSkillMd,
|
|
964
|
+
parsePricingAnnotation,
|
|
965
|
+
fetchAndCompile,
|
|
966
|
+
queryDnsTxt,
|
|
967
|
+
credentialRoutes,
|
|
968
|
+
tunnelRoutes
|
|
969
|
+
};
|