@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/index.cjs
ADDED
|
@@ -0,0 +1,2436 @@
|
|
|
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/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
CloudflareTunnelProvider: () => CloudflareTunnelProvider,
|
|
34
|
+
Context7Backend: () => Context7Backend,
|
|
35
|
+
Context7Client: () => Context7Client,
|
|
36
|
+
D1CredentialVault: () => D1CredentialVault,
|
|
37
|
+
D1MeterStore: () => D1MeterStore,
|
|
38
|
+
D1PeerStore: () => D1PeerStore,
|
|
39
|
+
D1RegistryStore: () => D1RegistryStore,
|
|
40
|
+
MemoryCredentialVault: () => MemoryCredentialVault,
|
|
41
|
+
MemoryMeterStore: () => MemoryMeterStore,
|
|
42
|
+
MemoryRegistryStore: () => MemoryRegistryStore,
|
|
43
|
+
MemoryTunnelStore: () => MemoryTunnelStore,
|
|
44
|
+
OnboardPipeline: () => OnboardPipeline,
|
|
45
|
+
VERSION: () => VERSION,
|
|
46
|
+
VirtualFileBackend: () => VirtualFileBackend,
|
|
47
|
+
checkAccess: () => checkAccess,
|
|
48
|
+
createRegistryResolver: () => createRegistryResolver,
|
|
49
|
+
createSqliteD1: () => createSqliteD1,
|
|
50
|
+
credentialRoutes: () => credentialRoutes,
|
|
51
|
+
discoverFromApisGuru: () => discoverFromApisGuru,
|
|
52
|
+
extractDomainPath: () => extractDomainPath,
|
|
53
|
+
lookupPricing: () => lookupPricing,
|
|
54
|
+
meter: () => meter,
|
|
55
|
+
parsePricingAnnotation: () => parsePricingAnnotation,
|
|
56
|
+
parseSkillMd: () => parseSkillMd,
|
|
57
|
+
queryDnsTxt: () => queryDnsTxt,
|
|
58
|
+
skillToHttpConfig: () => skillToHttpConfig,
|
|
59
|
+
tunnelRoutes: () => tunnelRoutes
|
|
60
|
+
});
|
|
61
|
+
module.exports = __toCommonJS(index_exports);
|
|
62
|
+
|
|
63
|
+
// src/registry/memory-store.ts
|
|
64
|
+
var MemoryRegistryStore = class {
|
|
65
|
+
// key = "domain@version"
|
|
66
|
+
records = /* @__PURE__ */ new Map();
|
|
67
|
+
key(domain, version) {
|
|
68
|
+
return `${domain}@${version}`;
|
|
69
|
+
}
|
|
70
|
+
async get(domain) {
|
|
71
|
+
for (const record of this.records.values()) {
|
|
72
|
+
if (record.domain === domain && record.isDefault) return record;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
async getVersion(domain, version) {
|
|
77
|
+
return this.records.get(this.key(domain, version)) ?? null;
|
|
78
|
+
}
|
|
79
|
+
async listVersions(domain) {
|
|
80
|
+
const versions = [];
|
|
81
|
+
for (const record of this.records.values()) {
|
|
82
|
+
if (record.domain === domain) {
|
|
83
|
+
versions.push({
|
|
84
|
+
version: record.version,
|
|
85
|
+
status: record.status,
|
|
86
|
+
isDefault: record.isDefault,
|
|
87
|
+
createdAt: record.createdAt,
|
|
88
|
+
updatedAt: record.updatedAt
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return versions.sort((a, b) => b.createdAt - a.createdAt);
|
|
93
|
+
}
|
|
94
|
+
async put(domain, record) {
|
|
95
|
+
this.records.set(this.key(domain, record.version), record);
|
|
96
|
+
}
|
|
97
|
+
async delete(domain) {
|
|
98
|
+
const keysToDelete = [];
|
|
99
|
+
for (const [key, record] of this.records.entries()) {
|
|
100
|
+
if (record.domain === domain) keysToDelete.push(key);
|
|
101
|
+
}
|
|
102
|
+
for (const key of keysToDelete) {
|
|
103
|
+
this.records.delete(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async list() {
|
|
107
|
+
const results = [];
|
|
108
|
+
for (const record of this.records.values()) {
|
|
109
|
+
if (record.isDefault) results.push(toSummary(record));
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
async search(query) {
|
|
114
|
+
const q = query.toLowerCase();
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const record of this.records.values()) {
|
|
117
|
+
if (!record.isDefault) continue;
|
|
118
|
+
const nameMatch = record.name.toLowerCase().includes(q) || record.description.toLowerCase().includes(q);
|
|
119
|
+
const matched = record.endpoints.filter(
|
|
120
|
+
(e) => e.description.toLowerCase().includes(q) || e.method.toLowerCase().includes(q) || e.path.toLowerCase().includes(q)
|
|
121
|
+
);
|
|
122
|
+
if (nameMatch || matched.length > 0) {
|
|
123
|
+
results.push({
|
|
124
|
+
...toSummary(record),
|
|
125
|
+
matchedEndpoints: matched.map((e) => ({
|
|
126
|
+
method: e.method,
|
|
127
|
+
path: e.path,
|
|
128
|
+
description: e.description
|
|
129
|
+
}))
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
function toSummary(record) {
|
|
137
|
+
return {
|
|
138
|
+
domain: record.domain,
|
|
139
|
+
name: record.name,
|
|
140
|
+
description: record.description,
|
|
141
|
+
isFirstParty: record.isFirstParty
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/registry/d1-store.ts
|
|
146
|
+
var CREATE_SERVICES = `
|
|
147
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
148
|
+
domain TEXT NOT NULL,
|
|
149
|
+
version TEXT NOT NULL,
|
|
150
|
+
name TEXT NOT NULL,
|
|
151
|
+
description TEXT,
|
|
152
|
+
roles TEXT,
|
|
153
|
+
skill_md TEXT NOT NULL,
|
|
154
|
+
endpoints TEXT,
|
|
155
|
+
is_first_party INTEGER DEFAULT 0,
|
|
156
|
+
status TEXT DEFAULT 'active',
|
|
157
|
+
is_default INTEGER DEFAULT 1,
|
|
158
|
+
source TEXT,
|
|
159
|
+
sunset_date INTEGER,
|
|
160
|
+
auth_mode TEXT,
|
|
161
|
+
created_at INTEGER NOT NULL,
|
|
162
|
+
updated_at INTEGER NOT NULL,
|
|
163
|
+
PRIMARY KEY (domain, version)
|
|
164
|
+
)`;
|
|
165
|
+
var CREATE_SERVICES_DEFAULT_INDEX = `
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_services_default ON services(domain, is_default)`;
|
|
167
|
+
var CREATE_DOMAIN_CHALLENGES = `
|
|
168
|
+
CREATE TABLE IF NOT EXISTS domain_challenges (
|
|
169
|
+
domain TEXT PRIMARY KEY,
|
|
170
|
+
challenge_code TEXT NOT NULL,
|
|
171
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
172
|
+
created_at INTEGER NOT NULL,
|
|
173
|
+
verified_at INTEGER,
|
|
174
|
+
expires_at INTEGER NOT NULL
|
|
175
|
+
)`;
|
|
176
|
+
function rowToRecord(row) {
|
|
177
|
+
return {
|
|
178
|
+
domain: row.domain,
|
|
179
|
+
name: row.name,
|
|
180
|
+
description: row.description,
|
|
181
|
+
version: row.version,
|
|
182
|
+
roles: JSON.parse(row.roles),
|
|
183
|
+
skillMd: row.skill_md,
|
|
184
|
+
endpoints: JSON.parse(row.endpoints),
|
|
185
|
+
isFirstParty: row.is_first_party === 1,
|
|
186
|
+
createdAt: row.created_at,
|
|
187
|
+
updatedAt: row.updated_at,
|
|
188
|
+
status: row.status,
|
|
189
|
+
isDefault: row.is_default === 1,
|
|
190
|
+
...row.source ? { source: JSON.parse(row.source) } : {},
|
|
191
|
+
...row.sunset_date ? { sunsetDate: row.sunset_date } : {},
|
|
192
|
+
...row.auth_mode ? { authMode: row.auth_mode } : {}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function toSummary2(row) {
|
|
196
|
+
return {
|
|
197
|
+
domain: row.domain,
|
|
198
|
+
name: row.name,
|
|
199
|
+
description: row.description,
|
|
200
|
+
isFirstParty: row.is_first_party === 1
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
var D1RegistryStore = class _D1RegistryStore {
|
|
204
|
+
constructor(db) {
|
|
205
|
+
this.db = db;
|
|
206
|
+
}
|
|
207
|
+
async initSchema() {
|
|
208
|
+
await this.db.exec(CREATE_SERVICES);
|
|
209
|
+
await this.db.exec(CREATE_SERVICES_DEFAULT_INDEX);
|
|
210
|
+
await this.db.exec(CREATE_DOMAIN_CHALLENGES);
|
|
211
|
+
}
|
|
212
|
+
async get(domain) {
|
|
213
|
+
const row = await this.db.prepare("SELECT * FROM services WHERE domain = ? AND is_default = 1").bind(domain).first();
|
|
214
|
+
return row ? rowToRecord(row) : null;
|
|
215
|
+
}
|
|
216
|
+
async getVersion(domain, version) {
|
|
217
|
+
const row = await this.db.prepare("SELECT * FROM services WHERE domain = ? AND version = ?").bind(domain, version).first();
|
|
218
|
+
return row ? rowToRecord(row) : null;
|
|
219
|
+
}
|
|
220
|
+
async listVersions(domain) {
|
|
221
|
+
const { results } = await this.db.prepare(
|
|
222
|
+
"SELECT version, status, is_default, created_at, updated_at FROM services WHERE domain = ? ORDER BY created_at DESC"
|
|
223
|
+
).bind(domain).all();
|
|
224
|
+
return results.map((row) => ({
|
|
225
|
+
version: row.version,
|
|
226
|
+
status: row.status,
|
|
227
|
+
isDefault: row.is_default === 1,
|
|
228
|
+
createdAt: row.created_at,
|
|
229
|
+
updatedAt: row.updated_at
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
/** Max endpoints JSON size before stripping verbose fields (parameters, requestBody, responses). */
|
|
233
|
+
static ENDPOINTS_SIZE_LIMIT = 8e5;
|
|
234
|
+
// ~800 KB, well under D1's ~1 MB per-value limit
|
|
235
|
+
async put(domain, record) {
|
|
236
|
+
let endpointsJson = JSON.stringify(record.endpoints);
|
|
237
|
+
if (endpointsJson.length > _D1RegistryStore.ENDPOINTS_SIZE_LIMIT) {
|
|
238
|
+
const slim = record.endpoints.map(({ method, path, description, price, pricing }) => ({
|
|
239
|
+
method,
|
|
240
|
+
path,
|
|
241
|
+
description,
|
|
242
|
+
...price ? { price } : {},
|
|
243
|
+
...pricing ? { pricing } : {}
|
|
244
|
+
}));
|
|
245
|
+
endpointsJson = JSON.stringify(slim);
|
|
246
|
+
}
|
|
247
|
+
await this.db.prepare(
|
|
248
|
+
`INSERT OR REPLACE INTO services
|
|
249
|
+
(domain, version, name, description, roles, skill_md, endpoints, is_first_party, status, is_default, source, sunset_date, auth_mode, created_at, updated_at)
|
|
250
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
251
|
+
).bind(
|
|
252
|
+
domain,
|
|
253
|
+
record.version,
|
|
254
|
+
record.name,
|
|
255
|
+
record.description,
|
|
256
|
+
JSON.stringify(record.roles),
|
|
257
|
+
record.skillMd,
|
|
258
|
+
endpointsJson,
|
|
259
|
+
record.isFirstParty ? 1 : 0,
|
|
260
|
+
record.status,
|
|
261
|
+
record.isDefault ? 1 : 0,
|
|
262
|
+
record.source ? JSON.stringify(record.source) : null,
|
|
263
|
+
record.sunsetDate ?? null,
|
|
264
|
+
record.authMode ?? null,
|
|
265
|
+
record.createdAt,
|
|
266
|
+
record.updatedAt
|
|
267
|
+
).run();
|
|
268
|
+
}
|
|
269
|
+
async delete(domain) {
|
|
270
|
+
await this.db.prepare("DELETE FROM services WHERE domain = ?").bind(domain).run();
|
|
271
|
+
}
|
|
272
|
+
async list() {
|
|
273
|
+
const { results } = await this.db.prepare("SELECT domain, name, description, is_first_party FROM services WHERE is_default = 1").all();
|
|
274
|
+
return results.map(toSummary2);
|
|
275
|
+
}
|
|
276
|
+
async search(query) {
|
|
277
|
+
const pattern = `%${query}%`;
|
|
278
|
+
const { results: rows } = await this.db.prepare(
|
|
279
|
+
`SELECT * FROM services
|
|
280
|
+
WHERE is_default = 1 AND (name LIKE ? OR description LIKE ? OR endpoints LIKE ?)`
|
|
281
|
+
).bind(pattern, pattern, pattern).all();
|
|
282
|
+
const q = query.toLowerCase();
|
|
283
|
+
return rows.map((row) => {
|
|
284
|
+
const endpoints = JSON.parse(row.endpoints);
|
|
285
|
+
const matched = endpoints.filter(
|
|
286
|
+
(e) => e.description.toLowerCase().includes(q) || e.method.toLowerCase().includes(q) || e.path.toLowerCase().includes(q)
|
|
287
|
+
);
|
|
288
|
+
return {
|
|
289
|
+
...toSummary2(row),
|
|
290
|
+
matchedEndpoints: matched.map((e) => ({
|
|
291
|
+
method: e.method,
|
|
292
|
+
path: e.path,
|
|
293
|
+
description: e.description
|
|
294
|
+
}))
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
async stats() {
|
|
299
|
+
const row = await this.db.prepare(
|
|
300
|
+
`SELECT COUNT(*) as service_count, COALESCE(SUM(json_array_length(endpoints)), 0) as endpoint_count
|
|
301
|
+
FROM services WHERE is_default = 1`
|
|
302
|
+
).first();
|
|
303
|
+
return {
|
|
304
|
+
serviceCount: row?.service_count ?? 0,
|
|
305
|
+
endpointCount: row?.endpoint_count ?? 0
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/registry/skill-parser.ts
|
|
311
|
+
var import_yaml = require("yaml");
|
|
312
|
+
function parseSkillMd(domain, raw, options) {
|
|
313
|
+
const { frontmatter, body } = extractFrontmatter(raw);
|
|
314
|
+
const parsed = (0, import_yaml.parse)(frontmatter) ?? {};
|
|
315
|
+
const description = extractDescription(body);
|
|
316
|
+
const endpoints = extractEndpoints(body);
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
return {
|
|
319
|
+
domain,
|
|
320
|
+
name: parsed.name ?? domain,
|
|
321
|
+
description,
|
|
322
|
+
version: parsed.version ?? "0.0",
|
|
323
|
+
roles: parsed.roles ?? ["agent"],
|
|
324
|
+
skillMd: raw,
|
|
325
|
+
endpoints,
|
|
326
|
+
isFirstParty: options?.isFirstParty ?? false,
|
|
327
|
+
createdAt: now,
|
|
328
|
+
updatedAt: now,
|
|
329
|
+
status: "active",
|
|
330
|
+
isDefault: true
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function parsePricingAnnotation(text) {
|
|
334
|
+
const match = text.match(
|
|
335
|
+
/(\d+(?:\.\d+)?)\s+(\w+)\s*\/\s*(call|byte|minute|次)/i
|
|
336
|
+
);
|
|
337
|
+
if (!match) return void 0;
|
|
338
|
+
return {
|
|
339
|
+
cost: parseFloat(match[1]),
|
|
340
|
+
currency: match[2].toUpperCase(),
|
|
341
|
+
per: match[3] === "\u6B21" ? "call" : match[3].toLowerCase()
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function extractFrontmatter(raw) {
|
|
345
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
346
|
+
if (!match) return { frontmatter: "", body: raw };
|
|
347
|
+
return { frontmatter: match[1], body: match[2] };
|
|
348
|
+
}
|
|
349
|
+
function extractDescription(body) {
|
|
350
|
+
const lines = body.split("\n");
|
|
351
|
+
let foundTitle = false;
|
|
352
|
+
const descLines = [];
|
|
353
|
+
for (const line of lines) {
|
|
354
|
+
if (!foundTitle) {
|
|
355
|
+
if (line.startsWith("# ")) foundTitle = true;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (line.startsWith("## ")) break;
|
|
359
|
+
const trimmed = line.trim();
|
|
360
|
+
if (trimmed === "" && descLines.length > 0) break;
|
|
361
|
+
if (trimmed !== "") descLines.push(trimmed);
|
|
362
|
+
}
|
|
363
|
+
return descLines.join(" ");
|
|
364
|
+
}
|
|
365
|
+
function extractEndpoints(body) {
|
|
366
|
+
const endpoints = [];
|
|
367
|
+
const lines = body.split("\n");
|
|
368
|
+
let inApiSection = false;
|
|
369
|
+
let currentHeading = null;
|
|
370
|
+
for (const line of lines) {
|
|
371
|
+
if (line.startsWith("## API")) {
|
|
372
|
+
inApiSection = true;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (inApiSection && line.startsWith("## ") && !line.startsWith("## API")) {
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
if (!inApiSection) continue;
|
|
379
|
+
if (line.startsWith("### ")) {
|
|
380
|
+
currentHeading = line.slice(4).trim();
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const endpointMatch = line.match(/^`(GET|POST|PUT|PATCH|DELETE)\s+(\S+)`/);
|
|
384
|
+
if (endpointMatch && currentHeading) {
|
|
385
|
+
const afterBacktick = line.slice(line.indexOf("`", 1) + 1).trim();
|
|
386
|
+
const pricing = afterBacktick.startsWith("\u2014") ? parsePricingAnnotation(afterBacktick.slice(1).trim()) : void 0;
|
|
387
|
+
endpoints.push({
|
|
388
|
+
method: endpointMatch[1],
|
|
389
|
+
path: endpointMatch[2],
|
|
390
|
+
description: currentHeading,
|
|
391
|
+
...pricing ? { pricing } : {}
|
|
392
|
+
});
|
|
393
|
+
currentHeading = null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return endpoints;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/registry/skill-to-config.ts
|
|
400
|
+
function skillToHttpConfig(record) {
|
|
401
|
+
let baseUrl = `https://${record.domain}`;
|
|
402
|
+
if (record.source?.basePath) {
|
|
403
|
+
baseUrl += record.source.basePath;
|
|
404
|
+
}
|
|
405
|
+
const resources = extractResources(record.skillMd);
|
|
406
|
+
const endpoints = extractHttpEndpoints(record.skillMd);
|
|
407
|
+
return {
|
|
408
|
+
baseUrl,
|
|
409
|
+
resources,
|
|
410
|
+
endpoints
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function extractResources(skillMd) {
|
|
414
|
+
const resources = [];
|
|
415
|
+
const lines = skillMd.split("\n");
|
|
416
|
+
let inSchema = false;
|
|
417
|
+
let current = null;
|
|
418
|
+
for (const line of lines) {
|
|
419
|
+
if (line.startsWith("## Schema")) {
|
|
420
|
+
inSchema = true;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (inSchema && line.startsWith("## ") && !line.startsWith("## Schema")) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
if (!inSchema) continue;
|
|
427
|
+
const tableMatch = line.match(/^### (\w+)\s/);
|
|
428
|
+
if (tableMatch) {
|
|
429
|
+
if (current) resources.push(toHttpResource(current));
|
|
430
|
+
current = { name: tableMatch[1], fields: [] };
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (current && line.startsWith("|") && !line.startsWith("|--") && !line.startsWith("| field")) {
|
|
434
|
+
const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
|
|
435
|
+
if (cells.length >= 3) {
|
|
436
|
+
current.fields.push({
|
|
437
|
+
name: cells[0],
|
|
438
|
+
type: cells[1],
|
|
439
|
+
description: cells[2]
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (current) resources.push(toHttpResource(current));
|
|
445
|
+
return resources;
|
|
446
|
+
}
|
|
447
|
+
function toHttpResource(parsed) {
|
|
448
|
+
return {
|
|
449
|
+
name: parsed.name,
|
|
450
|
+
apiPath: `/${parsed.name}`,
|
|
451
|
+
fields: parsed.fields
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function extractHttpEndpoints(skillMd) {
|
|
455
|
+
const endpoints = [];
|
|
456
|
+
const lines = skillMd.split("\n");
|
|
457
|
+
let inApi = false;
|
|
458
|
+
let currentHeading = null;
|
|
459
|
+
for (const line of lines) {
|
|
460
|
+
if (line.startsWith("## API")) {
|
|
461
|
+
inApi = true;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (inApi && line.startsWith("## ") && !line.startsWith("## API")) {
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
if (!inApi) continue;
|
|
468
|
+
if (line.startsWith("### ")) {
|
|
469
|
+
currentHeading = line.slice(4).trim();
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const match = line.match(/^`(GET|POST|PUT|PATCH|DELETE)\s+(\S+)`/);
|
|
473
|
+
if (match && currentHeading) {
|
|
474
|
+
const slug = currentHeading.toLowerCase().replace(/\s+/g, "-");
|
|
475
|
+
endpoints.push({
|
|
476
|
+
name: slug,
|
|
477
|
+
method: match[1],
|
|
478
|
+
apiPath: match[2],
|
|
479
|
+
description: currentHeading
|
|
480
|
+
});
|
|
481
|
+
currentHeading = null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return endpoints;
|
|
485
|
+
}
|
|
486
|
+
function skillToRpcConfig(meta) {
|
|
487
|
+
const resources = meta.resources.map((r) => {
|
|
488
|
+
const methods = {};
|
|
489
|
+
const builder = getParamsBuilder(meta.convention);
|
|
490
|
+
for (const [fsOp, rpcMethod] of Object.entries(r.methods)) {
|
|
491
|
+
const key = fsOp;
|
|
492
|
+
methods[key] = {
|
|
493
|
+
method: rpcMethod,
|
|
494
|
+
params: builder(key)
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const resource = {
|
|
498
|
+
name: r.name,
|
|
499
|
+
...r.idField ? { idField: r.idField } : {},
|
|
500
|
+
methods
|
|
501
|
+
};
|
|
502
|
+
if (meta.convention === "evm") {
|
|
503
|
+
resource.transform = buildEvmTransforms(r.name);
|
|
504
|
+
}
|
|
505
|
+
return resource;
|
|
506
|
+
});
|
|
507
|
+
return { resources };
|
|
508
|
+
}
|
|
509
|
+
function getParamsBuilder(convention) {
|
|
510
|
+
switch (convention) {
|
|
511
|
+
case "crud":
|
|
512
|
+
return crudParamsBuilder;
|
|
513
|
+
case "evm":
|
|
514
|
+
return evmParamsBuilder;
|
|
515
|
+
case "raw":
|
|
516
|
+
default:
|
|
517
|
+
return rawParamsBuilder;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function rawParamsBuilder(_fsOp) {
|
|
521
|
+
return (ctx) => {
|
|
522
|
+
if (ctx.data !== void 0) return [ctx.data];
|
|
523
|
+
if (ctx.id !== void 0) return [ctx.id];
|
|
524
|
+
return [];
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function crudParamsBuilder(fsOp) {
|
|
528
|
+
switch (fsOp) {
|
|
529
|
+
case "list":
|
|
530
|
+
return () => [];
|
|
531
|
+
case "read":
|
|
532
|
+
return (ctx) => [ctx.id];
|
|
533
|
+
case "write":
|
|
534
|
+
return (ctx) => [ctx.id, ctx.data];
|
|
535
|
+
case "create":
|
|
536
|
+
return (ctx) => [ctx.data];
|
|
537
|
+
case "remove":
|
|
538
|
+
return (ctx) => [ctx.id];
|
|
539
|
+
case "search":
|
|
540
|
+
return (ctx) => [ctx.pattern];
|
|
541
|
+
default:
|
|
542
|
+
return () => [];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function evmParamsBuilder(fsOp) {
|
|
546
|
+
switch (fsOp) {
|
|
547
|
+
case "list":
|
|
548
|
+
return () => [];
|
|
549
|
+
case "read":
|
|
550
|
+
return (ctx) => {
|
|
551
|
+
const id = ctx.id;
|
|
552
|
+
const hexId = /^\d+$/.test(id) ? "0x" + Number(id).toString(16) : id;
|
|
553
|
+
return [hexId, "latest"];
|
|
554
|
+
};
|
|
555
|
+
default:
|
|
556
|
+
return rawParamsBuilder(fsOp);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function buildEvmTransforms(resourceName) {
|
|
560
|
+
const transform = {};
|
|
561
|
+
if (resourceName === "blocks") {
|
|
562
|
+
transform.list = (data) => {
|
|
563
|
+
const hex = String(data);
|
|
564
|
+
const latest = parseInt(hex, 16);
|
|
565
|
+
if (isNaN(latest)) return [];
|
|
566
|
+
return Array.from({ length: 10 }, (_, i) => `${latest - i}.json`);
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (resourceName === "balances") {
|
|
570
|
+
transform.read = (data) => {
|
|
571
|
+
const hex = String(data);
|
|
572
|
+
return { wei: hex, raw: hex };
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return Object.keys(transform).length > 0 ? transform : void 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/registry/resolver.ts
|
|
579
|
+
var import_agent_fs = require("@nkmc/agent-fs");
|
|
580
|
+
var import_core = require("@nkmc/core");
|
|
581
|
+
|
|
582
|
+
// src/registry/virtual-files.ts
|
|
583
|
+
var VIRTUAL_FILES = ["_pricing.json", "_versions.json", "skill.md"];
|
|
584
|
+
var VirtualFileBackend = class {
|
|
585
|
+
inner;
|
|
586
|
+
domain;
|
|
587
|
+
store;
|
|
588
|
+
constructor(options) {
|
|
589
|
+
this.inner = options.inner;
|
|
590
|
+
this.domain = options.domain;
|
|
591
|
+
this.store = options.store;
|
|
592
|
+
}
|
|
593
|
+
async list(path) {
|
|
594
|
+
const entries = await this.inner.list(path);
|
|
595
|
+
if (path === "/" || path === "" || path === ".") {
|
|
596
|
+
return [...entries, ...VIRTUAL_FILES];
|
|
597
|
+
}
|
|
598
|
+
return entries;
|
|
599
|
+
}
|
|
600
|
+
async read(path) {
|
|
601
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
602
|
+
if (cleaned === "_pricing.json") {
|
|
603
|
+
const record = await this.store.get(this.domain);
|
|
604
|
+
if (!record) return { endpoints: [] };
|
|
605
|
+
return {
|
|
606
|
+
domain: this.domain,
|
|
607
|
+
endpoints: record.endpoints.filter((ep) => ep.pricing).map((ep) => ({
|
|
608
|
+
method: ep.method,
|
|
609
|
+
path: ep.path,
|
|
610
|
+
description: ep.description,
|
|
611
|
+
pricing: ep.pricing
|
|
612
|
+
}))
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
if (cleaned === "_versions.json") {
|
|
616
|
+
const versions = await this.store.listVersions(this.domain);
|
|
617
|
+
return { domain: this.domain, versions };
|
|
618
|
+
}
|
|
619
|
+
if (cleaned === "skill.md") {
|
|
620
|
+
const record = await this.store.get(this.domain);
|
|
621
|
+
if (!record) return "# Not found\n";
|
|
622
|
+
return record.skillMd;
|
|
623
|
+
}
|
|
624
|
+
return this.inner.read(path);
|
|
625
|
+
}
|
|
626
|
+
async write(path, data) {
|
|
627
|
+
return this.inner.write(path, data);
|
|
628
|
+
}
|
|
629
|
+
async remove(path) {
|
|
630
|
+
return this.inner.remove(path);
|
|
631
|
+
}
|
|
632
|
+
async search(path, pattern) {
|
|
633
|
+
return this.inner.search(path, pattern);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// src/federation/peer-backend.ts
|
|
638
|
+
var PeerBackend = class {
|
|
639
|
+
constructor(client, peer, agentId) {
|
|
640
|
+
this.client = client;
|
|
641
|
+
this.peer = peer;
|
|
642
|
+
this.agentId = agentId;
|
|
643
|
+
}
|
|
644
|
+
async list(path) {
|
|
645
|
+
const result = await this.execOnPeer(`ls ${path}`);
|
|
646
|
+
return result.data ?? [];
|
|
647
|
+
}
|
|
648
|
+
async read(path) {
|
|
649
|
+
const result = await this.execOnPeer(`cat ${path}`);
|
|
650
|
+
return result.data;
|
|
651
|
+
}
|
|
652
|
+
async write(path, data) {
|
|
653
|
+
const result = await this.execOnPeer(
|
|
654
|
+
`write ${path} ${JSON.stringify(data)}`
|
|
655
|
+
);
|
|
656
|
+
return result.data ?? { id: "" };
|
|
657
|
+
}
|
|
658
|
+
async remove(path) {
|
|
659
|
+
await this.execOnPeer(`rm ${path}`);
|
|
660
|
+
}
|
|
661
|
+
async search(path, pattern) {
|
|
662
|
+
const result = await this.execOnPeer(`grep ${pattern} ${path}`);
|
|
663
|
+
return result.data ?? [];
|
|
664
|
+
}
|
|
665
|
+
async execOnPeer(command) {
|
|
666
|
+
const result = await this.client.exec(this.peer, {
|
|
667
|
+
command,
|
|
668
|
+
agentId: this.agentId
|
|
669
|
+
});
|
|
670
|
+
if (!result.ok) {
|
|
671
|
+
if (result.paymentRequired) {
|
|
672
|
+
throw new Error(
|
|
673
|
+
`Payment required: ${result.paymentRequired.price} ${result.paymentRequired.currency}`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
throw new Error(result.error ?? "Peer execution failed");
|
|
677
|
+
}
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// src/registry/resolver.ts
|
|
683
|
+
function createRegistryResolver(storeOrOptions) {
|
|
684
|
+
const options = "get" in storeOrOptions && "put" in storeOrOptions ? { store: storeOrOptions } : storeOrOptions;
|
|
685
|
+
const { store, vault, gatewayPrivateKey } = options;
|
|
686
|
+
const loaded = /* @__PURE__ */ new Set();
|
|
687
|
+
async function tryPeerFallback(domain, version, addMount, agent) {
|
|
688
|
+
if (!options.peerClient || !options.peerStore) return false;
|
|
689
|
+
const peers = await options.peerStore.listPeers();
|
|
690
|
+
for (const peer of peers) {
|
|
691
|
+
if (peer.advertisedDomains.length > 0 && !peer.advertisedDomains.includes(domain)) {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
const result = await options.peerClient.query(peer, domain);
|
|
695
|
+
if (result.available) {
|
|
696
|
+
const peerBackend = new PeerBackend(options.peerClient, peer, agent?.id ?? "anonymous");
|
|
697
|
+
const mountPath = version ? `/${domain}@${version}` : `/${domain}`;
|
|
698
|
+
addMount({ path: mountPath, backend: peerBackend });
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
async function onMiss(path, addMount, agent) {
|
|
705
|
+
const { domain, version } = extractDomainPath(path);
|
|
706
|
+
if (!domain) return false;
|
|
707
|
+
const cacheKey = version ? `${domain}@${version}` : domain;
|
|
708
|
+
const record = version ? await store.getVersion(domain, version) : await store.get(domain);
|
|
709
|
+
if (!record) {
|
|
710
|
+
return tryPeerFallback(domain, version, addMount, agent);
|
|
711
|
+
}
|
|
712
|
+
const isNkmcJwt = record.authMode === "nkmc-jwt";
|
|
713
|
+
if (!isNkmcJwt && loaded.has(cacheKey)) return false;
|
|
714
|
+
if (record.status === "sunset") return false;
|
|
715
|
+
let auth;
|
|
716
|
+
if (isNkmcJwt && gatewayPrivateKey && agent) {
|
|
717
|
+
const token = await (0, import_core.signJwt)(gatewayPrivateKey, {
|
|
718
|
+
sub: agent.id,
|
|
719
|
+
roles: agent.roles,
|
|
720
|
+
svc: domain
|
|
721
|
+
}, { expiresIn: "5m" });
|
|
722
|
+
auth = { type: "bearer", token };
|
|
723
|
+
} else if (vault) {
|
|
724
|
+
const cred = await vault.get(domain, agent?.id);
|
|
725
|
+
if (cred) {
|
|
726
|
+
auth = cred.auth;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (!auth && !isNkmcJwt) {
|
|
730
|
+
const peerMounted = await tryPeerFallback(domain, version, addMount, agent);
|
|
731
|
+
if (peerMounted) return true;
|
|
732
|
+
}
|
|
733
|
+
let backend;
|
|
734
|
+
if (record.source?.type === "jsonrpc" && record.source.rpc) {
|
|
735
|
+
const { resources } = skillToRpcConfig(record.source.rpc);
|
|
736
|
+
const headers = {};
|
|
737
|
+
if (auth) {
|
|
738
|
+
if (auth.type === "bearer") {
|
|
739
|
+
headers["Authorization"] = `${auth.prefix ?? "Bearer"} ${auth.token}`;
|
|
740
|
+
} else if (auth.type === "api-key") {
|
|
741
|
+
headers[auth.header] = auth.key;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const transport = new import_agent_fs.JsonRpcTransport({ url: record.source.rpc.rpcUrl, headers });
|
|
745
|
+
backend = new import_agent_fs.RpcBackend({ transport, resources });
|
|
746
|
+
} else {
|
|
747
|
+
const config = skillToHttpConfig(record);
|
|
748
|
+
config.auth = auth;
|
|
749
|
+
backend = new import_agent_fs.HttpBackend(config);
|
|
750
|
+
}
|
|
751
|
+
let finalBackend = backend;
|
|
752
|
+
if (options.wrapVirtualFiles !== false) {
|
|
753
|
+
finalBackend = new VirtualFileBackend({ inner: backend, domain, store: options.store });
|
|
754
|
+
}
|
|
755
|
+
const mountPath = version ? `/${domain}@${version}` : `/${domain}`;
|
|
756
|
+
addMount({ path: mountPath, backend: finalBackend });
|
|
757
|
+
if (!isNkmcJwt) {
|
|
758
|
+
loaded.add(cacheKey);
|
|
759
|
+
}
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
async function listDomains() {
|
|
763
|
+
const summaries = await store.list();
|
|
764
|
+
return summaries.map((s) => s.domain);
|
|
765
|
+
}
|
|
766
|
+
async function searchDomains(query) {
|
|
767
|
+
return store.search(query);
|
|
768
|
+
}
|
|
769
|
+
async function searchEndpoints(domain, query) {
|
|
770
|
+
const record = await store.get(domain);
|
|
771
|
+
if (!record) return [];
|
|
772
|
+
const q = query.toLowerCase();
|
|
773
|
+
return record.endpoints.filter(
|
|
774
|
+
(e) => e.description.toLowerCase().includes(q) || e.method.toLowerCase().includes(q) || e.path.toLowerCase().includes(q)
|
|
775
|
+
).map((e) => ({ method: e.method, path: e.path, description: e.description }));
|
|
776
|
+
}
|
|
777
|
+
return { onMiss, listDomains, searchDomains, searchEndpoints };
|
|
778
|
+
}
|
|
779
|
+
function extractDomainPath(path) {
|
|
780
|
+
const segments = path.split("/").filter(Boolean);
|
|
781
|
+
if (segments.length === 0) return { domain: null, version: null };
|
|
782
|
+
const first = segments[0];
|
|
783
|
+
const atIndex = first.indexOf("@");
|
|
784
|
+
if (atIndex > 0) {
|
|
785
|
+
return {
|
|
786
|
+
domain: first.slice(0, atIndex),
|
|
787
|
+
version: first.slice(atIndex + 1)
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
return { domain: first, version: null };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/metering/memory-store.ts
|
|
794
|
+
var MemoryMeterStore = class {
|
|
795
|
+
records = [];
|
|
796
|
+
async record(entry) {
|
|
797
|
+
this.records.push(entry);
|
|
798
|
+
}
|
|
799
|
+
async query(filter) {
|
|
800
|
+
return this.records.filter((r) => this.matches(r, filter));
|
|
801
|
+
}
|
|
802
|
+
async sum(filter) {
|
|
803
|
+
const matched = this.records.filter((r) => this.matches(r, filter));
|
|
804
|
+
const total = matched.reduce((acc, r) => acc + r.cost, 0);
|
|
805
|
+
const currency = matched[0]?.currency ?? "USDC";
|
|
806
|
+
return { total, currency };
|
|
807
|
+
}
|
|
808
|
+
matches(record, filter) {
|
|
809
|
+
if (filter.domain && record.domain !== filter.domain) return false;
|
|
810
|
+
if (filter.agentId && record.agentId !== filter.agentId) return false;
|
|
811
|
+
if (filter.developerId && record.developerId !== filter.developerId) return false;
|
|
812
|
+
if (filter.from && record.timestamp < filter.from) return false;
|
|
813
|
+
if (filter.to && record.timestamp > filter.to) return false;
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/metering/d1-store.ts
|
|
819
|
+
var CREATE_METER_RECORDS = `
|
|
820
|
+
CREATE TABLE IF NOT EXISTS meter_records (
|
|
821
|
+
id TEXT PRIMARY KEY,
|
|
822
|
+
timestamp INTEGER NOT NULL,
|
|
823
|
+
domain TEXT NOT NULL,
|
|
824
|
+
version TEXT NOT NULL,
|
|
825
|
+
endpoint TEXT NOT NULL,
|
|
826
|
+
agent_id TEXT NOT NULL,
|
|
827
|
+
developer_id TEXT,
|
|
828
|
+
cost REAL NOT NULL,
|
|
829
|
+
currency TEXT NOT NULL DEFAULT 'USDC'
|
|
830
|
+
)`;
|
|
831
|
+
var CREATE_METER_INDEX_DOMAIN = `
|
|
832
|
+
CREATE INDEX IF NOT EXISTS idx_meter_domain ON meter_records(domain, timestamp)`;
|
|
833
|
+
var CREATE_METER_INDEX_AGENT = `
|
|
834
|
+
CREATE INDEX IF NOT EXISTS idx_meter_agent ON meter_records(agent_id, timestamp)`;
|
|
835
|
+
var D1MeterStore = class {
|
|
836
|
+
constructor(db) {
|
|
837
|
+
this.db = db;
|
|
838
|
+
}
|
|
839
|
+
async initSchema() {
|
|
840
|
+
await this.db.exec(CREATE_METER_RECORDS);
|
|
841
|
+
await this.db.exec(CREATE_METER_INDEX_DOMAIN);
|
|
842
|
+
await this.db.exec(CREATE_METER_INDEX_AGENT);
|
|
843
|
+
}
|
|
844
|
+
async record(entry) {
|
|
845
|
+
await this.db.prepare(
|
|
846
|
+
`INSERT INTO meter_records (id, timestamp, domain, version, endpoint, agent_id, developer_id, cost, currency)
|
|
847
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
848
|
+
).bind(
|
|
849
|
+
entry.id,
|
|
850
|
+
entry.timestamp,
|
|
851
|
+
entry.domain,
|
|
852
|
+
entry.version,
|
|
853
|
+
entry.endpoint,
|
|
854
|
+
entry.agentId,
|
|
855
|
+
entry.developerId ?? null,
|
|
856
|
+
entry.cost,
|
|
857
|
+
entry.currency
|
|
858
|
+
).run();
|
|
859
|
+
}
|
|
860
|
+
async query(filter) {
|
|
861
|
+
const { sql, bindings } = this.buildQuery("SELECT *", filter);
|
|
862
|
+
const { results } = await this.db.prepare(sql).bind(...bindings).all();
|
|
863
|
+
return results.map(rowToRecord2);
|
|
864
|
+
}
|
|
865
|
+
async sum(filter) {
|
|
866
|
+
const { sql, bindings } = this.buildQuery("SELECT COALESCE(SUM(cost), 0) as total, COALESCE(MIN(currency), 'USDC') as currency", filter);
|
|
867
|
+
const row = await this.db.prepare(sql).bind(...bindings).first();
|
|
868
|
+
return { total: row?.total ?? 0, currency: row?.currency ?? "USDC" };
|
|
869
|
+
}
|
|
870
|
+
buildQuery(select, filter) {
|
|
871
|
+
const conditions = [];
|
|
872
|
+
const bindings = [];
|
|
873
|
+
if (filter.domain) {
|
|
874
|
+
conditions.push("domain = ?");
|
|
875
|
+
bindings.push(filter.domain);
|
|
876
|
+
}
|
|
877
|
+
if (filter.agentId) {
|
|
878
|
+
conditions.push("agent_id = ?");
|
|
879
|
+
bindings.push(filter.agentId);
|
|
880
|
+
}
|
|
881
|
+
if (filter.developerId) {
|
|
882
|
+
conditions.push("developer_id = ?");
|
|
883
|
+
bindings.push(filter.developerId);
|
|
884
|
+
}
|
|
885
|
+
if (filter.from) {
|
|
886
|
+
conditions.push("timestamp >= ?");
|
|
887
|
+
bindings.push(filter.from);
|
|
888
|
+
}
|
|
889
|
+
if (filter.to) {
|
|
890
|
+
conditions.push("timestamp <= ?");
|
|
891
|
+
bindings.push(filter.to);
|
|
892
|
+
}
|
|
893
|
+
let sql = `${select} FROM meter_records`;
|
|
894
|
+
if (conditions.length > 0) {
|
|
895
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
896
|
+
}
|
|
897
|
+
sql += " ORDER BY timestamp DESC";
|
|
898
|
+
return { sql, bindings };
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
function rowToRecord2(row) {
|
|
902
|
+
return {
|
|
903
|
+
id: row.id,
|
|
904
|
+
timestamp: row.timestamp,
|
|
905
|
+
domain: row.domain,
|
|
906
|
+
version: row.version,
|
|
907
|
+
endpoint: row.endpoint,
|
|
908
|
+
agentId: row.agent_id,
|
|
909
|
+
...row.developer_id ? { developerId: row.developer_id } : {},
|
|
910
|
+
cost: row.cost,
|
|
911
|
+
currency: row.currency
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/metering/pricing-guard.ts
|
|
916
|
+
function lookupPricing(record, method, path) {
|
|
917
|
+
for (const ep of record.endpoints) {
|
|
918
|
+
if (ep.method.toUpperCase() !== method.toUpperCase()) continue;
|
|
919
|
+
if (matchPath(ep.path, path)) {
|
|
920
|
+
return ep.pricing ?? null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
function checkAccess(record) {
|
|
926
|
+
if (record.status === "sunset") {
|
|
927
|
+
return { allowed: false, reason: "Service has been sunset" };
|
|
928
|
+
}
|
|
929
|
+
if (record.sunsetDate && record.sunsetDate < Date.now()) {
|
|
930
|
+
return { allowed: false, reason: "Service sunset date has passed" };
|
|
931
|
+
}
|
|
932
|
+
return { allowed: true };
|
|
933
|
+
}
|
|
934
|
+
async function meter(store, opts) {
|
|
935
|
+
const entry = {
|
|
936
|
+
id: `m_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
937
|
+
timestamp: Date.now(),
|
|
938
|
+
domain: opts.domain,
|
|
939
|
+
version: opts.version,
|
|
940
|
+
endpoint: opts.endpoint,
|
|
941
|
+
agentId: opts.agentId,
|
|
942
|
+
developerId: opts.developerId,
|
|
943
|
+
cost: opts.pricing.cost,
|
|
944
|
+
currency: opts.pricing.currency
|
|
945
|
+
};
|
|
946
|
+
await store.record(entry);
|
|
947
|
+
return entry;
|
|
948
|
+
}
|
|
949
|
+
function matchPath(pattern, actual) {
|
|
950
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
951
|
+
const actualParts = actual.split("/").filter(Boolean);
|
|
952
|
+
if (patternParts.length !== actualParts.length) return false;
|
|
953
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
954
|
+
if (patternParts[i].startsWith(":")) continue;
|
|
955
|
+
if (patternParts[i] !== actualParts[i]) return false;
|
|
956
|
+
}
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/credential/memory-vault.ts
|
|
961
|
+
var MemoryCredentialVault = class {
|
|
962
|
+
// key = "pool:domain" or "byok:domain:developerId"
|
|
963
|
+
credentials = /* @__PURE__ */ new Map();
|
|
964
|
+
poolKey(domain) {
|
|
965
|
+
return `pool:${domain}`;
|
|
966
|
+
}
|
|
967
|
+
byokKey(domain, developerId) {
|
|
968
|
+
return `byok:${domain}:${developerId}`;
|
|
969
|
+
}
|
|
970
|
+
async get(domain, developerId) {
|
|
971
|
+
if (developerId) {
|
|
972
|
+
const byok = this.credentials.get(this.byokKey(domain, developerId));
|
|
973
|
+
if (byok) return byok;
|
|
974
|
+
}
|
|
975
|
+
return this.credentials.get(this.poolKey(domain)) ?? null;
|
|
976
|
+
}
|
|
977
|
+
async putPool(domain, auth) {
|
|
978
|
+
this.credentials.set(this.poolKey(domain), { domain, auth, scope: "pool" });
|
|
979
|
+
}
|
|
980
|
+
async putByok(domain, developerId, auth) {
|
|
981
|
+
this.credentials.set(this.byokKey(domain, developerId), {
|
|
982
|
+
domain,
|
|
983
|
+
auth,
|
|
984
|
+
scope: "byok",
|
|
985
|
+
developerId
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
async delete(domain, developerId) {
|
|
989
|
+
if (developerId) {
|
|
990
|
+
this.credentials.delete(this.byokKey(domain, developerId));
|
|
991
|
+
} else {
|
|
992
|
+
this.credentials.delete(this.poolKey(domain));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async listDomains() {
|
|
996
|
+
const domains = /* @__PURE__ */ new Set();
|
|
997
|
+
for (const cred of this.credentials.values()) {
|
|
998
|
+
domains.add(cred.domain);
|
|
999
|
+
}
|
|
1000
|
+
return Array.from(domains);
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
// src/credential/d1-vault.ts
|
|
1005
|
+
var CREATE_CREDENTIALS = `
|
|
1006
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
1007
|
+
domain TEXT NOT NULL,
|
|
1008
|
+
scope TEXT NOT NULL DEFAULT 'pool',
|
|
1009
|
+
developer_id TEXT NOT NULL DEFAULT '',
|
|
1010
|
+
auth_encrypted TEXT NOT NULL,
|
|
1011
|
+
created_at INTEGER NOT NULL,
|
|
1012
|
+
updated_at INTEGER NOT NULL,
|
|
1013
|
+
PRIMARY KEY (domain, scope, developer_id)
|
|
1014
|
+
)`;
|
|
1015
|
+
async function encrypt(auth, key) {
|
|
1016
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
1017
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(auth));
|
|
1018
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
1019
|
+
{ name: "AES-GCM", iv },
|
|
1020
|
+
key,
|
|
1021
|
+
plaintext
|
|
1022
|
+
);
|
|
1023
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
1024
|
+
combined.set(iv);
|
|
1025
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
1026
|
+
return btoa(String.fromCharCode(...combined));
|
|
1027
|
+
}
|
|
1028
|
+
async function decrypt(encoded, key) {
|
|
1029
|
+
const bytes = Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0));
|
|
1030
|
+
const iv = bytes.slice(0, 12);
|
|
1031
|
+
const ciphertext = bytes.slice(12);
|
|
1032
|
+
try {
|
|
1033
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
1034
|
+
{ name: "AES-GCM", iv },
|
|
1035
|
+
key,
|
|
1036
|
+
ciphertext
|
|
1037
|
+
);
|
|
1038
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
1039
|
+
} catch {
|
|
1040
|
+
return JSON.parse(atob(encoded));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
var D1CredentialVault = class {
|
|
1044
|
+
constructor(db, encryptionKey) {
|
|
1045
|
+
this.db = db;
|
|
1046
|
+
this.encryptionKey = encryptionKey;
|
|
1047
|
+
}
|
|
1048
|
+
async initSchema() {
|
|
1049
|
+
await this.db.exec(CREATE_CREDENTIALS);
|
|
1050
|
+
}
|
|
1051
|
+
async get(domain, developerId) {
|
|
1052
|
+
if (developerId) {
|
|
1053
|
+
const byok = await this.db.prepare("SELECT * FROM credentials WHERE domain = ? AND scope = 'byok' AND developer_id = ?").bind(domain, developerId).first();
|
|
1054
|
+
if (byok) return await this.rowToCredential(byok);
|
|
1055
|
+
}
|
|
1056
|
+
const pool = await this.db.prepare("SELECT * FROM credentials WHERE domain = ? AND scope = 'pool' AND developer_id = ''").bind(domain).first();
|
|
1057
|
+
return pool ? await this.rowToCredential(pool) : null;
|
|
1058
|
+
}
|
|
1059
|
+
async putPool(domain, auth) {
|
|
1060
|
+
const now = Date.now();
|
|
1061
|
+
await this.db.prepare(
|
|
1062
|
+
`INSERT OR REPLACE INTO credentials (domain, scope, developer_id, auth_encrypted, created_at, updated_at)
|
|
1063
|
+
VALUES (?, 'pool', '', ?, ?, ?)`
|
|
1064
|
+
).bind(domain, await encrypt(auth, this.encryptionKey), now, now).run();
|
|
1065
|
+
}
|
|
1066
|
+
async putByok(domain, developerId, auth) {
|
|
1067
|
+
const now = Date.now();
|
|
1068
|
+
await this.db.prepare(
|
|
1069
|
+
`INSERT OR REPLACE INTO credentials (domain, scope, developer_id, auth_encrypted, created_at, updated_at)
|
|
1070
|
+
VALUES (?, 'byok', ?, ?, ?, ?)`
|
|
1071
|
+
).bind(domain, developerId, await encrypt(auth, this.encryptionKey), now, now).run();
|
|
1072
|
+
}
|
|
1073
|
+
async delete(domain, developerId) {
|
|
1074
|
+
if (developerId) {
|
|
1075
|
+
await this.db.prepare("DELETE FROM credentials WHERE domain = ? AND scope = 'byok' AND developer_id = ?").bind(domain, developerId).run();
|
|
1076
|
+
} else {
|
|
1077
|
+
await this.db.prepare("DELETE FROM credentials WHERE domain = ? AND scope = 'pool' AND developer_id = ''").bind(domain).run();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
async listDomains() {
|
|
1081
|
+
const { results } = await this.db.prepare("SELECT DISTINCT domain FROM credentials").all();
|
|
1082
|
+
return results.map((r) => r.domain);
|
|
1083
|
+
}
|
|
1084
|
+
async rowToCredential(row) {
|
|
1085
|
+
return {
|
|
1086
|
+
domain: row.domain,
|
|
1087
|
+
auth: await decrypt(row.auth_encrypted, this.encryptionKey),
|
|
1088
|
+
scope: row.scope,
|
|
1089
|
+
...row.developer_id ? { developerId: row.developer_id } : {}
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
// src/http/routes/credentials.ts
|
|
1095
|
+
var import_hono = require("hono");
|
|
1096
|
+
function credentialRoutes(options) {
|
|
1097
|
+
const { vault } = options;
|
|
1098
|
+
const app = new import_hono.Hono();
|
|
1099
|
+
app.put("/:domain", async (c) => {
|
|
1100
|
+
const domain = c.req.param("domain");
|
|
1101
|
+
const body = await c.req.json();
|
|
1102
|
+
if (!body.auth?.type) {
|
|
1103
|
+
return c.json({ error: "Missing auth.type" }, 400);
|
|
1104
|
+
}
|
|
1105
|
+
await vault.putPool(domain, body.auth);
|
|
1106
|
+
return c.json({ ok: true, domain });
|
|
1107
|
+
});
|
|
1108
|
+
app.get("/", async (c) => {
|
|
1109
|
+
const domains = await vault.listDomains();
|
|
1110
|
+
return c.json({ domains });
|
|
1111
|
+
});
|
|
1112
|
+
app.delete("/:domain", async (c) => {
|
|
1113
|
+
const domain = c.req.param("domain");
|
|
1114
|
+
await vault.delete(domain);
|
|
1115
|
+
return c.json({ ok: true, domain });
|
|
1116
|
+
});
|
|
1117
|
+
return app;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/http/lib/dns.ts
|
|
1121
|
+
async function queryDnsTxt(domain) {
|
|
1122
|
+
const url = new URL("https://cloudflare-dns.com/dns-query");
|
|
1123
|
+
url.searchParams.set("name", domain);
|
|
1124
|
+
url.searchParams.set("type", "TXT");
|
|
1125
|
+
const res = await fetch(url.toString(), {
|
|
1126
|
+
headers: { Accept: "application/dns-json" }
|
|
1127
|
+
});
|
|
1128
|
+
if (!res.ok) {
|
|
1129
|
+
throw new Error(`DNS query failed: ${res.status}`);
|
|
1130
|
+
}
|
|
1131
|
+
const data = await res.json();
|
|
1132
|
+
return (data.Answer ?? []).filter((a) => a.type === 16).map((a) => a.data.replace(/^"|"$/g, ""));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/registry/context7.ts
|
|
1136
|
+
var Context7Client = class {
|
|
1137
|
+
apiKey;
|
|
1138
|
+
baseUrl;
|
|
1139
|
+
fetchFn;
|
|
1140
|
+
constructor(options) {
|
|
1141
|
+
this.apiKey = options?.apiKey;
|
|
1142
|
+
this.baseUrl = options?.baseUrl ?? "https://context7.com/api/v2";
|
|
1143
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
1144
|
+
}
|
|
1145
|
+
/** Search for a library by name. Returns matching library entries. */
|
|
1146
|
+
async searchLibraries(libraryName, query) {
|
|
1147
|
+
const params = new URLSearchParams({ libraryName });
|
|
1148
|
+
if (query) params.set("query", query);
|
|
1149
|
+
const resp = await this.fetchFn(`${this.baseUrl}/libs/search?${params}`, {
|
|
1150
|
+
headers: this.headers()
|
|
1151
|
+
});
|
|
1152
|
+
if (!resp.ok) throw new Error(`Context7 search failed: ${resp.status}`);
|
|
1153
|
+
return resp.json();
|
|
1154
|
+
}
|
|
1155
|
+
/** Query documentation for a specific library. Returns documentation text. */
|
|
1156
|
+
async queryDocs(libraryId, query) {
|
|
1157
|
+
const params = new URLSearchParams({ libraryId, query, type: "txt" });
|
|
1158
|
+
const resp = await this.fetchFn(`${this.baseUrl}/context?${params}`, {
|
|
1159
|
+
headers: this.headers()
|
|
1160
|
+
});
|
|
1161
|
+
if (!resp.ok) throw new Error(`Context7 query failed: ${resp.status}`);
|
|
1162
|
+
return resp.text();
|
|
1163
|
+
}
|
|
1164
|
+
headers() {
|
|
1165
|
+
const h = {};
|
|
1166
|
+
if (this.apiKey) h["Authorization"] = `Bearer ${this.apiKey}`;
|
|
1167
|
+
return h;
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
// src/registry/context7-backend.ts
|
|
1172
|
+
var Context7Backend = class {
|
|
1173
|
+
client;
|
|
1174
|
+
constructor(options) {
|
|
1175
|
+
this.client = new Context7Client(options);
|
|
1176
|
+
}
|
|
1177
|
+
async list(path) {
|
|
1178
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
1179
|
+
if (!cleaned) {
|
|
1180
|
+
return [
|
|
1181
|
+
'grep "<\u5173\u952E\u8BCD>" /context7/ \u2014 \u641C\u7D22\u5E93',
|
|
1182
|
+
'grep "<\u95EE\u9898>" /context7/{id} \u2014 \u67E5\u8BE2\u6587\u6863',
|
|
1183
|
+
"cat /context7/{owner}/{repo} \u2014 \u5E93\u6982\u89C8"
|
|
1184
|
+
];
|
|
1185
|
+
}
|
|
1186
|
+
return ['grep "<\u95EE\u9898>" /context7/' + cleaned + " \u2014 \u67E5\u8BE2\u6B64\u5E93\u6587\u6863"];
|
|
1187
|
+
}
|
|
1188
|
+
async read(path) {
|
|
1189
|
+
const libraryId = parseLibraryId(path);
|
|
1190
|
+
if (!libraryId) {
|
|
1191
|
+
return { usage: 'grep "<\u5173\u952E\u8BCD>" /context7/ \u2014 \u641C\u7D22\u5E93' };
|
|
1192
|
+
}
|
|
1193
|
+
const name = libraryId.split("/").pop() ?? libraryId;
|
|
1194
|
+
const docs = await this.client.queryDocs(libraryId, `${name} overview getting started`);
|
|
1195
|
+
return { libraryId, docs };
|
|
1196
|
+
}
|
|
1197
|
+
async write(_path, _data) {
|
|
1198
|
+
throw new Error("context7 is read-only");
|
|
1199
|
+
}
|
|
1200
|
+
async remove(_path) {
|
|
1201
|
+
throw new Error("context7 is read-only");
|
|
1202
|
+
}
|
|
1203
|
+
async search(path, pattern) {
|
|
1204
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
1205
|
+
if (!cleaned) {
|
|
1206
|
+
const results = await this.client.searchLibraries(pattern);
|
|
1207
|
+
return results.map(formatSearchResult);
|
|
1208
|
+
}
|
|
1209
|
+
const libraryId = parseLibraryId(path);
|
|
1210
|
+
if (!libraryId) return [];
|
|
1211
|
+
const docs = await this.client.queryDocs(libraryId, pattern);
|
|
1212
|
+
if (!docs) return [];
|
|
1213
|
+
return [{ libraryId, query: pattern, docs }];
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
function parseLibraryId(path) {
|
|
1217
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
1218
|
+
if (!cleaned) return null;
|
|
1219
|
+
const parts = cleaned.split("/");
|
|
1220
|
+
if (parts.length < 2) return null;
|
|
1221
|
+
return "/" + parts.slice(0, 2).join("/");
|
|
1222
|
+
}
|
|
1223
|
+
function formatSearchResult(r) {
|
|
1224
|
+
return {
|
|
1225
|
+
id: r.id,
|
|
1226
|
+
name: r.name,
|
|
1227
|
+
description: r.description ?? "",
|
|
1228
|
+
snippets: r.totalSnippets ?? 0
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/onboard/pipeline.ts
|
|
1233
|
+
var import_agent_fs2 = require("@nkmc/agent-fs");
|
|
1234
|
+
|
|
1235
|
+
// src/registry/openapi-compiler.ts
|
|
1236
|
+
var import_yaml2 = __toESM(require("yaml"), 1);
|
|
1237
|
+
function extractBasePath(spec) {
|
|
1238
|
+
const servers = spec.servers;
|
|
1239
|
+
if (!Array.isArray(servers) || servers.length === 0) return "";
|
|
1240
|
+
const serverUrl = servers[0]?.url;
|
|
1241
|
+
if (!serverUrl || typeof serverUrl !== "string") return "";
|
|
1242
|
+
try {
|
|
1243
|
+
if (serverUrl.startsWith("/")) {
|
|
1244
|
+
return serverUrl.replace(/\/+$/, "");
|
|
1245
|
+
}
|
|
1246
|
+
const parsed = new URL(serverUrl);
|
|
1247
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
1248
|
+
return pathname || "";
|
|
1249
|
+
} catch {
|
|
1250
|
+
return "";
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function resolveRef(spec, ref) {
|
|
1254
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
1255
|
+
const parts = ref.slice(2).split("/");
|
|
1256
|
+
let current = spec;
|
|
1257
|
+
for (const part of parts) {
|
|
1258
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
1259
|
+
current = current[part];
|
|
1260
|
+
}
|
|
1261
|
+
return current;
|
|
1262
|
+
}
|
|
1263
|
+
function resolveSchema(spec, schema) {
|
|
1264
|
+
if (!schema) return void 0;
|
|
1265
|
+
if (schema.$ref) return resolveRef(spec, schema.$ref);
|
|
1266
|
+
return schema;
|
|
1267
|
+
}
|
|
1268
|
+
function extractProperties(spec, schema) {
|
|
1269
|
+
const resolved = resolveSchema(spec, schema);
|
|
1270
|
+
if (!resolved || resolved.type !== "object" || !resolved.properties) return [];
|
|
1271
|
+
const requiredSet = new Set(resolved.required ?? []);
|
|
1272
|
+
const props = [];
|
|
1273
|
+
for (const [name, prop] of Object.entries(resolved.properties)) {
|
|
1274
|
+
const p = resolveSchema(spec, prop) ?? prop;
|
|
1275
|
+
props.push({
|
|
1276
|
+
name,
|
|
1277
|
+
type: p.type ?? (p.enum ? "enum" : "unknown"),
|
|
1278
|
+
required: requiredSet.has(name),
|
|
1279
|
+
...p.description ? { description: p.description } : {}
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
return props;
|
|
1283
|
+
}
|
|
1284
|
+
function extractParams(spec, operation) {
|
|
1285
|
+
const params = operation.parameters;
|
|
1286
|
+
if (!Array.isArray(params) || params.length === 0) return void 0;
|
|
1287
|
+
const result = [];
|
|
1288
|
+
for (const raw of params) {
|
|
1289
|
+
const p = resolveSchema(spec, raw) ?? raw;
|
|
1290
|
+
if (!p.name || !p.in) continue;
|
|
1291
|
+
if (!["path", "query", "header"].includes(p.in)) continue;
|
|
1292
|
+
const schema = resolveSchema(spec, p.schema) ?? p.schema;
|
|
1293
|
+
result.push({
|
|
1294
|
+
name: p.name,
|
|
1295
|
+
in: p.in,
|
|
1296
|
+
required: !!p.required,
|
|
1297
|
+
type: schema?.type ?? "string",
|
|
1298
|
+
...p.description ? { description: p.description } : {}
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
return result.length > 0 ? result : void 0;
|
|
1302
|
+
}
|
|
1303
|
+
function extractRequestBody(spec, operation) {
|
|
1304
|
+
const body = resolveSchema(spec, operation.requestBody);
|
|
1305
|
+
if (!body?.content) return void 0;
|
|
1306
|
+
const jsonContent = body.content["application/json"];
|
|
1307
|
+
if (!jsonContent?.schema) return void 0;
|
|
1308
|
+
const properties = extractProperties(spec, jsonContent.schema);
|
|
1309
|
+
if (properties.length === 0) return void 0;
|
|
1310
|
+
return {
|
|
1311
|
+
contentType: "application/json",
|
|
1312
|
+
required: !!body.required,
|
|
1313
|
+
properties
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function extractResponses(spec, operation) {
|
|
1317
|
+
const responses = operation.responses;
|
|
1318
|
+
if (!responses || typeof responses !== "object") return void 0;
|
|
1319
|
+
const result = [];
|
|
1320
|
+
for (const [code, raw] of Object.entries(responses)) {
|
|
1321
|
+
const status = parseInt(code, 10);
|
|
1322
|
+
if (isNaN(status) || status < 200 || status >= 300) continue;
|
|
1323
|
+
const resp = resolveSchema(spec, raw) ?? raw;
|
|
1324
|
+
const jsonContent = resp?.content?.["application/json"];
|
|
1325
|
+
const properties = jsonContent?.schema ? extractProperties(spec, jsonContent.schema) : void 0;
|
|
1326
|
+
result.push({
|
|
1327
|
+
status,
|
|
1328
|
+
description: resp?.description ?? "",
|
|
1329
|
+
...properties && properties.length > 0 ? { properties } : {}
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
return result.length > 0 ? result : void 0;
|
|
1333
|
+
}
|
|
1334
|
+
function compileOpenApiSpec(spec, options) {
|
|
1335
|
+
const info = spec.info ?? {};
|
|
1336
|
+
const name = info.title ?? options.domain;
|
|
1337
|
+
const description = info.description ?? "";
|
|
1338
|
+
const version = options.version ?? info.version ?? "1.0";
|
|
1339
|
+
const basePath = extractBasePath(spec);
|
|
1340
|
+
const endpoints = [];
|
|
1341
|
+
const resources = [];
|
|
1342
|
+
const resourcePaths = /* @__PURE__ */ new Map();
|
|
1343
|
+
const paths = spec.paths ?? {};
|
|
1344
|
+
for (const [path, methods] of Object.entries(paths)) {
|
|
1345
|
+
if (typeof methods !== "object" || methods === null) continue;
|
|
1346
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
1347
|
+
if (!["get", "post", "put", "patch", "delete"].includes(method)) continue;
|
|
1348
|
+
const operation = op;
|
|
1349
|
+
const parameters = extractParams(spec, operation);
|
|
1350
|
+
const requestBody = extractRequestBody(spec, operation);
|
|
1351
|
+
const responses = extractResponses(spec, operation);
|
|
1352
|
+
endpoints.push({
|
|
1353
|
+
method: method.toUpperCase(),
|
|
1354
|
+
path,
|
|
1355
|
+
description: operation.summary ?? operation.operationId ?? `${method.toUpperCase()} ${path}`,
|
|
1356
|
+
...parameters ? { parameters } : {},
|
|
1357
|
+
...requestBody ? { requestBody } : {},
|
|
1358
|
+
...responses ? { responses } : {}
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
const segments = path.split("/").filter(Boolean);
|
|
1362
|
+
if (segments.length >= 1) {
|
|
1363
|
+
const resourceName = segments[0];
|
|
1364
|
+
if (!resourcePaths.has(resourceName) && !resourceName.startsWith("{")) {
|
|
1365
|
+
resourcePaths.set(resourceName, {
|
|
1366
|
+
name: resourceName,
|
|
1367
|
+
apiPath: `/${resourceName}`
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
for (const r of resourcePaths.values()) {
|
|
1373
|
+
resources.push(r);
|
|
1374
|
+
}
|
|
1375
|
+
const skillMd = generateSkillMd(name, version, description, endpoints, resources);
|
|
1376
|
+
const now = Date.now();
|
|
1377
|
+
const record = {
|
|
1378
|
+
domain: options.domain,
|
|
1379
|
+
name,
|
|
1380
|
+
description,
|
|
1381
|
+
version,
|
|
1382
|
+
roles: ["agent"],
|
|
1383
|
+
skillMd,
|
|
1384
|
+
endpoints,
|
|
1385
|
+
isFirstParty: options.isFirstParty ?? false,
|
|
1386
|
+
createdAt: now,
|
|
1387
|
+
updatedAt: now,
|
|
1388
|
+
status: "active",
|
|
1389
|
+
isDefault: true,
|
|
1390
|
+
source: { type: "openapi", ...basePath ? { basePath } : {} }
|
|
1391
|
+
};
|
|
1392
|
+
return { record, resources, skillMd };
|
|
1393
|
+
}
|
|
1394
|
+
async function fetchAndCompile(specUrl, options, fetchFn = globalThis.fetch.bind(globalThis)) {
|
|
1395
|
+
const resp = await fetchFn(specUrl);
|
|
1396
|
+
if (!resp.ok) throw new Error(`Failed to fetch spec: ${resp.status} ${resp.statusText}`);
|
|
1397
|
+
const text = await resp.text();
|
|
1398
|
+
const spec = parseSpec(specUrl, resp.headers.get("content-type") ?? "", text);
|
|
1399
|
+
const result = compileOpenApiSpec(spec, options);
|
|
1400
|
+
const basePath = result.record.source?.basePath;
|
|
1401
|
+
result.record.source = { type: "openapi", url: specUrl, ...basePath ? { basePath } : {} };
|
|
1402
|
+
return result;
|
|
1403
|
+
}
|
|
1404
|
+
function parseSpec(url, contentType, text) {
|
|
1405
|
+
const isJson = contentType.includes("json") || url.endsWith(".json") || text.trimStart().startsWith("{");
|
|
1406
|
+
if (isJson) return JSON.parse(text);
|
|
1407
|
+
return import_yaml2.default.parse(text);
|
|
1408
|
+
}
|
|
1409
|
+
function propsTable(props) {
|
|
1410
|
+
let t = "| name | type | required |\n|------|------|----------|\n";
|
|
1411
|
+
for (const p of props) {
|
|
1412
|
+
t += `| ${p.name} | ${p.type} | ${p.required ? "*" : ""} |
|
|
1413
|
+
`;
|
|
1414
|
+
}
|
|
1415
|
+
return t;
|
|
1416
|
+
}
|
|
1417
|
+
function generateSkillMd(name, version, description, endpoints, resources) {
|
|
1418
|
+
let md = `---
|
|
1419
|
+
name: "${name}"
|
|
1420
|
+
gateway: nkmc
|
|
1421
|
+
version: "${version}"
|
|
1422
|
+
roles: [agent]
|
|
1423
|
+
---
|
|
1424
|
+
|
|
1425
|
+
`;
|
|
1426
|
+
md += `# ${name}
|
|
1427
|
+
|
|
1428
|
+
${description}
|
|
1429
|
+
|
|
1430
|
+
`;
|
|
1431
|
+
if (resources.length > 0) {
|
|
1432
|
+
md += `## Schema
|
|
1433
|
+
|
|
1434
|
+
`;
|
|
1435
|
+
for (const r of resources) {
|
|
1436
|
+
md += `### ${r.name} (public)
|
|
1437
|
+
|
|
1438
|
+
`;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (endpoints.length > 0) {
|
|
1442
|
+
md += `## API
|
|
1443
|
+
|
|
1444
|
+
`;
|
|
1445
|
+
for (const ep of endpoints) {
|
|
1446
|
+
md += `### ${ep.description}
|
|
1447
|
+
|
|
1448
|
+
`;
|
|
1449
|
+
md += `\`${ep.method} ${ep.path}\`
|
|
1450
|
+
|
|
1451
|
+
`;
|
|
1452
|
+
if (ep.parameters && ep.parameters.length > 0) {
|
|
1453
|
+
md += "**Parameters:**\n\n";
|
|
1454
|
+
md += "| name | in | type | required |\n|------|-----|------|----------|\n";
|
|
1455
|
+
for (const p of ep.parameters) {
|
|
1456
|
+
md += `| ${p.name} | ${p.in} | ${p.type} | ${p.required ? "*" : ""} |
|
|
1457
|
+
`;
|
|
1458
|
+
}
|
|
1459
|
+
md += "\n";
|
|
1460
|
+
}
|
|
1461
|
+
if (ep.requestBody) {
|
|
1462
|
+
const req = ep.requestBody;
|
|
1463
|
+
md += `**Body** (${req.contentType}${req.required ? ", required" : ""}):
|
|
1464
|
+
|
|
1465
|
+
`;
|
|
1466
|
+
md += propsTable(req.properties);
|
|
1467
|
+
md += "\n";
|
|
1468
|
+
}
|
|
1469
|
+
if (ep.responses && ep.responses.length > 0) {
|
|
1470
|
+
for (const r of ep.responses) {
|
|
1471
|
+
md += `**Response ${r.status}**${r.description ? `: ${r.description}` : ""}
|
|
1472
|
+
|
|
1473
|
+
`;
|
|
1474
|
+
if (r.properties && r.properties.length > 0) {
|
|
1475
|
+
md += propsTable(r.properties);
|
|
1476
|
+
md += "\n";
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return md;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/registry/rpc-compiler.ts
|
|
1486
|
+
function compileRpcDef(domain, rpcDef, options) {
|
|
1487
|
+
const convention = rpcDef.convention ?? "raw";
|
|
1488
|
+
const endpoints = rpcDef.methods.map((m) => ({
|
|
1489
|
+
method: "RPC",
|
|
1490
|
+
path: m.rpcMethod,
|
|
1491
|
+
description: m.description
|
|
1492
|
+
}));
|
|
1493
|
+
const resourceMap = /* @__PURE__ */ new Map();
|
|
1494
|
+
for (const m of rpcDef.methods) {
|
|
1495
|
+
const resName = m.resource ?? inferResource(m.rpcMethod);
|
|
1496
|
+
if (!resourceMap.has(resName)) {
|
|
1497
|
+
resourceMap.set(resName, { name: resName, methods: {} });
|
|
1498
|
+
}
|
|
1499
|
+
const entry = resourceMap.get(resName);
|
|
1500
|
+
if (m.fsOp) {
|
|
1501
|
+
entry.methods[m.fsOp] = m.rpcMethod;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
const rpcMeta = {
|
|
1505
|
+
rpcUrl: rpcDef.url,
|
|
1506
|
+
convention,
|
|
1507
|
+
resources: Array.from(resourceMap.values())
|
|
1508
|
+
};
|
|
1509
|
+
const skillMd = generateSkillMd2(domain, rpcDef, endpoints);
|
|
1510
|
+
const now = Date.now();
|
|
1511
|
+
const record = {
|
|
1512
|
+
domain,
|
|
1513
|
+
name: domain,
|
|
1514
|
+
description: `JSON-RPC service at ${domain}`,
|
|
1515
|
+
version: options?.version ?? "1.0",
|
|
1516
|
+
roles: ["agent"],
|
|
1517
|
+
skillMd,
|
|
1518
|
+
endpoints,
|
|
1519
|
+
isFirstParty: options?.isFirstParty ?? false,
|
|
1520
|
+
createdAt: now,
|
|
1521
|
+
updatedAt: now,
|
|
1522
|
+
status: "active",
|
|
1523
|
+
isDefault: true,
|
|
1524
|
+
source: {
|
|
1525
|
+
type: "jsonrpc",
|
|
1526
|
+
url: rpcDef.url,
|
|
1527
|
+
rpc: rpcMeta
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
return { record, skillMd };
|
|
1531
|
+
}
|
|
1532
|
+
function inferResource(rpcMethod) {
|
|
1533
|
+
const underscoreIdx = rpcMethod.indexOf("_");
|
|
1534
|
+
if (underscoreIdx < 0) return rpcMethod;
|
|
1535
|
+
const action = rpcMethod.slice(underscoreIdx + 1);
|
|
1536
|
+
const verbPrefixes = ["get", "send", "subscribe", "unsubscribe", "new", "call"];
|
|
1537
|
+
let noun = action;
|
|
1538
|
+
for (const prefix of verbPrefixes) {
|
|
1539
|
+
if (action.startsWith(prefix) && action.length > prefix.length) {
|
|
1540
|
+
noun = action.slice(prefix.length);
|
|
1541
|
+
break;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
const camelMatch = noun.match(/^([A-Z][a-z]+)/);
|
|
1545
|
+
if (camelMatch) {
|
|
1546
|
+
noun = camelMatch[1];
|
|
1547
|
+
}
|
|
1548
|
+
const lower = noun.toLowerCase();
|
|
1549
|
+
return lower.endsWith("s") ? lower : lower + "s";
|
|
1550
|
+
}
|
|
1551
|
+
function generateSkillMd2(domain, rpcDef, endpoints) {
|
|
1552
|
+
const lines = [
|
|
1553
|
+
"---",
|
|
1554
|
+
`name: "${domain}"`,
|
|
1555
|
+
`version: "1.0"`,
|
|
1556
|
+
`roles: [agent]`,
|
|
1557
|
+
"---",
|
|
1558
|
+
"",
|
|
1559
|
+
`# ${domain}`,
|
|
1560
|
+
"",
|
|
1561
|
+
`JSON-RPC service at ${rpcDef.url}`,
|
|
1562
|
+
"",
|
|
1563
|
+
`Convention: ${rpcDef.convention ?? "raw"}`,
|
|
1564
|
+
"",
|
|
1565
|
+
"## RPC Methods",
|
|
1566
|
+
"",
|
|
1567
|
+
"| method | description |",
|
|
1568
|
+
"|--------|-------------|"
|
|
1569
|
+
];
|
|
1570
|
+
for (const ep of endpoints) {
|
|
1571
|
+
lines.push(`| ${ep.path} | ${ep.description} |`);
|
|
1572
|
+
}
|
|
1573
|
+
return lines.join("\n");
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/onboard/pipeline.ts
|
|
1577
|
+
var OnboardPipeline = class {
|
|
1578
|
+
store;
|
|
1579
|
+
vault;
|
|
1580
|
+
smokeTest;
|
|
1581
|
+
concurrency;
|
|
1582
|
+
fetchFn;
|
|
1583
|
+
onProgress;
|
|
1584
|
+
constructor(options) {
|
|
1585
|
+
this.store = options.store;
|
|
1586
|
+
this.vault = options.vault;
|
|
1587
|
+
this.smokeTest = options.smokeTest !== false;
|
|
1588
|
+
this.concurrency = options.concurrency ?? 5;
|
|
1589
|
+
this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
1590
|
+
this.onProgress = options.onProgress;
|
|
1591
|
+
}
|
|
1592
|
+
/** Onboard a single service */
|
|
1593
|
+
async onboardOne(entry) {
|
|
1594
|
+
const start = Date.now();
|
|
1595
|
+
const base = {
|
|
1596
|
+
domain: entry.domain,
|
|
1597
|
+
source: "none",
|
|
1598
|
+
endpoints: 0,
|
|
1599
|
+
resources: 0,
|
|
1600
|
+
hasCredentials: false
|
|
1601
|
+
};
|
|
1602
|
+
if (entry.disabled) {
|
|
1603
|
+
return { ...base, status: "skipped", durationMs: Date.now() - start };
|
|
1604
|
+
}
|
|
1605
|
+
try {
|
|
1606
|
+
if (entry.specUrl) {
|
|
1607
|
+
const result = await fetchAndCompile(entry.specUrl, { domain: entry.domain }, this.fetchFn);
|
|
1608
|
+
await this.store.put(entry.domain, result.record);
|
|
1609
|
+
base.source = "openapi";
|
|
1610
|
+
base.endpoints = result.record.endpoints.length;
|
|
1611
|
+
base.resources = result.resources.length;
|
|
1612
|
+
} else if (entry.skillMdUrl) {
|
|
1613
|
+
const resp = await this.fetchFn(entry.skillMdUrl);
|
|
1614
|
+
if (!resp.ok) throw new Error(`Failed to fetch skill.md: ${resp.status}`);
|
|
1615
|
+
const md = await resp.text();
|
|
1616
|
+
const record = parseSkillMd(entry.domain, md);
|
|
1617
|
+
await this.store.put(entry.domain, record);
|
|
1618
|
+
base.source = "wellknown";
|
|
1619
|
+
base.endpoints = record.endpoints.length;
|
|
1620
|
+
} else if (entry.skillMd) {
|
|
1621
|
+
const record = parseSkillMd(entry.domain, entry.skillMd);
|
|
1622
|
+
await this.store.put(entry.domain, record);
|
|
1623
|
+
base.source = "skillmd";
|
|
1624
|
+
base.endpoints = record.endpoints.length;
|
|
1625
|
+
} else if (entry.rpcDef) {
|
|
1626
|
+
const { record } = compileRpcDef(entry.domain, entry.rpcDef);
|
|
1627
|
+
await this.store.put(entry.domain, record);
|
|
1628
|
+
base.source = "jsonrpc";
|
|
1629
|
+
base.endpoints = record.endpoints.length;
|
|
1630
|
+
base.resources = record.source?.rpc?.resources.length ?? 0;
|
|
1631
|
+
} else {
|
|
1632
|
+
return { ...base, status: "skipped", error: "No spec, skillMdUrl, or skillMd provided", durationMs: Date.now() - start };
|
|
1633
|
+
}
|
|
1634
|
+
if (entry.auth && this.vault) {
|
|
1635
|
+
const auth = resolveAuth(entry.auth);
|
|
1636
|
+
await this.vault.putPool(entry.domain, auth);
|
|
1637
|
+
base.hasCredentials = true;
|
|
1638
|
+
}
|
|
1639
|
+
if (this.smokeTest) {
|
|
1640
|
+
base.smokeTest = await this.runSmokeTest(entry.domain, base.hasCredentials);
|
|
1641
|
+
}
|
|
1642
|
+
return { ...base, status: "ok", durationMs: Date.now() - start };
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
return {
|
|
1645
|
+
...base,
|
|
1646
|
+
status: "failed",
|
|
1647
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1648
|
+
durationMs: Date.now() - start
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
/** Onboard many services with controlled concurrency */
|
|
1653
|
+
async onboardMany(entries) {
|
|
1654
|
+
const start = Date.now();
|
|
1655
|
+
const results = [];
|
|
1656
|
+
let index = 0;
|
|
1657
|
+
for (let i = 0; i < entries.length; i += this.concurrency) {
|
|
1658
|
+
const batch = entries.slice(i, i + this.concurrency);
|
|
1659
|
+
const batchResults = await Promise.all(
|
|
1660
|
+
batch.map(async (entry) => {
|
|
1661
|
+
const result = await this.onboardOne(entry);
|
|
1662
|
+
const idx = index++;
|
|
1663
|
+
this.onProgress?.(result, idx, entries.length);
|
|
1664
|
+
return result;
|
|
1665
|
+
})
|
|
1666
|
+
);
|
|
1667
|
+
results.push(...batchResults);
|
|
1668
|
+
}
|
|
1669
|
+
return {
|
|
1670
|
+
total: results.length,
|
|
1671
|
+
ok: results.filter((r) => r.status === "ok").length,
|
|
1672
|
+
failed: results.filter((r) => r.status === "failed").length,
|
|
1673
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
1674
|
+
results,
|
|
1675
|
+
durationMs: Date.now() - start
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
async runSmokeTest(domain, hasCredentials) {
|
|
1679
|
+
const resolverOpts = this.vault ? { store: this.store, vault: this.vault, wrapVirtualFiles: false } : { store: this.store, wrapVirtualFiles: false };
|
|
1680
|
+
const { onMiss, listDomains } = createRegistryResolver(resolverOpts);
|
|
1681
|
+
const fs = new import_agent_fs2.AgentFs({ mounts: [], onMiss, listDomains });
|
|
1682
|
+
const test = { ls: false, cat: false };
|
|
1683
|
+
const lsResult = await fs.execute(`ls /${domain}/`);
|
|
1684
|
+
test.ls = lsResult.ok === true;
|
|
1685
|
+
if (test.ls && lsResult.ok) {
|
|
1686
|
+
const entries = lsResult.data;
|
|
1687
|
+
const resource = entries.find((e) => e.endsWith("/") && !e.startsWith("_"));
|
|
1688
|
+
if (resource) {
|
|
1689
|
+
const catResult = await fs.execute(`ls /${domain}/${resource}`);
|
|
1690
|
+
test.cat = catResult.ok === true;
|
|
1691
|
+
test.catEndpoint = `ls /${domain}/${resource}`;
|
|
1692
|
+
} else if (entries.includes("_api/")) {
|
|
1693
|
+
const apiResult = await fs.execute(`ls /${domain}/_api/`);
|
|
1694
|
+
test.cat = apiResult.ok === true;
|
|
1695
|
+
test.catEndpoint = `ls /${domain}/_api/`;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
return test;
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
function resolveAuth(auth) {
|
|
1702
|
+
const resolve = (val) => {
|
|
1703
|
+
if (!val) return void 0;
|
|
1704
|
+
const match = val.match(/^\$\{(\w+)\}$/);
|
|
1705
|
+
if (match) {
|
|
1706
|
+
const envVal = process.env[match[1]];
|
|
1707
|
+
if (!envVal) throw new Error(`Environment variable ${match[1]} is not set`);
|
|
1708
|
+
return envVal;
|
|
1709
|
+
}
|
|
1710
|
+
return val;
|
|
1711
|
+
};
|
|
1712
|
+
if (auth.type === "bearer") {
|
|
1713
|
+
const token = resolve(auth.token);
|
|
1714
|
+
if (!token) throw new Error("Bearer auth requires token");
|
|
1715
|
+
return { type: "bearer", token, ...auth.prefix ? { prefix: auth.prefix } : {} };
|
|
1716
|
+
}
|
|
1717
|
+
if (auth.type === "api-key") {
|
|
1718
|
+
const header = resolve(auth.header);
|
|
1719
|
+
const key = resolve(auth.key);
|
|
1720
|
+
if (!header || !key) throw new Error("API key auth requires header and key");
|
|
1721
|
+
return { type: "api-key", header, key };
|
|
1722
|
+
}
|
|
1723
|
+
if (auth.type === "basic") {
|
|
1724
|
+
const username = resolve(auth.username);
|
|
1725
|
+
const password = resolve(auth.password);
|
|
1726
|
+
if (!username || !password) throw new Error("Basic auth requires username and password");
|
|
1727
|
+
return { type: "basic", username, password };
|
|
1728
|
+
}
|
|
1729
|
+
if (auth.type === "oauth2") {
|
|
1730
|
+
const tokenUrl = resolve(auth.tokenUrl);
|
|
1731
|
+
const clientId = resolve(auth.clientId);
|
|
1732
|
+
const clientSecret = resolve(auth.clientSecret);
|
|
1733
|
+
if (!tokenUrl || !clientId || !clientSecret) throw new Error("OAuth2 auth requires tokenUrl, clientId, and clientSecret");
|
|
1734
|
+
return { type: "oauth2", tokenUrl, clientId, clientSecret, ...auth.scope ? { scope: auth.scope } : {} };
|
|
1735
|
+
}
|
|
1736
|
+
throw new Error(`Unknown auth type: ${auth.type}`);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
// src/onboard/apis-guru.ts
|
|
1740
|
+
var APIS_GURU_LIST = "https://api.apis.guru/v2/list.json";
|
|
1741
|
+
async function discoverFromApisGuru(options) {
|
|
1742
|
+
const fetchFn = options?.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
1743
|
+
const limit = options?.limit ?? 100;
|
|
1744
|
+
const filter = options?.filter?.toLowerCase();
|
|
1745
|
+
const resp = await fetchFn(APIS_GURU_LIST);
|
|
1746
|
+
if (!resp.ok) throw new Error(`apis.guru fetch failed: ${resp.status}`);
|
|
1747
|
+
const catalog = await resp.json();
|
|
1748
|
+
const entries = [];
|
|
1749
|
+
for (const [key, api] of Object.entries(catalog)) {
|
|
1750
|
+
if (entries.length >= limit) break;
|
|
1751
|
+
const version = api.versions[api.preferred];
|
|
1752
|
+
if (!version?.swaggerUrl) continue;
|
|
1753
|
+
const title = version.info?.title ?? key;
|
|
1754
|
+
const desc = version.info?.description ?? "";
|
|
1755
|
+
if (filter) {
|
|
1756
|
+
const text = `${key} ${title} ${desc}`.toLowerCase();
|
|
1757
|
+
if (!text.includes(filter)) continue;
|
|
1758
|
+
}
|
|
1759
|
+
const domain = key.split(":")[0];
|
|
1760
|
+
entries.push({
|
|
1761
|
+
domain,
|
|
1762
|
+
specUrl: version.swaggerUrl,
|
|
1763
|
+
tags: ["apis-guru", "public"]
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
return entries;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/onboard/manifest.ts
|
|
1770
|
+
var FREE_APIS = [
|
|
1771
|
+
{
|
|
1772
|
+
domain: "petstore3.swagger.io",
|
|
1773
|
+
specUrl: "https://petstore3.swagger.io/api/v3/openapi.json",
|
|
1774
|
+
tags: ["demo", "free"]
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
domain: "api.weather.gov",
|
|
1778
|
+
specUrl: "https://api.weather.gov/openapi.json",
|
|
1779
|
+
tags: ["weather", "government", "free"]
|
|
1780
|
+
},
|
|
1781
|
+
{
|
|
1782
|
+
domain: "en.wikipedia.org",
|
|
1783
|
+
specUrl: "https://en.wikipedia.org/api/rest_v1/?spec",
|
|
1784
|
+
tags: ["knowledge", "encyclopedia", "free"]
|
|
1785
|
+
}
|
|
1786
|
+
];
|
|
1787
|
+
var FREEMIUM_APIS = [
|
|
1788
|
+
{
|
|
1789
|
+
domain: "api.github.com",
|
|
1790
|
+
specUrl: "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.json",
|
|
1791
|
+
auth: { type: "bearer", token: "${GITHUB_TOKEN}" },
|
|
1792
|
+
tags: ["developer-tools", "vcs", "freemium"]
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
domain: "huggingface.co",
|
|
1796
|
+
specUrl: "https://huggingface.co/.well-known/openapi.json",
|
|
1797
|
+
auth: { type: "bearer", token: "${HF_TOKEN}" },
|
|
1798
|
+
tags: ["ai", "ml", "models", "freemium"]
|
|
1799
|
+
}
|
|
1800
|
+
];
|
|
1801
|
+
var DEVELOPER_TOOL_APIS = [
|
|
1802
|
+
{
|
|
1803
|
+
domain: "gitlab.com",
|
|
1804
|
+
specUrl: "https://gitlab.com/gitlab-org/gitlab/-/raw/master/doc/api/openapi/openapi.yaml",
|
|
1805
|
+
auth: { type: "bearer", token: "${GITLAB_TOKEN}" },
|
|
1806
|
+
tags: ["developer-tools", "vcs"]
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
domain: "api.vercel.com",
|
|
1810
|
+
specUrl: "https://openapi.vercel.sh",
|
|
1811
|
+
auth: { type: "bearer", token: "${VERCEL_TOKEN}" },
|
|
1812
|
+
tags: ["developer-tools", "hosting"]
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
domain: "sentry.io",
|
|
1816
|
+
specUrl: "https://raw.githubusercontent.com/getsentry/sentry-api-schema/main/openapi-derefed.json",
|
|
1817
|
+
auth: { type: "bearer", token: "${SENTRY_AUTH_TOKEN}" },
|
|
1818
|
+
tags: ["developer-tools", "monitoring"]
|
|
1819
|
+
},
|
|
1820
|
+
{
|
|
1821
|
+
domain: "api.pagerduty.com",
|
|
1822
|
+
specUrl: "https://raw.githubusercontent.com/PagerDuty/api-schema/main/reference/REST/openapiv3.json",
|
|
1823
|
+
auth: { type: "bearer", token: "${PAGERDUTY_TOKEN}" },
|
|
1824
|
+
tags: ["developer-tools", "incident-management"]
|
|
1825
|
+
}
|
|
1826
|
+
];
|
|
1827
|
+
var AI_APIS = [
|
|
1828
|
+
{
|
|
1829
|
+
domain: "api.mistral.ai",
|
|
1830
|
+
specUrl: "https://raw.githubusercontent.com/mistralai/platform-docs-public/main/openapi.yaml",
|
|
1831
|
+
auth: { type: "bearer", token: "${MISTRAL_API_KEY}" },
|
|
1832
|
+
tags: ["ai", "llm"],
|
|
1833
|
+
disabled: true
|
|
1834
|
+
// upstream YAML spec has unescaped quotes in example data
|
|
1835
|
+
},
|
|
1836
|
+
{
|
|
1837
|
+
domain: "api.openai.com",
|
|
1838
|
+
specUrl: "https://raw.githubusercontent.com/openai/openai-openapi/manual_spec/openapi.yaml",
|
|
1839
|
+
auth: { type: "bearer", token: "${OPENAI_API_KEY}" },
|
|
1840
|
+
tags: ["ai", "llm"]
|
|
1841
|
+
},
|
|
1842
|
+
{
|
|
1843
|
+
domain: "openrouter.ai",
|
|
1844
|
+
specUrl: "https://openrouter.ai/openapi.json",
|
|
1845
|
+
auth: { type: "bearer", token: "${OPENROUTER_API_KEY}" },
|
|
1846
|
+
tags: ["ai", "llm", "gateway"]
|
|
1847
|
+
}
|
|
1848
|
+
];
|
|
1849
|
+
var CLOUD_APIS = [
|
|
1850
|
+
{
|
|
1851
|
+
domain: "api.cloudflare.com",
|
|
1852
|
+
specUrl: "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.yaml",
|
|
1853
|
+
auth: { type: "bearer", token: "${CLOUDFLARE_API_TOKEN}" },
|
|
1854
|
+
tags: ["cloud", "cdn", "dns"]
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
domain: "api.digitalocean.com",
|
|
1858
|
+
specUrl: "https://raw.githubusercontent.com/digitalocean/openapi/main/specification/DigitalOcean-public.v2.yaml",
|
|
1859
|
+
auth: { type: "bearer", token: "${DIGITALOCEAN_TOKEN}" },
|
|
1860
|
+
tags: ["cloud", "infrastructure"]
|
|
1861
|
+
},
|
|
1862
|
+
{
|
|
1863
|
+
domain: "fly.io",
|
|
1864
|
+
specUrl: "https://docs.machines.dev/spec/openapi3.json",
|
|
1865
|
+
auth: { type: "bearer", token: "${FLY_API_TOKEN}" },
|
|
1866
|
+
tags: ["cloud", "deployment"]
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
domain: "api.render.com",
|
|
1870
|
+
specUrl: "https://api-docs.render.com/v1.0/openapi/render-public-api-1.json",
|
|
1871
|
+
auth: { type: "bearer", token: "${RENDER_API_KEY}" },
|
|
1872
|
+
tags: ["cloud", "deployment"]
|
|
1873
|
+
}
|
|
1874
|
+
];
|
|
1875
|
+
var PRODUCTIVITY_APIS = [
|
|
1876
|
+
{
|
|
1877
|
+
domain: "api.notion.com",
|
|
1878
|
+
specUrl: "https://raw.githubusercontent.com/makenotion/notion-mcp-server/main/scripts/notion-openapi.json",
|
|
1879
|
+
auth: { type: "bearer", token: "${NOTION_API_KEY}" },
|
|
1880
|
+
tags: ["productivity", "database"]
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
domain: "app.asana.com",
|
|
1884
|
+
specUrl: "https://raw.githubusercontent.com/Asana/openapi/master/defs/asana_oas.yaml",
|
|
1885
|
+
auth: { type: "bearer", token: "${ASANA_ACCESS_TOKEN}" },
|
|
1886
|
+
tags: ["productivity", "project-management"]
|
|
1887
|
+
},
|
|
1888
|
+
{
|
|
1889
|
+
domain: "jira.atlassian.com",
|
|
1890
|
+
specUrl: "https://developer.atlassian.com/cloud/jira/platform/swagger-v3.v3.json",
|
|
1891
|
+
auth: { type: "bearer", token: "${ATLASSIAN_API_TOKEN}" },
|
|
1892
|
+
tags: ["productivity", "project-management"]
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
domain: "api.spotify.com",
|
|
1896
|
+
specUrl: "https://raw.githubusercontent.com/sonallux/spotify-web-api/main/fixed-spotify-open-api.yml",
|
|
1897
|
+
auth: { type: "bearer", token: "${SPOTIFY_ACCESS_TOKEN}" },
|
|
1898
|
+
tags: ["media", "music"]
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
domain: "api.getpostman.com",
|
|
1902
|
+
specUrl: "https://api.apis.guru/v2/specs/getpostman.com/1.20.0/openapi.json",
|
|
1903
|
+
auth: { type: "api-key", header: "X-Api-Key", key: "${POSTMAN_API_KEY}" },
|
|
1904
|
+
tags: ["developer-tools", "api-testing"]
|
|
1905
|
+
}
|
|
1906
|
+
];
|
|
1907
|
+
var DEVOPS_APIS = [
|
|
1908
|
+
{
|
|
1909
|
+
domain: "circleci.com",
|
|
1910
|
+
specUrl: "https://circleci.com/api/v2/openapi.json",
|
|
1911
|
+
auth: { type: "bearer", token: "${CIRCLECI_TOKEN}" },
|
|
1912
|
+
tags: ["devops", "ci-cd"]
|
|
1913
|
+
},
|
|
1914
|
+
{
|
|
1915
|
+
domain: "api.datadoghq.com",
|
|
1916
|
+
specUrl: "https://raw.githubusercontent.com/DataDog/datadog-api-client-python/master/.generator/schemas/v2/openapi.yaml",
|
|
1917
|
+
auth: { type: "api-key", header: "DD-API-KEY", key: "${DATADOG_API_KEY}" },
|
|
1918
|
+
tags: ["devops", "monitoring"]
|
|
1919
|
+
}
|
|
1920
|
+
];
|
|
1921
|
+
var DATABASE_APIS = [
|
|
1922
|
+
{
|
|
1923
|
+
domain: "api.supabase.com",
|
|
1924
|
+
specUrl: "https://raw.githubusercontent.com/supabase/supabase/master/apps/docs/spec/api_v1_openapi.json",
|
|
1925
|
+
auth: { type: "bearer", token: "${SUPABASE_ACCESS_TOKEN}" },
|
|
1926
|
+
tags: ["database", "baas"]
|
|
1927
|
+
},
|
|
1928
|
+
{
|
|
1929
|
+
domain: "api.turso.tech",
|
|
1930
|
+
specUrl: "https://raw.githubusercontent.com/tursodatabase/turso-docs/main/api-reference/openapi.json",
|
|
1931
|
+
auth: { type: "bearer", token: "${TURSO_API_TOKEN}" },
|
|
1932
|
+
tags: ["database", "edge"]
|
|
1933
|
+
},
|
|
1934
|
+
{
|
|
1935
|
+
domain: "console.neon.tech",
|
|
1936
|
+
specUrl: "https://raw.githubusercontent.com/neondatabase/neon-api-python/main/v2.json",
|
|
1937
|
+
auth: { type: "bearer", token: "${NEON_API_KEY}" },
|
|
1938
|
+
tags: ["database", "serverless-postgres"]
|
|
1939
|
+
}
|
|
1940
|
+
];
|
|
1941
|
+
var COMMERCE_APIS = [
|
|
1942
|
+
{
|
|
1943
|
+
domain: "api.stripe.com",
|
|
1944
|
+
specUrl: "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json",
|
|
1945
|
+
auth: { type: "bearer", token: "${STRIPE_SECRET_KEY}" },
|
|
1946
|
+
tags: ["commerce", "payments"]
|
|
1947
|
+
}
|
|
1948
|
+
];
|
|
1949
|
+
var COMMUNICATION_APIS = [
|
|
1950
|
+
{
|
|
1951
|
+
domain: "slack.com",
|
|
1952
|
+
specUrl: "https://raw.githubusercontent.com/slackapi/slack-api-specs/master/web-api/slack_web_openapi_v2.json",
|
|
1953
|
+
auth: { type: "bearer", token: "${SLACK_BOT_TOKEN}" },
|
|
1954
|
+
tags: ["communication", "messaging"]
|
|
1955
|
+
},
|
|
1956
|
+
{
|
|
1957
|
+
domain: "discord.com",
|
|
1958
|
+
specUrl: "https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json",
|
|
1959
|
+
auth: { type: "bearer", token: "${DISCORD_BOT_TOKEN}", prefix: "Bot" },
|
|
1960
|
+
tags: ["communication", "messaging"]
|
|
1961
|
+
},
|
|
1962
|
+
{
|
|
1963
|
+
domain: "api.twilio.com",
|
|
1964
|
+
specUrl: "https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json",
|
|
1965
|
+
auth: { type: "basic", token: "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}" },
|
|
1966
|
+
tags: ["communication", "sms", "voice"]
|
|
1967
|
+
},
|
|
1968
|
+
{
|
|
1969
|
+
domain: "api.resend.com",
|
|
1970
|
+
specUrl: "https://raw.githubusercontent.com/resendlabs/resend-openapi/main/resend.yaml",
|
|
1971
|
+
auth: { type: "bearer", token: "${RESEND_API_KEY}" },
|
|
1972
|
+
tags: ["communication", "email"]
|
|
1973
|
+
}
|
|
1974
|
+
];
|
|
1975
|
+
var EVM_METHODS = [
|
|
1976
|
+
{ rpcMethod: "eth_blockNumber", description: "Returns the latest block number", resource: "blocks", fsOp: "list" },
|
|
1977
|
+
{ rpcMethod: "eth_getBlockByNumber", description: "Returns block by number", resource: "blocks", fsOp: "read" },
|
|
1978
|
+
{ rpcMethod: "eth_getBalance", description: "Returns account balance in wei", resource: "balances", fsOp: "read" },
|
|
1979
|
+
{ rpcMethod: "eth_getTransactionByHash", description: "Returns transaction by hash", resource: "transactions", fsOp: "read" },
|
|
1980
|
+
{ rpcMethod: "eth_getTransactionReceipt", description: "Returns transaction receipt", resource: "receipts", fsOp: "read" },
|
|
1981
|
+
{ rpcMethod: "eth_call", description: "Executes a call without creating a transaction", resource: "calls", fsOp: "read" },
|
|
1982
|
+
{ rpcMethod: "eth_estimateGas", description: "Estimates gas needed for a transaction", resource: "gas", fsOp: "read" },
|
|
1983
|
+
{ rpcMethod: "eth_gasPrice", description: "Returns current gas price in wei" },
|
|
1984
|
+
{ rpcMethod: "eth_chainId", description: "Returns the chain ID" },
|
|
1985
|
+
{ rpcMethod: "eth_getCode", description: "Returns contract bytecode at address", resource: "code", fsOp: "read" },
|
|
1986
|
+
{ rpcMethod: "eth_getLogs", description: "Returns logs matching a filter", resource: "logs", fsOp: "list" },
|
|
1987
|
+
{ rpcMethod: "eth_getTransactionCount", description: "Returns the number of transactions sent from an address", resource: "nonces", fsOp: "read" },
|
|
1988
|
+
{ rpcMethod: "net_version", description: "Returns the network ID" }
|
|
1989
|
+
];
|
|
1990
|
+
var RPC_APIS = [
|
|
1991
|
+
// ── Free / Public RPC Providers ──────────────────────────────────
|
|
1992
|
+
{
|
|
1993
|
+
domain: "rpc.ankr.com",
|
|
1994
|
+
rpcDef: { url: "https://rpc.ankr.com/eth", convention: "evm", methods: EVM_METHODS },
|
|
1995
|
+
tags: ["blockchain", "ethereum", "free"]
|
|
1996
|
+
},
|
|
1997
|
+
{
|
|
1998
|
+
domain: "cloudflare-eth.com",
|
|
1999
|
+
rpcDef: { url: "https://cloudflare-eth.com", convention: "evm", methods: EVM_METHODS },
|
|
2000
|
+
tags: ["blockchain", "ethereum", "free"]
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
domain: "ethereum-rpc.publicnode.com",
|
|
2004
|
+
rpcDef: { url: "https://ethereum-rpc.publicnode.com", convention: "evm", methods: EVM_METHODS },
|
|
2005
|
+
tags: ["blockchain", "ethereum", "free"]
|
|
2006
|
+
},
|
|
2007
|
+
// ── Auth-Required RPC Providers ──────────────────────────────────
|
|
2008
|
+
{
|
|
2009
|
+
domain: "eth-mainnet.g.alchemy.com",
|
|
2010
|
+
rpcDef: { url: "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}", convention: "evm", methods: EVM_METHODS },
|
|
2011
|
+
tags: ["blockchain", "ethereum"]
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
domain: "mainnet.infura.io",
|
|
2015
|
+
rpcDef: { url: "https://mainnet.infura.io/v3/${INFURA_API_KEY}", convention: "evm", methods: EVM_METHODS },
|
|
2016
|
+
tags: ["blockchain", "ethereum"]
|
|
2017
|
+
},
|
|
2018
|
+
// ── L2 / Alt Chains (Free) ───────────────────────────────────────
|
|
2019
|
+
{
|
|
2020
|
+
domain: "arb1.arbitrum.io",
|
|
2021
|
+
rpcDef: { url: "https://arb1.arbitrum.io/rpc", convention: "evm", methods: EVM_METHODS },
|
|
2022
|
+
tags: ["blockchain", "arbitrum", "l2", "free"]
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
domain: "mainnet.optimism.io",
|
|
2026
|
+
rpcDef: { url: "https://mainnet.optimism.io", convention: "evm", methods: EVM_METHODS },
|
|
2027
|
+
tags: ["blockchain", "optimism", "l2", "free"]
|
|
2028
|
+
},
|
|
2029
|
+
{
|
|
2030
|
+
domain: "mainnet.base.org",
|
|
2031
|
+
rpcDef: { url: "https://mainnet.base.org", convention: "evm", methods: EVM_METHODS },
|
|
2032
|
+
tags: ["blockchain", "base", "l2", "free"]
|
|
2033
|
+
},
|
|
2034
|
+
{
|
|
2035
|
+
domain: "polygon-rpc.com",
|
|
2036
|
+
rpcDef: { url: "https://polygon-rpc.com", convention: "evm", methods: EVM_METHODS },
|
|
2037
|
+
tags: ["blockchain", "polygon", "free"]
|
|
2038
|
+
}
|
|
2039
|
+
];
|
|
2040
|
+
var ALL_APIS = [
|
|
2041
|
+
...FREE_APIS,
|
|
2042
|
+
...FREEMIUM_APIS,
|
|
2043
|
+
...DEVELOPER_TOOL_APIS,
|
|
2044
|
+
...AI_APIS,
|
|
2045
|
+
...CLOUD_APIS,
|
|
2046
|
+
...PRODUCTIVITY_APIS,
|
|
2047
|
+
...DEVOPS_APIS,
|
|
2048
|
+
...DATABASE_APIS,
|
|
2049
|
+
...COMMERCE_APIS,
|
|
2050
|
+
...COMMUNICATION_APIS,
|
|
2051
|
+
...RPC_APIS
|
|
2052
|
+
];
|
|
2053
|
+
|
|
2054
|
+
// src/d1/sqlite-adapter.ts
|
|
2055
|
+
var BoundStatement = class {
|
|
2056
|
+
constructor(db, sql) {
|
|
2057
|
+
this.db = db;
|
|
2058
|
+
this.sql = sql;
|
|
2059
|
+
}
|
|
2060
|
+
params = [];
|
|
2061
|
+
bind(...values) {
|
|
2062
|
+
this.params = values;
|
|
2063
|
+
return this;
|
|
2064
|
+
}
|
|
2065
|
+
async first() {
|
|
2066
|
+
const stmt = this.db.prepare(this.sql);
|
|
2067
|
+
const row = stmt.get(...this.params);
|
|
2068
|
+
return row ?? null;
|
|
2069
|
+
}
|
|
2070
|
+
async all() {
|
|
2071
|
+
const stmt = this.db.prepare(this.sql);
|
|
2072
|
+
const rows = stmt.all(...this.params);
|
|
2073
|
+
return { results: rows, success: true };
|
|
2074
|
+
}
|
|
2075
|
+
async run() {
|
|
2076
|
+
const stmt = this.db.prepare(this.sql);
|
|
2077
|
+
const info = stmt.run(...this.params);
|
|
2078
|
+
return {
|
|
2079
|
+
success: true,
|
|
2080
|
+
changes: info.changes,
|
|
2081
|
+
lastRowId: Number(info.lastInsertRowid)
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
function createSqliteD1(db) {
|
|
2086
|
+
return {
|
|
2087
|
+
prepare(sql) {
|
|
2088
|
+
return new BoundStatement(db, sql);
|
|
2089
|
+
},
|
|
2090
|
+
async exec(sql) {
|
|
2091
|
+
db.exec(sql);
|
|
2092
|
+
}
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/federation/d1-peer-store.ts
|
|
2097
|
+
function rowToPeer(row) {
|
|
2098
|
+
return {
|
|
2099
|
+
id: row.id,
|
|
2100
|
+
name: row.name,
|
|
2101
|
+
url: row.url,
|
|
2102
|
+
sharedSecret: row.shared_secret,
|
|
2103
|
+
status: row.status,
|
|
2104
|
+
advertisedDomains: JSON.parse(row.advertised_domains),
|
|
2105
|
+
lastSeen: row.last_seen,
|
|
2106
|
+
createdAt: row.created_at
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
function rowToRule(row) {
|
|
2110
|
+
return {
|
|
2111
|
+
domain: row.domain,
|
|
2112
|
+
allow: row.allow === 1,
|
|
2113
|
+
peers: JSON.parse(row.peers),
|
|
2114
|
+
pricing: JSON.parse(row.pricing),
|
|
2115
|
+
...row.rate_limit ? { rateLimit: JSON.parse(row.rate_limit) } : {},
|
|
2116
|
+
createdAt: row.created_at,
|
|
2117
|
+
updatedAt: row.updated_at
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
var D1PeerStore = class {
|
|
2121
|
+
constructor(db) {
|
|
2122
|
+
this.db = db;
|
|
2123
|
+
}
|
|
2124
|
+
async initSchema() {
|
|
2125
|
+
await this.db.exec(`
|
|
2126
|
+
CREATE TABLE IF NOT EXISTS peers (
|
|
2127
|
+
id TEXT PRIMARY KEY,
|
|
2128
|
+
name TEXT NOT NULL,
|
|
2129
|
+
url TEXT NOT NULL,
|
|
2130
|
+
shared_secret TEXT NOT NULL,
|
|
2131
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
2132
|
+
advertised_domains TEXT NOT NULL DEFAULT '[]',
|
|
2133
|
+
last_seen INTEGER NOT NULL,
|
|
2134
|
+
created_at INTEGER NOT NULL
|
|
2135
|
+
)`);
|
|
2136
|
+
await this.db.exec(`
|
|
2137
|
+
CREATE TABLE IF NOT EXISTS lending_rules (
|
|
2138
|
+
domain TEXT PRIMARY KEY,
|
|
2139
|
+
allow INTEGER NOT NULL DEFAULT 1,
|
|
2140
|
+
peers TEXT NOT NULL DEFAULT '"*"',
|
|
2141
|
+
pricing TEXT NOT NULL DEFAULT '{"mode":"free"}',
|
|
2142
|
+
rate_limit TEXT,
|
|
2143
|
+
created_at INTEGER NOT NULL,
|
|
2144
|
+
updated_at INTEGER NOT NULL
|
|
2145
|
+
)`);
|
|
2146
|
+
}
|
|
2147
|
+
async getPeer(id) {
|
|
2148
|
+
const row = await this.db.prepare("SELECT * FROM peers WHERE id = ?").bind(id).first();
|
|
2149
|
+
return row ? rowToPeer(row) : null;
|
|
2150
|
+
}
|
|
2151
|
+
async putPeer(peer) {
|
|
2152
|
+
await this.db.prepare(
|
|
2153
|
+
`INSERT OR REPLACE INTO peers (id, name, url, shared_secret, status, advertised_domains, last_seen, created_at)
|
|
2154
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2155
|
+
).bind(
|
|
2156
|
+
peer.id,
|
|
2157
|
+
peer.name,
|
|
2158
|
+
peer.url,
|
|
2159
|
+
peer.sharedSecret,
|
|
2160
|
+
peer.status,
|
|
2161
|
+
JSON.stringify(peer.advertisedDomains),
|
|
2162
|
+
peer.lastSeen,
|
|
2163
|
+
peer.createdAt
|
|
2164
|
+
).run();
|
|
2165
|
+
}
|
|
2166
|
+
async deletePeer(id) {
|
|
2167
|
+
await this.db.prepare("DELETE FROM peers WHERE id = ?").bind(id).run();
|
|
2168
|
+
}
|
|
2169
|
+
async listPeers() {
|
|
2170
|
+
const { results } = await this.db.prepare("SELECT * FROM peers WHERE status = 'active'").all();
|
|
2171
|
+
return results.map(rowToPeer);
|
|
2172
|
+
}
|
|
2173
|
+
async updateLastSeen(id, timestamp) {
|
|
2174
|
+
await this.db.prepare("UPDATE peers SET last_seen = ? WHERE id = ?").bind(timestamp, id).run();
|
|
2175
|
+
}
|
|
2176
|
+
async getRule(domain) {
|
|
2177
|
+
const row = await this.db.prepare("SELECT * FROM lending_rules WHERE domain = ?").bind(domain).first();
|
|
2178
|
+
return row ? rowToRule(row) : null;
|
|
2179
|
+
}
|
|
2180
|
+
async putRule(rule) {
|
|
2181
|
+
await this.db.prepare(
|
|
2182
|
+
`INSERT OR REPLACE INTO lending_rules (domain, allow, peers, pricing, rate_limit, created_at, updated_at)
|
|
2183
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
2184
|
+
).bind(
|
|
2185
|
+
rule.domain,
|
|
2186
|
+
rule.allow ? 1 : 0,
|
|
2187
|
+
JSON.stringify(rule.peers),
|
|
2188
|
+
JSON.stringify(rule.pricing),
|
|
2189
|
+
rule.rateLimit ? JSON.stringify(rule.rateLimit) : null,
|
|
2190
|
+
rule.createdAt,
|
|
2191
|
+
rule.updatedAt
|
|
2192
|
+
).run();
|
|
2193
|
+
}
|
|
2194
|
+
async deleteRule(domain) {
|
|
2195
|
+
await this.db.prepare("DELETE FROM lending_rules WHERE domain = ?").bind(domain).run();
|
|
2196
|
+
}
|
|
2197
|
+
async listRules() {
|
|
2198
|
+
const { results } = await this.db.prepare("SELECT * FROM lending_rules").all();
|
|
2199
|
+
return results.map(rowToRule);
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
// src/tunnel/memory-store.ts
|
|
2204
|
+
var MemoryTunnelStore = class {
|
|
2205
|
+
records = /* @__PURE__ */ new Map();
|
|
2206
|
+
async get(id) {
|
|
2207
|
+
return this.records.get(id) ?? null;
|
|
2208
|
+
}
|
|
2209
|
+
async getByAgent(agentId) {
|
|
2210
|
+
for (const record of this.records.values()) {
|
|
2211
|
+
if (record.agentId === agentId && record.status === "active") {
|
|
2212
|
+
return record;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return null;
|
|
2216
|
+
}
|
|
2217
|
+
async put(record) {
|
|
2218
|
+
this.records.set(record.id, record);
|
|
2219
|
+
}
|
|
2220
|
+
async delete(id) {
|
|
2221
|
+
this.records.delete(id);
|
|
2222
|
+
}
|
|
2223
|
+
async list() {
|
|
2224
|
+
return Array.from(this.records.values());
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
// src/tunnel/cloudflare-provider.ts
|
|
2229
|
+
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
2230
|
+
var CloudflareTunnelProvider = class {
|
|
2231
|
+
constructor(accountId, apiToken, tunnelDomain, zoneId) {
|
|
2232
|
+
this.accountId = accountId;
|
|
2233
|
+
this.apiToken = apiToken;
|
|
2234
|
+
this.tunnelDomain = tunnelDomain;
|
|
2235
|
+
this.zoneId = zoneId;
|
|
2236
|
+
}
|
|
2237
|
+
async create(name, hostname) {
|
|
2238
|
+
const secretBytes = new Uint8Array(32);
|
|
2239
|
+
crypto.getRandomValues(secretBytes);
|
|
2240
|
+
const tunnelSecret = btoa(String.fromCharCode(...secretBytes));
|
|
2241
|
+
const tunnelRes = await this.cfFetch(
|
|
2242
|
+
`/accounts/${this.accountId}/cfd_tunnel`,
|
|
2243
|
+
{
|
|
2244
|
+
method: "POST",
|
|
2245
|
+
body: JSON.stringify({
|
|
2246
|
+
name,
|
|
2247
|
+
tunnel_secret: tunnelSecret
|
|
2248
|
+
})
|
|
2249
|
+
}
|
|
2250
|
+
);
|
|
2251
|
+
const tunnelId = tunnelRes.id;
|
|
2252
|
+
try {
|
|
2253
|
+
await this.cfFetch(`/zones/${this.zoneId}/dns_records`, {
|
|
2254
|
+
method: "POST",
|
|
2255
|
+
body: JSON.stringify({
|
|
2256
|
+
type: "CNAME",
|
|
2257
|
+
name: hostname,
|
|
2258
|
+
content: `${tunnelId}.cfargotunnel.com`,
|
|
2259
|
+
proxied: true
|
|
2260
|
+
})
|
|
2261
|
+
});
|
|
2262
|
+
await this.cfFetch(
|
|
2263
|
+
`/accounts/${this.accountId}/cfd_tunnel/${tunnelId}/configurations`,
|
|
2264
|
+
{
|
|
2265
|
+
method: "PUT",
|
|
2266
|
+
body: JSON.stringify({
|
|
2267
|
+
config: {
|
|
2268
|
+
ingress: [
|
|
2269
|
+
{ hostname, service: "http://localhost:9090" },
|
|
2270
|
+
{ service: "http_status:404" }
|
|
2271
|
+
]
|
|
2272
|
+
}
|
|
2273
|
+
})
|
|
2274
|
+
}
|
|
2275
|
+
);
|
|
2276
|
+
const tokenRes = await this.cfFetch(
|
|
2277
|
+
`/accounts/${this.accountId}/cfd_tunnel/${tunnelId}/token`
|
|
2278
|
+
);
|
|
2279
|
+
return { tunnelId, tunnelToken: tokenRes };
|
|
2280
|
+
} catch (err) {
|
|
2281
|
+
await this.cfFetch(
|
|
2282
|
+
`/accounts/${this.accountId}/cfd_tunnel/${tunnelId}?cascade=true`,
|
|
2283
|
+
{ method: "DELETE" }
|
|
2284
|
+
).catch(() => {
|
|
2285
|
+
});
|
|
2286
|
+
throw err;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
async delete(tunnelId) {
|
|
2290
|
+
const dnsRecords = await this.cfFetch(
|
|
2291
|
+
`/zones/${this.zoneId}/dns_records?type=CNAME&content=${tunnelId}.cfargotunnel.com`
|
|
2292
|
+
);
|
|
2293
|
+
for (const record of dnsRecords) {
|
|
2294
|
+
await this.cfFetch(`/zones/${this.zoneId}/dns_records/${record.id}`, {
|
|
2295
|
+
method: "DELETE"
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
await this.cfFetch(
|
|
2299
|
+
`/accounts/${this.accountId}/cfd_tunnel/${tunnelId}?cascade=true`,
|
|
2300
|
+
{ method: "DELETE" }
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
async cfFetch(path, init) {
|
|
2304
|
+
const res = await fetch(`${CF_API}${path}`, {
|
|
2305
|
+
...init,
|
|
2306
|
+
headers: {
|
|
2307
|
+
Authorization: `Bearer ${this.apiToken}`,
|
|
2308
|
+
"Content-Type": "application/json",
|
|
2309
|
+
...init?.headers
|
|
2310
|
+
}
|
|
2311
|
+
});
|
|
2312
|
+
const data = await res.json();
|
|
2313
|
+
if (!data.success) {
|
|
2314
|
+
const msg = data.errors.map((e) => e.message).join(", ");
|
|
2315
|
+
throw new Error(`Cloudflare API error: ${msg}`);
|
|
2316
|
+
}
|
|
2317
|
+
return data.result;
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
// src/http/routes/tunnels.ts
|
|
2322
|
+
var import_hono2 = require("hono");
|
|
2323
|
+
var import_nanoid = require("nanoid");
|
|
2324
|
+
function tunnelRoutes(options) {
|
|
2325
|
+
const { tunnelStore, tunnelProvider, tunnelDomain } = options;
|
|
2326
|
+
const app = new import_hono2.Hono();
|
|
2327
|
+
app.post("/create", async (c) => {
|
|
2328
|
+
const agent = c.get("agent");
|
|
2329
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2330
|
+
const existing = await tunnelStore.getByAgent(agent.id);
|
|
2331
|
+
if (existing && existing.status === "active") {
|
|
2332
|
+
return c.json({
|
|
2333
|
+
tunnelId: existing.id,
|
|
2334
|
+
publicUrl: existing.publicUrl,
|
|
2335
|
+
message: "Tunnel already exists"
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
const id = (0, import_nanoid.nanoid)(12);
|
|
2339
|
+
const hostname = `${id}.${tunnelDomain}`;
|
|
2340
|
+
const publicUrl = `https://${hostname}`;
|
|
2341
|
+
const { tunnelId, tunnelToken } = await tunnelProvider.create(
|
|
2342
|
+
`nkmc-${agent.id}-${id}`,
|
|
2343
|
+
hostname
|
|
2344
|
+
);
|
|
2345
|
+
const now = Date.now();
|
|
2346
|
+
await tunnelStore.put({
|
|
2347
|
+
id,
|
|
2348
|
+
agentId: agent.id,
|
|
2349
|
+
tunnelId,
|
|
2350
|
+
publicUrl,
|
|
2351
|
+
status: "active",
|
|
2352
|
+
createdAt: now,
|
|
2353
|
+
advertisedDomains: body.advertisedDomains ?? [],
|
|
2354
|
+
gatewayName: body.gatewayName,
|
|
2355
|
+
lastSeen: now
|
|
2356
|
+
});
|
|
2357
|
+
return c.json({ tunnelId: id, tunnelToken, publicUrl }, 201);
|
|
2358
|
+
});
|
|
2359
|
+
app.delete("/:id", async (c) => {
|
|
2360
|
+
const id = c.req.param("id");
|
|
2361
|
+
const agent = c.get("agent");
|
|
2362
|
+
const record = await tunnelStore.get(id);
|
|
2363
|
+
if (!record) return c.json({ error: "Tunnel not found" }, 404);
|
|
2364
|
+
if (record.agentId !== agent.id)
|
|
2365
|
+
return c.json({ error: "Not your tunnel" }, 403);
|
|
2366
|
+
await tunnelProvider.delete(record.tunnelId);
|
|
2367
|
+
await tunnelStore.delete(id);
|
|
2368
|
+
return c.json({ ok: true });
|
|
2369
|
+
});
|
|
2370
|
+
app.get("/", async (c) => {
|
|
2371
|
+
const agent = c.get("agent");
|
|
2372
|
+
const all = await tunnelStore.list();
|
|
2373
|
+
const mine = all.filter((t) => t.agentId === agent.id);
|
|
2374
|
+
return c.json({ tunnels: mine });
|
|
2375
|
+
});
|
|
2376
|
+
app.get("/discover", async (c) => {
|
|
2377
|
+
const domain = c.req.query("domain");
|
|
2378
|
+
const all = await tunnelStore.list();
|
|
2379
|
+
let results = all.filter((t) => t.status === "active");
|
|
2380
|
+
if (domain) {
|
|
2381
|
+
results = results.filter((t) => t.advertisedDomains.includes(domain));
|
|
2382
|
+
}
|
|
2383
|
+
return c.json({
|
|
2384
|
+
gateways: results.map((t) => ({
|
|
2385
|
+
id: t.id,
|
|
2386
|
+
name: t.gatewayName ?? `gateway-${t.id}`,
|
|
2387
|
+
publicUrl: t.publicUrl,
|
|
2388
|
+
advertisedDomains: t.advertisedDomains
|
|
2389
|
+
}))
|
|
2390
|
+
});
|
|
2391
|
+
});
|
|
2392
|
+
app.post("/heartbeat", async (c) => {
|
|
2393
|
+
const agent = c.get("agent");
|
|
2394
|
+
const body = await c.req.json();
|
|
2395
|
+
const record = await tunnelStore.getByAgent(agent.id);
|
|
2396
|
+
if (!record) return c.json({ error: "No active tunnel" }, 404);
|
|
2397
|
+
record.advertisedDomains = body.advertisedDomains ?? record.advertisedDomains;
|
|
2398
|
+
record.lastSeen = Date.now();
|
|
2399
|
+
await tunnelStore.put(record);
|
|
2400
|
+
return c.json({ ok: true });
|
|
2401
|
+
});
|
|
2402
|
+
return app;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
// src/index.ts
|
|
2406
|
+
var VERSION = "0.1.0";
|
|
2407
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2408
|
+
0 && (module.exports = {
|
|
2409
|
+
CloudflareTunnelProvider,
|
|
2410
|
+
Context7Backend,
|
|
2411
|
+
Context7Client,
|
|
2412
|
+
D1CredentialVault,
|
|
2413
|
+
D1MeterStore,
|
|
2414
|
+
D1PeerStore,
|
|
2415
|
+
D1RegistryStore,
|
|
2416
|
+
MemoryCredentialVault,
|
|
2417
|
+
MemoryMeterStore,
|
|
2418
|
+
MemoryRegistryStore,
|
|
2419
|
+
MemoryTunnelStore,
|
|
2420
|
+
OnboardPipeline,
|
|
2421
|
+
VERSION,
|
|
2422
|
+
VirtualFileBackend,
|
|
2423
|
+
checkAccess,
|
|
2424
|
+
createRegistryResolver,
|
|
2425
|
+
createSqliteD1,
|
|
2426
|
+
credentialRoutes,
|
|
2427
|
+
discoverFromApisGuru,
|
|
2428
|
+
extractDomainPath,
|
|
2429
|
+
lookupPricing,
|
|
2430
|
+
meter,
|
|
2431
|
+
parsePricingAnnotation,
|
|
2432
|
+
parseSkillMd,
|
|
2433
|
+
queryDnsTxt,
|
|
2434
|
+
skillToHttpConfig,
|
|
2435
|
+
tunnelRoutes
|
|
2436
|
+
});
|