@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,228 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { AgentFs } from "@nkmc/agent-fs";
|
|
3
|
+
import { MemoryBackend } from "@nkmc/agent-fs/testing";
|
|
4
|
+
import { MemoryRegistryStore } from "../../src/registry/memory-store.js";
|
|
5
|
+
import { parseSkillMd } from "../../src/registry/skill-parser.js";
|
|
6
|
+
import { createRegistryResolver, extractDomainPath } from "../../src/registry/resolver.js";
|
|
7
|
+
|
|
8
|
+
const ACME_SKILL = `---
|
|
9
|
+
name: "Acme Store"
|
|
10
|
+
gateway: nkmc
|
|
11
|
+
version: "1.0"
|
|
12
|
+
roles: [agent]
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Acme Store
|
|
16
|
+
|
|
17
|
+
E-commerce store.
|
|
18
|
+
|
|
19
|
+
## API
|
|
20
|
+
|
|
21
|
+
### List products
|
|
22
|
+
|
|
23
|
+
\`GET /api/products\` — 免費,public
|
|
24
|
+
|
|
25
|
+
### Create order
|
|
26
|
+
|
|
27
|
+
\`POST /api/orders\` — 0.05 USDC / 次,agent
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
describe("RegistryResolver", () => {
|
|
31
|
+
let store: MemoryRegistryStore;
|
|
32
|
+
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
store = new MemoryRegistryStore();
|
|
35
|
+
const record = parseSkillMd("acme-store.com", ACME_SKILL);
|
|
36
|
+
await store.put("acme-store.com", record);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should create AgentFs hooks from RegistryStore", async () => {
|
|
40
|
+
const { onMiss, listDomains } = createRegistryResolver(store);
|
|
41
|
+
expect(typeof onMiss).toBe("function");
|
|
42
|
+
expect(typeof listDomains).toBe("function");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("listDomains should return registered domains", async () => {
|
|
46
|
+
const { listDomains } = createRegistryResolver(store);
|
|
47
|
+
const domains = await listDomains();
|
|
48
|
+
expect(domains).toContain("acme-store.com");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("onMiss should create HttpBackend for known domain", async () => {
|
|
52
|
+
const { onMiss } = createRegistryResolver(store);
|
|
53
|
+
let mountAdded = false;
|
|
54
|
+
const added = await onMiss("/acme-store.com/products", (mount) => {
|
|
55
|
+
mountAdded = true;
|
|
56
|
+
expect(mount.path).toBe("/acme-store.com");
|
|
57
|
+
});
|
|
58
|
+
expect(added).toBe(true);
|
|
59
|
+
expect(mountAdded).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("onMiss should return false for unknown domain", async () => {
|
|
63
|
+
const { onMiss } = createRegistryResolver(store);
|
|
64
|
+
const added = await onMiss("/unknown.com/test", () => {});
|
|
65
|
+
expect(added).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("onMiss should cache — not recreate backend for same domain", async () => {
|
|
69
|
+
const { onMiss } = createRegistryResolver(store);
|
|
70
|
+
let addCount = 0;
|
|
71
|
+
const addMount = () => {
|
|
72
|
+
addCount++;
|
|
73
|
+
};
|
|
74
|
+
await onMiss("/acme-store.com/products", addMount);
|
|
75
|
+
await onMiss("/acme-store.com/orders", addMount);
|
|
76
|
+
expect(addCount).toBe(1); // Only first call creates the mount
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should work end-to-end with AgentFs for ls /", async () => {
|
|
80
|
+
const { onMiss, listDomains } = createRegistryResolver(store);
|
|
81
|
+
const staticBackend = new MemoryBackend();
|
|
82
|
+
|
|
83
|
+
const fs = new AgentFs({
|
|
84
|
+
mounts: [{ path: "/memory", backend: staticBackend }],
|
|
85
|
+
onMiss,
|
|
86
|
+
listDomains,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await fs.execute("ls /");
|
|
90
|
+
expect(result.ok).toBe(true);
|
|
91
|
+
if (result.ok) {
|
|
92
|
+
const entries = result.data as string[];
|
|
93
|
+
expect(entries).toContain("memory/");
|
|
94
|
+
expect(entries).toContain("acme-store.com/");
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("grep on root should search registry and return SearchResult", async () => {
|
|
99
|
+
const { onMiss, listDomains, searchDomains } =
|
|
100
|
+
createRegistryResolver(store);
|
|
101
|
+
const fs = new AgentFs({
|
|
102
|
+
mounts: [],
|
|
103
|
+
onMiss,
|
|
104
|
+
listDomains,
|
|
105
|
+
searchDomains,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const result = await fs.execute('grep "e-commerce" /');
|
|
109
|
+
expect(result.ok).toBe(true);
|
|
110
|
+
if (result.ok) {
|
|
111
|
+
const data = result.data as { domain: string; matchedEndpoints: unknown[] }[];
|
|
112
|
+
expect(data.length).toBeGreaterThan(0);
|
|
113
|
+
expect(data[0].domain).toBe("acme-store.com");
|
|
114
|
+
// "e-commerce" matches service description, not specific endpoints
|
|
115
|
+
expect(data[0].matchedEndpoints).toEqual([]);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("grep on root should return matchedEndpoints when query matches endpoint", async () => {
|
|
120
|
+
const { searchDomains } = createRegistryResolver(store);
|
|
121
|
+
const results = await searchDomains("products");
|
|
122
|
+
expect(results.length).toBeGreaterThan(0);
|
|
123
|
+
expect(results[0].matchedEndpoints.length).toBeGreaterThan(0);
|
|
124
|
+
expect(results[0].matchedEndpoints[0].path).toBe("/api/products");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("searchEndpoints should filter endpoints within a domain", async () => {
|
|
128
|
+
const { searchEndpoints } = createRegistryResolver(store);
|
|
129
|
+
const results = await searchEndpoints("acme-store.com", "products");
|
|
130
|
+
expect(results).toHaveLength(1);
|
|
131
|
+
expect(results[0]).toEqual({
|
|
132
|
+
method: "GET",
|
|
133
|
+
path: "/api/products",
|
|
134
|
+
description: expect.any(String),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("searchEndpoints should return empty for unknown domain", async () => {
|
|
139
|
+
const { searchEndpoints } = createRegistryResolver(store);
|
|
140
|
+
const results = await searchEndpoints("unknown.com", "test");
|
|
141
|
+
expect(results).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("searchEndpoints should return empty when no endpoints match", async () => {
|
|
145
|
+
const { searchEndpoints } = createRegistryResolver(store);
|
|
146
|
+
const results = await searchEndpoints("acme-store.com", "zzzzz");
|
|
147
|
+
expect(results).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("searchEndpoints should work end-to-end with AgentFs for domain grep", async () => {
|
|
151
|
+
const { onMiss, listDomains, searchDomains, searchEndpoints } =
|
|
152
|
+
createRegistryResolver(store);
|
|
153
|
+
const fs = new AgentFs({
|
|
154
|
+
mounts: [],
|
|
155
|
+
onMiss,
|
|
156
|
+
listDomains,
|
|
157
|
+
searchDomains,
|
|
158
|
+
searchEndpoints,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const result = await fs.execute('grep "orders" /acme-store.com/');
|
|
162
|
+
expect(result.ok).toBe(true);
|
|
163
|
+
if (result.ok) {
|
|
164
|
+
const data = result.data as { method: string; path: string }[];
|
|
165
|
+
expect(data).toHaveLength(1);
|
|
166
|
+
expect(data[0].method).toBe("POST");
|
|
167
|
+
expect(data[0].path).toBe("/api/orders");
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("onMiss should reject sunset services", async () => {
|
|
172
|
+
const record = parseSkillMd("old-api.com", ACME_SKILL);
|
|
173
|
+
(record as any).status = "sunset";
|
|
174
|
+
(record as any).domain = "old-api.com";
|
|
175
|
+
await store.put("old-api.com", record);
|
|
176
|
+
|
|
177
|
+
const { onMiss } = createRegistryResolver(store);
|
|
178
|
+
const added = await onMiss("/old-api.com/test", () => {});
|
|
179
|
+
expect(added).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should accept RegistryResolverOptions object", async () => {
|
|
183
|
+
const { onMiss, listDomains } = createRegistryResolver({ store });
|
|
184
|
+
const domains = await listDomains();
|
|
185
|
+
expect(domains).toContain("acme-store.com");
|
|
186
|
+
|
|
187
|
+
let mounted = false;
|
|
188
|
+
await onMiss("/acme-store.com/test", () => { mounted = true; });
|
|
189
|
+
expect(mounted).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("extractDomainPath", () => {
|
|
194
|
+
it("should extract domain without version", () => {
|
|
195
|
+
expect(extractDomainPath("/api.cloudflare.com/zones/")).toEqual({
|
|
196
|
+
domain: "api.cloudflare.com",
|
|
197
|
+
version: null,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should extract domain with @version", () => {
|
|
202
|
+
expect(extractDomainPath("/api.cloudflare.com@v5/zones/")).toEqual({
|
|
203
|
+
domain: "api.cloudflare.com",
|
|
204
|
+
version: "v5",
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should return null for empty path", () => {
|
|
209
|
+
expect(extractDomainPath("/")).toEqual({
|
|
210
|
+
domain: null,
|
|
211
|
+
version: null,
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should handle bare domain", () => {
|
|
216
|
+
expect(extractDomainPath("/acme.com")).toEqual({
|
|
217
|
+
domain: "acme.com",
|
|
218
|
+
version: null,
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should handle domain@version without trailing path", () => {
|
|
223
|
+
expect(extractDomainPath("/acme.com@2.0")).toEqual({
|
|
224
|
+
domain: "acme.com",
|
|
225
|
+
version: "2.0",
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { compileRpcDef } from "../../src/registry/rpc-compiler.js";
|
|
3
|
+
import type { RpcManifestDef } from "../../src/onboard/types.js";
|
|
4
|
+
|
|
5
|
+
const ETH_RPC_DEF: RpcManifestDef = {
|
|
6
|
+
url: "https://rpc.ankr.com/eth",
|
|
7
|
+
convention: "evm",
|
|
8
|
+
methods: [
|
|
9
|
+
{ rpcMethod: "eth_blockNumber", description: "Returns the latest block number", resource: "blocks", fsOp: "list" },
|
|
10
|
+
{ rpcMethod: "eth_getBlockByNumber", description: "Returns block by number", resource: "blocks", fsOp: "read" },
|
|
11
|
+
{ rpcMethod: "eth_getBalance", description: "Returns account balance in wei", resource: "balances", fsOp: "read" },
|
|
12
|
+
{ rpcMethod: "eth_gasPrice", description: "Returns current gas price in wei" },
|
|
13
|
+
{ rpcMethod: "net_version", description: "Returns the network ID" },
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe("compileRpcDef", () => {
|
|
18
|
+
it("should create a ServiceRecord with RPC endpoints", () => {
|
|
19
|
+
const { record } = compileRpcDef("rpc.ankr.com", ETH_RPC_DEF);
|
|
20
|
+
expect(record.domain).toBe("rpc.ankr.com");
|
|
21
|
+
expect(record.endpoints).toHaveLength(5);
|
|
22
|
+
expect(record.endpoints[0]).toMatchObject({
|
|
23
|
+
method: "RPC",
|
|
24
|
+
path: "eth_blockNumber",
|
|
25
|
+
description: "Returns the latest block number",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should set source type to jsonrpc", () => {
|
|
30
|
+
const { record } = compileRpcDef("rpc.ankr.com", ETH_RPC_DEF);
|
|
31
|
+
expect(record.source?.type).toBe("jsonrpc");
|
|
32
|
+
expect(record.source?.url).toBe("https://rpc.ankr.com/eth");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should populate rpc meta with resources", () => {
|
|
36
|
+
const { record } = compileRpcDef("rpc.ankr.com", ETH_RPC_DEF);
|
|
37
|
+
const rpc = record.source?.rpc;
|
|
38
|
+
expect(rpc).toBeDefined();
|
|
39
|
+
expect(rpc!.rpcUrl).toBe("https://rpc.ankr.com/eth");
|
|
40
|
+
expect(rpc!.convention).toBe("evm");
|
|
41
|
+
|
|
42
|
+
// Should have blocks, balances, plus inferred resources for gasPrice and net_version
|
|
43
|
+
const blockRes = rpc!.resources.find((r) => r.name === "blocks");
|
|
44
|
+
expect(blockRes).toBeDefined();
|
|
45
|
+
expect(blockRes!.methods).toEqual({
|
|
46
|
+
list: "eth_blockNumber",
|
|
47
|
+
read: "eth_getBlockByNumber",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const balanceRes = rpc!.resources.find((r) => r.name === "balances");
|
|
51
|
+
expect(balanceRes).toBeDefined();
|
|
52
|
+
expect(balanceRes!.methods).toEqual({ read: "eth_getBalance" });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should infer resource names from method prefix when not specified", () => {
|
|
56
|
+
const def: RpcManifestDef = {
|
|
57
|
+
url: "https://example.com/rpc",
|
|
58
|
+
methods: [
|
|
59
|
+
{ rpcMethod: "eth_getBlockByNumber", description: "Get block" },
|
|
60
|
+
{ rpcMethod: "eth_sendTransaction", description: "Send tx" },
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
const { record } = compileRpcDef("example.com", def);
|
|
64
|
+
const rpc = record.source?.rpc;
|
|
65
|
+
expect(rpc!.convention).toBe("raw");
|
|
66
|
+
// eth_getBlockByNumber → "blocks", eth_sendTransaction → "transactions"
|
|
67
|
+
const names = rpc!.resources.map((r) => r.name);
|
|
68
|
+
expect(names).toContain("blocks");
|
|
69
|
+
expect(names).toContain("transactions");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should generate skillMd with RPC Methods section", () => {
|
|
73
|
+
const { skillMd } = compileRpcDef("rpc.ankr.com", ETH_RPC_DEF);
|
|
74
|
+
expect(skillMd).toContain("# rpc.ankr.com");
|
|
75
|
+
expect(skillMd).toContain("## RPC Methods");
|
|
76
|
+
expect(skillMd).toContain("eth_blockNumber");
|
|
77
|
+
expect(skillMd).toContain("eth_getBalance");
|
|
78
|
+
expect(skillMd).toContain("Convention: evm");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should use custom version and isFirstParty", () => {
|
|
82
|
+
const { record } = compileRpcDef("rpc.ankr.com", ETH_RPC_DEF, {
|
|
83
|
+
version: "2.0",
|
|
84
|
+
isFirstParty: true,
|
|
85
|
+
});
|
|
86
|
+
expect(record.version).toBe("2.0");
|
|
87
|
+
expect(record.isFirstParty).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should set status active and isDefault true", () => {
|
|
91
|
+
const { record } = compileRpcDef("rpc.ankr.com", ETH_RPC_DEF);
|
|
92
|
+
expect(record.status).toBe("active");
|
|
93
|
+
expect(record.isDefault).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should handle methods without resource or fsOp", () => {
|
|
97
|
+
const def: RpcManifestDef = {
|
|
98
|
+
url: "https://example.com/rpc",
|
|
99
|
+
methods: [
|
|
100
|
+
{ rpcMethod: "net_version", description: "Network version" },
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
const { record } = compileRpcDef("example.com", def);
|
|
104
|
+
expect(record.endpoints).toHaveLength(1);
|
|
105
|
+
expect(record.endpoints[0]).toMatchObject({ method: "RPC", path: "net_version" });
|
|
106
|
+
// "net_version" → action "version" → pluralized "versions"
|
|
107
|
+
const rpc = record.source?.rpc;
|
|
108
|
+
const res = rpc!.resources.find((r) => r.name === "versions");
|
|
109
|
+
expect(res).toBeDefined();
|
|
110
|
+
expect(res!.methods).toEqual({});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// packages/gateway/test/registry/skill-parser.test.ts
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { parseSkillMd, parsePricingAnnotation } from "../../src/registry/skill-parser.js";
|
|
4
|
+
|
|
5
|
+
const SAMPLE_SKILL_MD = `---
|
|
6
|
+
name: "Acme Store"
|
|
7
|
+
gateway: nkmc
|
|
8
|
+
version: "1.0"
|
|
9
|
+
roles:
|
|
10
|
+
- agent
|
|
11
|
+
- premium
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Acme Store
|
|
15
|
+
|
|
16
|
+
An e-commerce store for widgets.
|
|
17
|
+
|
|
18
|
+
## Schema
|
|
19
|
+
|
|
20
|
+
### products (読: public / 写: agent)
|
|
21
|
+
|
|
22
|
+
Product catalog.
|
|
23
|
+
|
|
24
|
+
| field | type | description |
|
|
25
|
+
|-------|------|-------------|
|
|
26
|
+
| id | string | Product ID |
|
|
27
|
+
| name | string | Product name |
|
|
28
|
+
| price | number | Price in USD |
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
### List products
|
|
33
|
+
|
|
34
|
+
\`GET /api/products\` — 免費,public
|
|
35
|
+
|
|
36
|
+
Returns all products.
|
|
37
|
+
|
|
38
|
+
### Create order
|
|
39
|
+
|
|
40
|
+
\`POST /api/orders\` — 0.05 USDC / 次,agent
|
|
41
|
+
|
|
42
|
+
Creates a new order.
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
describe("parseSkillMd", () => {
|
|
46
|
+
it("should extract frontmatter fields", () => {
|
|
47
|
+
const result = parseSkillMd("acme-store.com", SAMPLE_SKILL_MD);
|
|
48
|
+
expect(result.domain).toBe("acme-store.com");
|
|
49
|
+
expect(result.name).toBe("Acme Store");
|
|
50
|
+
expect(result.version).toBe("1.0");
|
|
51
|
+
expect(result.roles).toEqual(["agent", "premium"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should extract description from first paragraph", () => {
|
|
55
|
+
const result = parseSkillMd("acme-store.com", SAMPLE_SKILL_MD);
|
|
56
|
+
expect(result.description).toContain("e-commerce store");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should extract endpoint summaries from API section", () => {
|
|
60
|
+
const result = parseSkillMd("acme-store.com", SAMPLE_SKILL_MD);
|
|
61
|
+
expect(result.endpoints).toHaveLength(2);
|
|
62
|
+
expect(result.endpoints[0]).toMatchObject({
|
|
63
|
+
method: "GET",
|
|
64
|
+
path: "/api/products",
|
|
65
|
+
description: "List products",
|
|
66
|
+
});
|
|
67
|
+
expect(result.endpoints[1]).toMatchObject({
|
|
68
|
+
method: "POST",
|
|
69
|
+
path: "/api/orders",
|
|
70
|
+
description: "Create order",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should extract pricing from endpoint annotation", () => {
|
|
75
|
+
const result = parseSkillMd("acme-store.com", SAMPLE_SKILL_MD);
|
|
76
|
+
expect(result.endpoints[1].pricing).toEqual({
|
|
77
|
+
cost: 0.05,
|
|
78
|
+
currency: "USDC",
|
|
79
|
+
per: "call",
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should not have pricing for free endpoints", () => {
|
|
84
|
+
const result = parseSkillMd("acme-store.com", SAMPLE_SKILL_MD);
|
|
85
|
+
expect(result.endpoints[0].pricing).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should store the raw skill.md", () => {
|
|
89
|
+
const result = parseSkillMd("acme-store.com", SAMPLE_SKILL_MD);
|
|
90
|
+
expect(result.skillMd).toBe(SAMPLE_SKILL_MD);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should default isFirstParty to false", () => {
|
|
94
|
+
const result = parseSkillMd("acme-store.com", SAMPLE_SKILL_MD);
|
|
95
|
+
expect(result.isFirstParty).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should accept isFirstParty override", () => {
|
|
99
|
+
const result = parseSkillMd("memory", SAMPLE_SKILL_MD, { isFirstParty: true });
|
|
100
|
+
expect(result.isFirstParty).toBe(true);
|
|
101
|
+
expect(result.domain).toBe("memory");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should handle minimal skill.md", () => {
|
|
105
|
+
const minimal = `---\nname: "Minimal"\ngateway: nkmc\nversion: "0.1"\nroles: [agent]\n---\n\n# Minimal\n\nA minimal service.\n`;
|
|
106
|
+
const result = parseSkillMd("minimal.com", minimal);
|
|
107
|
+
expect(result.name).toBe("Minimal");
|
|
108
|
+
expect(result.endpoints).toEqual([]);
|
|
109
|
+
expect(result.description).toContain("minimal service");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should set status to active and isDefault to true", () => {
|
|
113
|
+
const result = parseSkillMd("acme-store.com", SAMPLE_SKILL_MD);
|
|
114
|
+
expect(result.status).toBe("active");
|
|
115
|
+
expect(result.isDefault).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("parsePricingAnnotation", () => {
|
|
120
|
+
it("should parse USDC per call", () => {
|
|
121
|
+
expect(parsePricingAnnotation("0.05 USDC / call")).toEqual({
|
|
122
|
+
cost: 0.05,
|
|
123
|
+
currency: "USDC",
|
|
124
|
+
per: "call",
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should parse 次 as call", () => {
|
|
129
|
+
expect(parsePricingAnnotation("0.05 USDC / 次")).toEqual({
|
|
130
|
+
cost: 0.05,
|
|
131
|
+
currency: "USDC",
|
|
132
|
+
per: "call",
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should parse per byte", () => {
|
|
137
|
+
expect(parsePricingAnnotation("0.001 ETH / byte")).toEqual({
|
|
138
|
+
cost: 0.001,
|
|
139
|
+
currency: "ETH",
|
|
140
|
+
per: "byte",
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should return undefined for free text", () => {
|
|
145
|
+
expect(parsePricingAnnotation("免費,public")).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should return undefined for empty string", () => {
|
|
149
|
+
expect(parsePricingAnnotation("")).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// packages/gateway/test/registry/skill-to-config.test.ts
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { skillToHttpConfig } from "../../src/registry/skill-to-config.js";
|
|
4
|
+
import type { ServiceRecord } from "../../src/registry/types.js";
|
|
5
|
+
|
|
6
|
+
function makeRecord(domain: string, skillMd: string): ServiceRecord {
|
|
7
|
+
return {
|
|
8
|
+
domain,
|
|
9
|
+
name: domain,
|
|
10
|
+
description: "test",
|
|
11
|
+
version: "1.0",
|
|
12
|
+
roles: ["agent"],
|
|
13
|
+
skillMd,
|
|
14
|
+
endpoints: [],
|
|
15
|
+
isFirstParty: false,
|
|
16
|
+
createdAt: Date.now(),
|
|
17
|
+
updatedAt: Date.now(),
|
|
18
|
+
status: "active",
|
|
19
|
+
isDefault: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("skillToHttpConfig", () => {
|
|
24
|
+
it("should set baseUrl from domain with https", () => {
|
|
25
|
+
const record = makeRecord("acme-store.com", `---
|
|
26
|
+
name: "Acme"
|
|
27
|
+
gateway: nkmc
|
|
28
|
+
version: "1.0"
|
|
29
|
+
roles: [agent]
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# Acme
|
|
33
|
+
|
|
34
|
+
Test.
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
### List products
|
|
39
|
+
|
|
40
|
+
\`GET /api/products\` — 免费,public
|
|
41
|
+
`);
|
|
42
|
+
const config = skillToHttpConfig(record);
|
|
43
|
+
expect(config.baseUrl).toBe("https://acme-store.com");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should create endpoints from API section", () => {
|
|
47
|
+
const record = makeRecord("acme-store.com", `---
|
|
48
|
+
name: "Acme"
|
|
49
|
+
gateway: nkmc
|
|
50
|
+
version: "1.0"
|
|
51
|
+
roles: [agent]
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
# Acme
|
|
55
|
+
|
|
56
|
+
Test.
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
### List products
|
|
61
|
+
|
|
62
|
+
\`GET /api/products\` — 免费,public
|
|
63
|
+
|
|
64
|
+
### Create order
|
|
65
|
+
|
|
66
|
+
\`POST /api/orders\` — 0.05 USDC / 次,agent
|
|
67
|
+
`);
|
|
68
|
+
const config = skillToHttpConfig(record);
|
|
69
|
+
expect(config.endpoints).toHaveLength(2);
|
|
70
|
+
expect(config.endpoints![0]).toEqual({
|
|
71
|
+
name: "list-products",
|
|
72
|
+
method: "GET",
|
|
73
|
+
apiPath: "/api/products",
|
|
74
|
+
description: "List products",
|
|
75
|
+
});
|
|
76
|
+
expect(config.endpoints![1]).toEqual({
|
|
77
|
+
name: "create-order",
|
|
78
|
+
method: "POST",
|
|
79
|
+
apiPath: "/api/orders",
|
|
80
|
+
description: "Create order",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should create resources from Schema section", () => {
|
|
85
|
+
const record = makeRecord("acme-store.com", `---
|
|
86
|
+
name: "Acme"
|
|
87
|
+
gateway: nkmc
|
|
88
|
+
version: "1.0"
|
|
89
|
+
roles: [agent]
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
# Acme
|
|
93
|
+
|
|
94
|
+
Test.
|
|
95
|
+
|
|
96
|
+
## Schema
|
|
97
|
+
|
|
98
|
+
### products (読: public / 写: agent)
|
|
99
|
+
|
|
100
|
+
Product catalog.
|
|
101
|
+
|
|
102
|
+
| field | type | description |
|
|
103
|
+
|-------|------|-------------|
|
|
104
|
+
| id | string | Product ID |
|
|
105
|
+
| name | string | Product name |
|
|
106
|
+
`);
|
|
107
|
+
const config = skillToHttpConfig(record);
|
|
108
|
+
expect(config.resources).toHaveLength(1);
|
|
109
|
+
expect(config.resources![0].name).toBe("products");
|
|
110
|
+
expect(config.resources![0].apiPath).toBe("/products");
|
|
111
|
+
expect(config.resources![0].fields).toEqual([
|
|
112
|
+
{ name: "id", type: "string", description: "Product ID" },
|
|
113
|
+
{ name: "name", type: "string", description: "Product name" },
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should include basePath in baseUrl when source has basePath", () => {
|
|
118
|
+
const record = makeRecord("api.cloudflare.com", `---
|
|
119
|
+
name: "Cloudflare"
|
|
120
|
+
gateway: nkmc
|
|
121
|
+
version: "1.0"
|
|
122
|
+
roles: [agent]
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
# Cloudflare
|
|
126
|
+
|
|
127
|
+
Test.
|
|
128
|
+
`);
|
|
129
|
+
record.source = { type: "openapi", basePath: "/client/v4" };
|
|
130
|
+
const config = skillToHttpConfig(record);
|
|
131
|
+
expect(config.baseUrl).toBe("https://api.cloudflare.com/client/v4");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should handle skill.md with no schema or api", () => {
|
|
135
|
+
const record = makeRecord("simple.com", `---
|
|
136
|
+
name: "Simple"
|
|
137
|
+
gateway: nkmc
|
|
138
|
+
version: "1.0"
|
|
139
|
+
roles: [agent]
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
# Simple
|
|
143
|
+
|
|
144
|
+
A simple service.
|
|
145
|
+
`);
|
|
146
|
+
const config = skillToHttpConfig(record);
|
|
147
|
+
expect(config.baseUrl).toBe("https://simple.com");
|
|
148
|
+
expect(config.resources).toEqual([]);
|
|
149
|
+
expect(config.endpoints).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
});
|