@nkmc/gateway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-56RA53VS.js +37 -0
- package/dist/chunk-CZJ75YTV.js +969 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/http.cjs +1772 -0
- package/dist/http.d.cts +49 -0
- package/dist/http.d.ts +49 -0
- package/dist/http.js +748 -0
- package/dist/index.cjs +2436 -0
- package/dist/index.d.cts +436 -0
- package/dist/index.d.ts +436 -0
- package/dist/index.js +1434 -0
- package/dist/proxy-ClPcDgsO.d.cts +283 -0
- package/dist/proxy-qpda1ANS.d.ts +283 -0
- package/dist/proxy.cjs +148 -0
- package/dist/proxy.d.cts +6 -0
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +90 -0
- package/dist/testing.cjs +865 -0
- package/dist/testing.d.cts +12 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +831 -0
- package/dist/tunnels-BviBEaih.d.cts +12 -0
- package/dist/tunnels-DFHNgmN7.d.ts +12 -0
- package/dist/types-C6JC9oTm.d.cts +21 -0
- package/dist/types-C6JC9oTm.d.ts +21 -0
- package/package.json +47 -0
- package/src/__tests__/sqlite-integration.test.ts +384 -0
- package/src/credential/d1-vault.ts +134 -0
- package/src/credential/memory-vault.ts +50 -0
- package/src/credential/types.ts +16 -0
- package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
- package/src/d1/sqlite-adapter.ts +59 -0
- package/src/d1/types.ts +22 -0
- package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
- package/src/federation/__tests__/peer-client.test.ts +205 -0
- package/src/federation/__tests__/peer-store.test.ts +114 -0
- package/src/federation/d1-peer-store.ts +164 -0
- package/src/federation/peer-backend.ts +60 -0
- package/src/federation/peer-client.ts +122 -0
- package/src/federation/peer-store.ts +45 -0
- package/src/federation/types.ts +39 -0
- package/src/http/app.ts +152 -0
- package/src/http/lib/dns.ts +30 -0
- package/src/http/middleware/admin-auth.ts +18 -0
- package/src/http/middleware/agent-auth.ts +27 -0
- package/src/http/middleware/publish-auth.ts +39 -0
- package/src/http/routes/__tests__/federation.test.ts +364 -0
- package/src/http/routes/__tests__/peers.test.ts +290 -0
- package/src/http/routes/__tests__/proxy.test.ts +159 -0
- package/src/http/routes/auth.ts +39 -0
- package/src/http/routes/byok.ts +62 -0
- package/src/http/routes/credentials.ts +40 -0
- package/src/http/routes/domains.ts +174 -0
- package/src/http/routes/federation.ts +170 -0
- package/src/http/routes/fs.ts +89 -0
- package/src/http/routes/peers.ts +103 -0
- package/src/http/routes/proxy.ts +57 -0
- package/src/http/routes/registry.ts +222 -0
- package/src/http/routes/tunnels.ts +124 -0
- package/src/http.ts +9 -0
- package/src/index.ts +63 -0
- package/src/metering/d1-store.ts +123 -0
- package/src/metering/memory-store.ts +29 -0
- package/src/metering/pricing-guard.ts +68 -0
- package/src/metering/types.ts +25 -0
- package/src/onboard/apis-guru.ts +64 -0
- package/src/onboard/index.ts +4 -0
- package/src/onboard/manifest.ts +362 -0
- package/src/onboard/pipeline.ts +214 -0
- package/src/onboard/types.ts +72 -0
- package/src/proxy/__tests__/tool-registry.test.ts +93 -0
- package/src/proxy/tool-registry.ts +122 -0
- package/src/proxy.ts +12 -0
- package/src/registry/context7-backend.ts +93 -0
- package/src/registry/context7.ts +54 -0
- package/src/registry/d1-store.ts +242 -0
- package/src/registry/memory-store.ts +101 -0
- package/src/registry/openapi-compiler.ts +284 -0
- package/src/registry/resolver.ts +196 -0
- package/src/registry/rpc-compiler.ts +142 -0
- package/src/registry/skill-parser.ts +119 -0
- package/src/registry/skill-to-config.ts +239 -0
- package/src/registry/source-refresher.ts +83 -0
- package/src/registry/types.ts +129 -0
- package/src/registry/virtual-files.ts +76 -0
- package/src/testing/sqlite-d1.ts +64 -0
- package/src/testing.ts +2 -0
- package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
- package/src/tunnel/__tests__/tunnel.test.ts +542 -0
- package/src/tunnel/cloudflare-provider.ts +121 -0
- package/src/tunnel/memory-store.ts +30 -0
- package/src/tunnel/types.ts +28 -0
- package/test/credential/d1-vault.test.ts +127 -0
- package/test/credential/injection.test.ts +67 -0
- package/test/credential/memory-vault.test.ts +63 -0
- package/test/http/app.test.ts +300 -0
- package/test/http/byok-e2e.test.ts +240 -0
- package/test/http/byok.test.ts +115 -0
- package/test/http/credentials.test.ts +57 -0
- package/test/http/e2e.test.ts +260 -0
- package/test/integration/authenticated-apis.test.ts +185 -0
- package/test/integration/free-apis-e2e.test.ts +222 -0
- package/test/metering/d1-store.test.ts +82 -0
- package/test/metering/memory-store.test.ts +76 -0
- package/test/metering/pricing-guard.test.ts +108 -0
- package/test/onboard/apis-guru.test.ts +57 -0
- package/test/onboard/e2e.test.ts +70 -0
- package/test/onboard/pipeline.test.ts +318 -0
- package/test/onboard/real-apis.test.ts +483 -0
- package/test/registry/compilation-correctness.test.ts +132 -0
- package/test/registry/context7-backend.test.ts +88 -0
- package/test/registry/context7-e2e.test.ts +92 -0
- package/test/registry/context7.test.ts +73 -0
- package/test/registry/d1-store.test.ts +184 -0
- package/test/registry/integration.test.ts +129 -0
- package/test/registry/lazy-mount.test.ts +138 -0
- package/test/registry/memory-store.test.ts +171 -0
- package/test/registry/openapi-compiler.test.ts +267 -0
- package/test/registry/openapi-e2e.test.ts +154 -0
- package/test/registry/passthrough-e2e.test.ts +109 -0
- package/test/registry/resolver-peer.test.ts +299 -0
- package/test/registry/resolver.test.ts +228 -0
- package/test/registry/rpc-compiler.test.ts +112 -0
- package/test/registry/skill-parser.test.ts +151 -0
- package/test/registry/skill-to-config.test.ts +151 -0
- package/test/registry/skill-to-rpc-config.test.ts +142 -0
- package/test/registry/source-refresher.test.ts +90 -0
- package/test/registry/virtual-files.test.ts +96 -0
- package/tsconfig.json +4 -0
- package/tsup.config.ts +8 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E: Onboard real major APIs from the internet, verify they compile
|
|
3
|
+
* and become browsable via AgentFs.
|
|
4
|
+
*
|
|
5
|
+
* This test makes REAL network calls to fetch OpenAPI specs.
|
|
6
|
+
* It does NOT call the actual APIs (no auth needed).
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
9
|
+
import { OnboardPipeline } from "../../src/onboard/pipeline.js";
|
|
10
|
+
import { MemoryRegistryStore } from "../../src/registry/memory-store.js";
|
|
11
|
+
import { createRegistryResolver } from "../../src/registry/resolver.js";
|
|
12
|
+
import { AgentFs } from "@nkmc/agent-fs";
|
|
13
|
+
import { ALL_APIS, FREE_APIS, RPC_APIS } from "../../src/onboard/manifest.js";
|
|
14
|
+
import type { OnboardReport, OnboardResult } from "../../src/onboard/types.js";
|
|
15
|
+
|
|
16
|
+
// ── Helper ────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function printReport(report: OnboardReport) {
|
|
19
|
+
const summary = `Total: ${report.total} | OK: ${report.ok} | Failed: ${report.failed} | Skipped: ${report.skipped} | ${report.durationMs}ms`;
|
|
20
|
+
console.log(`\n${"─".repeat(60)}`);
|
|
21
|
+
console.log(summary);
|
|
22
|
+
console.log(`${"─".repeat(60)}`);
|
|
23
|
+
for (const r of report.results) {
|
|
24
|
+
const icon = r.status === "ok" ? "✓" : r.status === "failed" ? "✗" : "○";
|
|
25
|
+
const info = r.status === "ok"
|
|
26
|
+
? `${r.endpoints} endpoints, ${r.resources} resources (${r.durationMs}ms)`
|
|
27
|
+
: r.error ?? "skipped";
|
|
28
|
+
console.log(` ${icon} ${r.domain.padEnd(30)} ${info}`);
|
|
29
|
+
}
|
|
30
|
+
console.log();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Tests ─────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe("Real API Onboarding (network)", () => {
|
|
36
|
+
let store: MemoryRegistryStore;
|
|
37
|
+
let pipeline: OnboardPipeline;
|
|
38
|
+
let report: OnboardReport;
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
store = new MemoryRegistryStore();
|
|
42
|
+
pipeline = new OnboardPipeline({
|
|
43
|
+
store,
|
|
44
|
+
smokeTest: false,
|
|
45
|
+
concurrency: 3,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Strip auth — we only need to fetch & compile specs, not call APIs
|
|
49
|
+
const entries = ALL_APIS.map((e) => ({ ...e, auth: undefined }));
|
|
50
|
+
report = await pipeline.onboardMany(entries);
|
|
51
|
+
printReport(report);
|
|
52
|
+
}, 120_000);
|
|
53
|
+
|
|
54
|
+
it("should onboard majority of APIs successfully", () => {
|
|
55
|
+
// Allow some failures (network issues, spec changes), but most should work
|
|
56
|
+
expect(report.ok).toBeGreaterThanOrEqual(Math.floor(report.total * 0.6));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should compile specs with endpoints", () => {
|
|
60
|
+
const successful = report.results.filter((r) => r.status === "ok");
|
|
61
|
+
for (const r of successful) {
|
|
62
|
+
expect(r.endpoints).toBeGreaterThan(0);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should compile specs with resources", () => {
|
|
67
|
+
const successful = report.results.filter((r) => r.status === "ok");
|
|
68
|
+
const withResources = successful.filter((r) => r.resources > 0);
|
|
69
|
+
// Most OpenAPI specs should infer at least 1 resource
|
|
70
|
+
expect(withResources.length).toBeGreaterThanOrEqual(
|
|
71
|
+
Math.floor(successful.length * 0.5),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should register all successful services in store", async () => {
|
|
76
|
+
const services = await store.list();
|
|
77
|
+
expect(services.length).toBe(report.ok);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ── Per-service verification ──────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
describe("Individual API verification", () => {
|
|
83
|
+
const knownApis = [
|
|
84
|
+
{ domain: "petstore3.swagger.io", minEndpoints: 10 },
|
|
85
|
+
{ domain: "api.weather.gov", minEndpoints: 5 },
|
|
86
|
+
{ domain: "api.github.com", minEndpoints: 100 },
|
|
87
|
+
{ domain: "api.stripe.com", minEndpoints: 50 },
|
|
88
|
+
{ domain: "api.cloudflare.com", minEndpoints: 50 },
|
|
89
|
+
{ domain: "discord.com", minEndpoints: 20 },
|
|
90
|
+
{ domain: "slack.com", minEndpoints: 50 },
|
|
91
|
+
{ domain: "api.digitalocean.com", minEndpoints: 30 },
|
|
92
|
+
{ domain: "sentry.io", minEndpoints: 20 },
|
|
93
|
+
{ domain: "api.vercel.com", minEndpoints: 10 },
|
|
94
|
+
{ domain: "api.mistral.ai", minEndpoints: 3 },
|
|
95
|
+
{ domain: "api.openai.com", minEndpoints: 10 },
|
|
96
|
+
{ domain: "api.twilio.com", minEndpoints: 50 },
|
|
97
|
+
{ domain: "api.resend.com", minEndpoints: 10 },
|
|
98
|
+
// Batch 2
|
|
99
|
+
{ domain: "openrouter.ai", minEndpoints: 3 },
|
|
100
|
+
{ domain: "fly.io", minEndpoints: 20 },
|
|
101
|
+
{ domain: "api.render.com", minEndpoints: 30 },
|
|
102
|
+
{ domain: "api.notion.com", minEndpoints: 5 },
|
|
103
|
+
{ domain: "app.asana.com", minEndpoints: 50 },
|
|
104
|
+
{ domain: "circleci.com", minEndpoints: 30 },
|
|
105
|
+
{ domain: "api.datadoghq.com", minEndpoints: 100 },
|
|
106
|
+
// Batch 3
|
|
107
|
+
{ domain: "en.wikipedia.org", minEndpoints: 20 },
|
|
108
|
+
{ domain: "jira.atlassian.com", minEndpoints: 100 },
|
|
109
|
+
{ domain: "api.spotify.com", minEndpoints: 30 },
|
|
110
|
+
{ domain: "api.getpostman.com", minEndpoints: 10 },
|
|
111
|
+
{ domain: "api.supabase.com", minEndpoints: 30 },
|
|
112
|
+
{ domain: "api.turso.tech", minEndpoints: 10 },
|
|
113
|
+
{ domain: "console.neon.tech", minEndpoints: 20 },
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
for (const { domain, minEndpoints } of knownApis) {
|
|
117
|
+
it(`${domain}: should have >= ${minEndpoints} endpoints`, () => {
|
|
118
|
+
const r = report.results.find((r) => r.domain === domain);
|
|
119
|
+
if (!r || r.status !== "ok") {
|
|
120
|
+
// Skip if network fetch failed — don't fail the whole suite
|
|
121
|
+
console.warn(` ⚠ ${domain} was not onboarded (${r?.error ?? "missing"})`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
expect(r.endpoints).toBeGreaterThanOrEqual(minEndpoints);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── RPC services (all use the shared EVM_METHODS with 13 methods) ──
|
|
129
|
+
|
|
130
|
+
const rpcApis = RPC_APIS.map((e) => e.domain);
|
|
131
|
+
|
|
132
|
+
for (const domain of rpcApis) {
|
|
133
|
+
it(`${domain}: should be onboarded as jsonrpc with >= 13 endpoints`, () => {
|
|
134
|
+
const r = report.results.find((r) => r.domain === domain);
|
|
135
|
+
expect(r).toBeDefined();
|
|
136
|
+
expect(r!.status).toBe("ok");
|
|
137
|
+
expect(r!.source).toBe("jsonrpc");
|
|
138
|
+
expect(r!.endpoints).toBeGreaterThanOrEqual(13);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it(`${domain}: should have source.rpc metadata in store`, async () => {
|
|
142
|
+
const record = await store.get(domain);
|
|
143
|
+
expect(record).not.toBeNull();
|
|
144
|
+
expect(record!.source?.type).toBe("jsonrpc");
|
|
145
|
+
expect(record!.source?.rpc).toBeDefined();
|
|
146
|
+
expect(record!.source!.rpc!.convention).toBe("evm");
|
|
147
|
+
expect(record!.source!.rpc!.resources.length).toBeGreaterThan(0);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── AgentFs browsing ──────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
describe("AgentFs browsing after onboard", () => {
|
|
155
|
+
let fs: AgentFs;
|
|
156
|
+
|
|
157
|
+
beforeAll(() => {
|
|
158
|
+
const { onMiss, listDomains } = createRegistryResolver({
|
|
159
|
+
store,
|
|
160
|
+
wrapVirtualFiles: false,
|
|
161
|
+
});
|
|
162
|
+
fs = new AgentFs({ mounts: [], onMiss, listDomains });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("ls / should list all onboarded domains", async () => {
|
|
166
|
+
const result = await fs.execute("ls /");
|
|
167
|
+
expect(result.ok).toBe(true);
|
|
168
|
+
const entries = result.data as string[];
|
|
169
|
+
// At least the free APIs should be listed
|
|
170
|
+
for (const api of FREE_APIS) {
|
|
171
|
+
if (report.results.find((r) => r.domain === api.domain && r.status === "ok")) {
|
|
172
|
+
expect(entries).toContain(`${api.domain}/`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("ls /petstore3.swagger.io/ should show pet resources", async () => {
|
|
178
|
+
const r = report.results.find((r) => r.domain === "petstore3.swagger.io");
|
|
179
|
+
if (!r || r.status !== "ok") return;
|
|
180
|
+
|
|
181
|
+
const result = await fs.execute("ls /petstore3.swagger.io/");
|
|
182
|
+
expect(result.ok).toBe(true);
|
|
183
|
+
const entries = result.data as string[];
|
|
184
|
+
expect(entries.some((e) => e.includes("pet"))).toBe(true);
|
|
185
|
+
expect(entries).toContain("_api/");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("ls /api.github.com/ should show repo-related resources", async () => {
|
|
189
|
+
const r = report.results.find((r) => r.domain === "api.github.com");
|
|
190
|
+
if (!r || r.status !== "ok") return;
|
|
191
|
+
|
|
192
|
+
const result = await fs.execute("ls /api.github.com/");
|
|
193
|
+
expect(result.ok).toBe(true);
|
|
194
|
+
const entries = result.data as string[];
|
|
195
|
+
expect(entries.some((e) => e.includes("repos"))).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("ls /api.stripe.com/ should show payment resources", async () => {
|
|
199
|
+
const r = report.results.find((r) => r.domain === "api.stripe.com");
|
|
200
|
+
if (!r || r.status !== "ok") return;
|
|
201
|
+
|
|
202
|
+
const result = await fs.execute("ls /api.stripe.com/");
|
|
203
|
+
expect(result.ok).toBe(true);
|
|
204
|
+
const entries = result.data as string[];
|
|
205
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("ls /api.cloudflare.com/ should show zone/dns resources", async () => {
|
|
209
|
+
const r = report.results.find((r) => r.domain === "api.cloudflare.com");
|
|
210
|
+
if (!r || r.status !== "ok") return;
|
|
211
|
+
|
|
212
|
+
const result = await fs.execute("ls /api.cloudflare.com/");
|
|
213
|
+
expect(result.ok).toBe(true);
|
|
214
|
+
const entries = result.data as string[];
|
|
215
|
+
expect(entries.some((e) => e.includes("zone"))).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("ls /api.openai.com/ should show AI resources", async () => {
|
|
219
|
+
const r = report.results.find((r) => r.domain === "api.openai.com");
|
|
220
|
+
if (!r || r.status !== "ok") return;
|
|
221
|
+
|
|
222
|
+
const result = await fs.execute("ls /api.openai.com/");
|
|
223
|
+
expect(result.ok).toBe(true);
|
|
224
|
+
const entries = result.data as string[];
|
|
225
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
226
|
+
expect(entries).toContain("_api/");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("ls /api.twilio.com/ should show communication resources", async () => {
|
|
230
|
+
const r = report.results.find((r) => r.domain === "api.twilio.com");
|
|
231
|
+
if (!r || r.status !== "ok") return;
|
|
232
|
+
|
|
233
|
+
const result = await fs.execute("ls /api.twilio.com/");
|
|
234
|
+
expect(result.ok).toBe(true);
|
|
235
|
+
const entries = result.data as string[];
|
|
236
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
237
|
+
expect(entries).toContain("_api/");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("ls /api.resend.com/ should show email resources", async () => {
|
|
241
|
+
const r = report.results.find((r) => r.domain === "api.resend.com");
|
|
242
|
+
if (!r || r.status !== "ok") return;
|
|
243
|
+
|
|
244
|
+
const result = await fs.execute("ls /api.resend.com/");
|
|
245
|
+
expect(result.ok).toBe(true);
|
|
246
|
+
const entries = result.data as string[];
|
|
247
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
248
|
+
expect(entries.some((e) => e.includes("email") || e.includes("domain"))).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ── Batch 2 browsing ──────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
it("ls /openrouter.ai/ should show AI gateway resources", async () => {
|
|
254
|
+
const r = report.results.find((r) => r.domain === "openrouter.ai");
|
|
255
|
+
if (!r || r.status !== "ok") return;
|
|
256
|
+
|
|
257
|
+
const result = await fs.execute("ls /openrouter.ai/");
|
|
258
|
+
expect(result.ok).toBe(true);
|
|
259
|
+
const entries = result.data as string[];
|
|
260
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
261
|
+
expect(entries).toContain("_api/");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("ls /fly.io/ should show deployment resources", async () => {
|
|
265
|
+
const r = report.results.find((r) => r.domain === "fly.io");
|
|
266
|
+
if (!r || r.status !== "ok") return;
|
|
267
|
+
|
|
268
|
+
const result = await fs.execute("ls /fly.io/");
|
|
269
|
+
expect(result.ok).toBe(true);
|
|
270
|
+
const entries = result.data as string[];
|
|
271
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
272
|
+
expect(entries.some((e) => e.includes("app"))).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("ls /api.render.com/ should show service resources", async () => {
|
|
276
|
+
const r = report.results.find((r) => r.domain === "api.render.com");
|
|
277
|
+
if (!r || r.status !== "ok") return;
|
|
278
|
+
|
|
279
|
+
const result = await fs.execute("ls /api.render.com/");
|
|
280
|
+
expect(result.ok).toBe(true);
|
|
281
|
+
const entries = result.data as string[];
|
|
282
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("ls /api.notion.com/ should show productivity resources", async () => {
|
|
286
|
+
const r = report.results.find((r) => r.domain === "api.notion.com");
|
|
287
|
+
if (!r || r.status !== "ok") return;
|
|
288
|
+
|
|
289
|
+
const result = await fs.execute("ls /api.notion.com/");
|
|
290
|
+
expect(result.ok).toBe(true);
|
|
291
|
+
const entries = result.data as string[];
|
|
292
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("ls /app.asana.com/ should show project management resources", async () => {
|
|
296
|
+
const r = report.results.find((r) => r.domain === "app.asana.com");
|
|
297
|
+
if (!r || r.status !== "ok") return;
|
|
298
|
+
|
|
299
|
+
const result = await fs.execute("ls /app.asana.com/");
|
|
300
|
+
expect(result.ok).toBe(true);
|
|
301
|
+
const entries = result.data as string[];
|
|
302
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("ls /circleci.com/ should show CI/CD resources", async () => {
|
|
306
|
+
const r = report.results.find((r) => r.domain === "circleci.com");
|
|
307
|
+
if (!r || r.status !== "ok") return;
|
|
308
|
+
|
|
309
|
+
const result = await fs.execute("ls /circleci.com/");
|
|
310
|
+
expect(result.ok).toBe(true);
|
|
311
|
+
const entries = result.data as string[];
|
|
312
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("ls /api.datadoghq.com/ should show monitoring resources", async () => {
|
|
316
|
+
const r = report.results.find((r) => r.domain === "api.datadoghq.com");
|
|
317
|
+
if (!r || r.status !== "ok") return;
|
|
318
|
+
|
|
319
|
+
const result = await fs.execute("ls /api.datadoghq.com/");
|
|
320
|
+
expect(result.ok).toBe(true);
|
|
321
|
+
const entries = result.data as string[];
|
|
322
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ── Batch 3 browsing ──────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
it("ls /en.wikipedia.org/ should show knowledge resources", async () => {
|
|
328
|
+
const r = report.results.find((r) => r.domain === "en.wikipedia.org");
|
|
329
|
+
if (!r || r.status !== "ok") return;
|
|
330
|
+
|
|
331
|
+
const result = await fs.execute("ls /en.wikipedia.org/");
|
|
332
|
+
expect(result.ok).toBe(true);
|
|
333
|
+
const entries = result.data as string[];
|
|
334
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("ls /jira.atlassian.com/ should show project resources", async () => {
|
|
338
|
+
const r = report.results.find((r) => r.domain === "jira.atlassian.com");
|
|
339
|
+
if (!r || r.status !== "ok") return;
|
|
340
|
+
|
|
341
|
+
const result = await fs.execute("ls /jira.atlassian.com/");
|
|
342
|
+
expect(result.ok).toBe(true);
|
|
343
|
+
const entries = result.data as string[];
|
|
344
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("ls /api.spotify.com/ should show music resources", async () => {
|
|
348
|
+
const r = report.results.find((r) => r.domain === "api.spotify.com");
|
|
349
|
+
if (!r || r.status !== "ok") return;
|
|
350
|
+
|
|
351
|
+
const result = await fs.execute("ls /api.spotify.com/");
|
|
352
|
+
expect(result.ok).toBe(true);
|
|
353
|
+
const entries = result.data as string[];
|
|
354
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("ls /api.supabase.com/ should show database resources", async () => {
|
|
358
|
+
const r = report.results.find((r) => r.domain === "api.supabase.com");
|
|
359
|
+
if (!r || r.status !== "ok") return;
|
|
360
|
+
|
|
361
|
+
const result = await fs.execute("ls /api.supabase.com/");
|
|
362
|
+
expect(result.ok).toBe(true);
|
|
363
|
+
const entries = result.data as string[];
|
|
364
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("ls /console.neon.tech/ should show database resources", async () => {
|
|
368
|
+
const r = report.results.find((r) => r.domain === "console.neon.tech");
|
|
369
|
+
if (!r || r.status !== "ok") return;
|
|
370
|
+
|
|
371
|
+
const result = await fs.execute("ls /console.neon.tech/");
|
|
372
|
+
expect(result.ok).toBe(true);
|
|
373
|
+
const entries = result.data as string[];
|
|
374
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// ── RPC service browsing ──────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
for (const api of RPC_APIS) {
|
|
380
|
+
it(`ls /${api.domain}/ should show EVM resources`, async () => {
|
|
381
|
+
const result = await fs.execute(`ls /${api.domain}/`);
|
|
382
|
+
expect(result.ok).toBe(true);
|
|
383
|
+
const entries = result.data as string[];
|
|
384
|
+
// All EVM services should expose blocks, balances, transactions
|
|
385
|
+
expect(entries).toContain("blocks/");
|
|
386
|
+
expect(entries).toContain("balances/");
|
|
387
|
+
expect(entries).toContain("transactions/");
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// ── Live HTTP test for free APIs ──────────────────────────────────
|
|
393
|
+
|
|
394
|
+
describe("Live HTTP calls to free APIs", () => {
|
|
395
|
+
let fs: AgentFs;
|
|
396
|
+
|
|
397
|
+
beforeAll(() => {
|
|
398
|
+
const { onMiss, listDomains } = createRegistryResolver({
|
|
399
|
+
store,
|
|
400
|
+
wrapVirtualFiles: false,
|
|
401
|
+
});
|
|
402
|
+
fs = new AgentFs({ mounts: [], onMiss, listDomains });
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should fetch real data from Petstore API via AgentFs", async () => {
|
|
406
|
+
const r = report.results.find((r) => r.domain === "petstore3.swagger.io");
|
|
407
|
+
if (!r || r.status !== "ok") return;
|
|
408
|
+
|
|
409
|
+
// List the _api directory to see available endpoints
|
|
410
|
+
const apiList = await fs.execute("ls /petstore3.swagger.io/_api/");
|
|
411
|
+
expect(apiList.ok).toBe(true);
|
|
412
|
+
const apiEntries = apiList.data as string[];
|
|
413
|
+
expect(apiEntries.length).toBeGreaterThan(0);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("should fetch weather alerts from NWS API via AgentFs", async () => {
|
|
417
|
+
const r = report.results.find((r) => r.domain === "api.weather.gov");
|
|
418
|
+
if (!r || r.status !== "ok") return;
|
|
419
|
+
|
|
420
|
+
const result = await fs.execute("ls /api.weather.gov/");
|
|
421
|
+
expect(result.ok).toBe(true);
|
|
422
|
+
const entries = result.data as string[];
|
|
423
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// ── Live RPC calls to free blockchain providers ────────────────────
|
|
428
|
+
// Free public RPC endpoints may have rate limits, geo-restrictions,
|
|
429
|
+
// or occasional downtime. We test at least ONE provider end-to-end
|
|
430
|
+
// and treat individual provider failures as non-fatal warnings.
|
|
431
|
+
|
|
432
|
+
describe("Live RPC calls to free blockchain APIs", () => {
|
|
433
|
+
let fs: AgentFs;
|
|
434
|
+
|
|
435
|
+
// Free RPC providers that need no API key
|
|
436
|
+
const freeRpcDomains = RPC_APIS
|
|
437
|
+
.filter((e) => e.tags?.includes("free"))
|
|
438
|
+
.map((e) => e.domain);
|
|
439
|
+
|
|
440
|
+
const lsResults = new Map<string, string[]>();
|
|
441
|
+
|
|
442
|
+
beforeAll(() => {
|
|
443
|
+
const { onMiss, listDomains } = createRegistryResolver({
|
|
444
|
+
store,
|
|
445
|
+
wrapVirtualFiles: false,
|
|
446
|
+
});
|
|
447
|
+
fs = new AgentFs({ mounts: [], onMiss, listDomains });
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
for (const domain of freeRpcDomains) {
|
|
451
|
+
it(`${domain}: ls blocks/ should return recent block numbers`, async () => {
|
|
452
|
+
const result = await fs.execute(`ls /${domain}/blocks/`);
|
|
453
|
+
if (!result.ok) {
|
|
454
|
+
console.warn(` ⚠ ${domain}: ls blocks/ failed (rate limit or network) — skipping`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const entries = result.data as string[];
|
|
458
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
459
|
+
expect(entries[0]).toMatch(/\.json$/);
|
|
460
|
+
lsResults.set(domain, entries);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it(`${domain}: cat blocks/{id}.json should return block data`, async () => {
|
|
464
|
+
const entries = lsResults.get(domain);
|
|
465
|
+
if (!entries || entries.length === 0) {
|
|
466
|
+
console.warn(` ⚠ ${domain}: skipping cat (no blocks from ls)`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const result = await fs.execute(`cat /${domain}/blocks/${entries[0]}`);
|
|
471
|
+
if (!result.ok) {
|
|
472
|
+
console.warn(` ⚠ ${domain}: cat blocks/ failed (rate limit or network) — skipping`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
expect(result.data).toBeDefined();
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
it("at least one free RPC provider should be fully reachable", () => {
|
|
480
|
+
expect(lsResults.size).toBeGreaterThan(0);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
}, 120_000);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { compileOpenApiSpec, extractBasePath } from "../../src/registry/openapi-compiler.js";
|
|
3
|
+
import { skillToHttpConfig } from "../../src/registry/skill-to-config.js";
|
|
4
|
+
import type { ServiceRecord } from "../../src/registry/types.js";
|
|
5
|
+
|
|
6
|
+
// --- basePath extraction ---
|
|
7
|
+
|
|
8
|
+
describe("extractBasePath", () => {
|
|
9
|
+
it("Cloudflare: should extract /client/v4", () => {
|
|
10
|
+
const spec = { servers: [{ url: "https://api.cloudflare.com/client/v4" }] };
|
|
11
|
+
expect(extractBasePath(spec)).toBe("/client/v4");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("Slack: should extract /api", () => {
|
|
15
|
+
const spec = { servers: [{ url: "https://slack.com/api" }] };
|
|
16
|
+
expect(extractBasePath(spec)).toBe("/api");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("Stripe: should extract /v1", () => {
|
|
20
|
+
const spec = { servers: [{ url: "https://api.stripe.com/v1" }] };
|
|
21
|
+
expect(extractBasePath(spec)).toBe("/v1");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("Petstore: should extract /api/v3", () => {
|
|
25
|
+
const spec = { servers: [{ url: "https://petstore3.swagger.io/api/v3" }] };
|
|
26
|
+
expect(extractBasePath(spec)).toBe("/api/v3");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("GitHub: should return empty string (no path)", () => {
|
|
30
|
+
const spec = { servers: [{ url: "https://api.github.com" }] };
|
|
31
|
+
expect(extractBasePath(spec)).toBe("");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should return empty string when no servers field", () => {
|
|
35
|
+
expect(extractBasePath({})).toBe("");
|
|
36
|
+
expect(extractBasePath({ servers: [] })).toBe("");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should handle relative URL", () => {
|
|
40
|
+
const spec = { servers: [{ url: "/api/v2" }] };
|
|
41
|
+
expect(extractBasePath(spec)).toBe("/api/v2");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should strip trailing slash", () => {
|
|
45
|
+
const spec = { servers: [{ url: "https://example.com/api/" }] };
|
|
46
|
+
expect(extractBasePath(spec)).toBe("/api");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should handle root path URL", () => {
|
|
50
|
+
const spec = { servers: [{ url: "https://example.com/" }] };
|
|
51
|
+
expect(extractBasePath(spec)).toBe("");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// --- basePath stored in compiled record ---
|
|
56
|
+
|
|
57
|
+
describe("compileOpenApiSpec basePath", () => {
|
|
58
|
+
it("should store basePath in source config when present", () => {
|
|
59
|
+
const spec = {
|
|
60
|
+
openapi: "3.0.0",
|
|
61
|
+
info: { title: "Test", version: "1.0" },
|
|
62
|
+
servers: [{ url: "https://api.cloudflare.com/client/v4" }],
|
|
63
|
+
paths: {},
|
|
64
|
+
};
|
|
65
|
+
const { record } = compileOpenApiSpec(spec, { domain: "api.cloudflare.com" });
|
|
66
|
+
expect(record.source?.basePath).toBe("/client/v4");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should not include basePath when path is empty", () => {
|
|
70
|
+
const spec = {
|
|
71
|
+
openapi: "3.0.0",
|
|
72
|
+
info: { title: "Test", version: "1.0" },
|
|
73
|
+
servers: [{ url: "https://api.github.com" }],
|
|
74
|
+
paths: {},
|
|
75
|
+
};
|
|
76
|
+
const { record } = compileOpenApiSpec(spec, { domain: "api.github.com" });
|
|
77
|
+
expect(record.source?.basePath).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// --- baseUrl → HttpBackendConfig ---
|
|
82
|
+
|
|
83
|
+
describe("skillToHttpConfig with basePath", () => {
|
|
84
|
+
function makeRecord(domain: string, basePath?: string): ServiceRecord {
|
|
85
|
+
return {
|
|
86
|
+
domain,
|
|
87
|
+
name: domain,
|
|
88
|
+
description: "test",
|
|
89
|
+
version: "1.0",
|
|
90
|
+
roles: ["agent"],
|
|
91
|
+
skillMd: `---\nname: "${domain}"\ngateway: nkmc\nversion: "1.0"\nroles: [agent]\n---\n\n# ${domain}\n\nTest.\n`,
|
|
92
|
+
endpoints: [],
|
|
93
|
+
isFirstParty: false,
|
|
94
|
+
createdAt: Date.now(),
|
|
95
|
+
updatedAt: Date.now(),
|
|
96
|
+
status: "active",
|
|
97
|
+
isDefault: true,
|
|
98
|
+
source: { type: "openapi", ...(basePath ? { basePath } : {}) },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
it("should include basePath in baseUrl when present", () => {
|
|
103
|
+
const record = makeRecord("api.cloudflare.com", "/client/v4");
|
|
104
|
+
const config = skillToHttpConfig(record);
|
|
105
|
+
expect(config.baseUrl).toBe("https://api.cloudflare.com/client/v4");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should include /api basePath for Slack", () => {
|
|
109
|
+
const record = makeRecord("slack.com", "/api");
|
|
110
|
+
const config = skillToHttpConfig(record);
|
|
111
|
+
expect(config.baseUrl).toBe("https://slack.com/api");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should include /v1 basePath for Stripe", () => {
|
|
115
|
+
const record = makeRecord("api.stripe.com", "/v1");
|
|
116
|
+
const config = skillToHttpConfig(record);
|
|
117
|
+
expect(config.baseUrl).toBe("https://api.stripe.com/v1");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should not append anything when no basePath", () => {
|
|
121
|
+
const record = makeRecord("api.github.com");
|
|
122
|
+
const config = skillToHttpConfig(record);
|
|
123
|
+
expect(config.baseUrl).toBe("https://api.github.com");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should not append anything when no source", () => {
|
|
127
|
+
const record = makeRecord("simple.com");
|
|
128
|
+
delete (record as any).source;
|
|
129
|
+
const config = skillToHttpConfig(record);
|
|
130
|
+
expect(config.baseUrl).toBe("https://simple.com");
|
|
131
|
+
});
|
|
132
|
+
});
|