@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.
Files changed (130) hide show
  1. package/dist/chunk-56RA53VS.js +37 -0
  2. package/dist/chunk-CZJ75YTV.js +969 -0
  3. package/dist/chunk-QGM4M3NI.js +37 -0
  4. package/dist/http.cjs +1772 -0
  5. package/dist/http.d.cts +49 -0
  6. package/dist/http.d.ts +49 -0
  7. package/dist/http.js +748 -0
  8. package/dist/index.cjs +2436 -0
  9. package/dist/index.d.cts +436 -0
  10. package/dist/index.d.ts +436 -0
  11. package/dist/index.js +1434 -0
  12. package/dist/proxy-ClPcDgsO.d.cts +283 -0
  13. package/dist/proxy-qpda1ANS.d.ts +283 -0
  14. package/dist/proxy.cjs +148 -0
  15. package/dist/proxy.d.cts +6 -0
  16. package/dist/proxy.d.ts +6 -0
  17. package/dist/proxy.js +90 -0
  18. package/dist/testing.cjs +865 -0
  19. package/dist/testing.d.cts +12 -0
  20. package/dist/testing.d.ts +12 -0
  21. package/dist/testing.js +831 -0
  22. package/dist/tunnels-BviBEaih.d.cts +12 -0
  23. package/dist/tunnels-DFHNgmN7.d.ts +12 -0
  24. package/dist/types-C6JC9oTm.d.cts +21 -0
  25. package/dist/types-C6JC9oTm.d.ts +21 -0
  26. package/package.json +47 -0
  27. package/src/__tests__/sqlite-integration.test.ts +384 -0
  28. package/src/credential/d1-vault.ts +134 -0
  29. package/src/credential/memory-vault.ts +50 -0
  30. package/src/credential/types.ts +16 -0
  31. package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
  32. package/src/d1/sqlite-adapter.ts +59 -0
  33. package/src/d1/types.ts +22 -0
  34. package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
  35. package/src/federation/__tests__/peer-client.test.ts +205 -0
  36. package/src/federation/__tests__/peer-store.test.ts +114 -0
  37. package/src/federation/d1-peer-store.ts +164 -0
  38. package/src/federation/peer-backend.ts +60 -0
  39. package/src/federation/peer-client.ts +122 -0
  40. package/src/federation/peer-store.ts +45 -0
  41. package/src/federation/types.ts +39 -0
  42. package/src/http/app.ts +152 -0
  43. package/src/http/lib/dns.ts +30 -0
  44. package/src/http/middleware/admin-auth.ts +18 -0
  45. package/src/http/middleware/agent-auth.ts +27 -0
  46. package/src/http/middleware/publish-auth.ts +39 -0
  47. package/src/http/routes/__tests__/federation.test.ts +364 -0
  48. package/src/http/routes/__tests__/peers.test.ts +290 -0
  49. package/src/http/routes/__tests__/proxy.test.ts +159 -0
  50. package/src/http/routes/auth.ts +39 -0
  51. package/src/http/routes/byok.ts +62 -0
  52. package/src/http/routes/credentials.ts +40 -0
  53. package/src/http/routes/domains.ts +174 -0
  54. package/src/http/routes/federation.ts +170 -0
  55. package/src/http/routes/fs.ts +89 -0
  56. package/src/http/routes/peers.ts +103 -0
  57. package/src/http/routes/proxy.ts +57 -0
  58. package/src/http/routes/registry.ts +222 -0
  59. package/src/http/routes/tunnels.ts +124 -0
  60. package/src/http.ts +9 -0
  61. package/src/index.ts +63 -0
  62. package/src/metering/d1-store.ts +123 -0
  63. package/src/metering/memory-store.ts +29 -0
  64. package/src/metering/pricing-guard.ts +68 -0
  65. package/src/metering/types.ts +25 -0
  66. package/src/onboard/apis-guru.ts +64 -0
  67. package/src/onboard/index.ts +4 -0
  68. package/src/onboard/manifest.ts +362 -0
  69. package/src/onboard/pipeline.ts +214 -0
  70. package/src/onboard/types.ts +72 -0
  71. package/src/proxy/__tests__/tool-registry.test.ts +93 -0
  72. package/src/proxy/tool-registry.ts +122 -0
  73. package/src/proxy.ts +12 -0
  74. package/src/registry/context7-backend.ts +93 -0
  75. package/src/registry/context7.ts +54 -0
  76. package/src/registry/d1-store.ts +242 -0
  77. package/src/registry/memory-store.ts +101 -0
  78. package/src/registry/openapi-compiler.ts +284 -0
  79. package/src/registry/resolver.ts +196 -0
  80. package/src/registry/rpc-compiler.ts +142 -0
  81. package/src/registry/skill-parser.ts +119 -0
  82. package/src/registry/skill-to-config.ts +239 -0
  83. package/src/registry/source-refresher.ts +83 -0
  84. package/src/registry/types.ts +129 -0
  85. package/src/registry/virtual-files.ts +76 -0
  86. package/src/testing/sqlite-d1.ts +64 -0
  87. package/src/testing.ts +2 -0
  88. package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
  89. package/src/tunnel/__tests__/tunnel.test.ts +542 -0
  90. package/src/tunnel/cloudflare-provider.ts +121 -0
  91. package/src/tunnel/memory-store.ts +30 -0
  92. package/src/tunnel/types.ts +28 -0
  93. package/test/credential/d1-vault.test.ts +127 -0
  94. package/test/credential/injection.test.ts +67 -0
  95. package/test/credential/memory-vault.test.ts +63 -0
  96. package/test/http/app.test.ts +300 -0
  97. package/test/http/byok-e2e.test.ts +240 -0
  98. package/test/http/byok.test.ts +115 -0
  99. package/test/http/credentials.test.ts +57 -0
  100. package/test/http/e2e.test.ts +260 -0
  101. package/test/integration/authenticated-apis.test.ts +185 -0
  102. package/test/integration/free-apis-e2e.test.ts +222 -0
  103. package/test/metering/d1-store.test.ts +82 -0
  104. package/test/metering/memory-store.test.ts +76 -0
  105. package/test/metering/pricing-guard.test.ts +108 -0
  106. package/test/onboard/apis-guru.test.ts +57 -0
  107. package/test/onboard/e2e.test.ts +70 -0
  108. package/test/onboard/pipeline.test.ts +318 -0
  109. package/test/onboard/real-apis.test.ts +483 -0
  110. package/test/registry/compilation-correctness.test.ts +132 -0
  111. package/test/registry/context7-backend.test.ts +88 -0
  112. package/test/registry/context7-e2e.test.ts +92 -0
  113. package/test/registry/context7.test.ts +73 -0
  114. package/test/registry/d1-store.test.ts +184 -0
  115. package/test/registry/integration.test.ts +129 -0
  116. package/test/registry/lazy-mount.test.ts +138 -0
  117. package/test/registry/memory-store.test.ts +171 -0
  118. package/test/registry/openapi-compiler.test.ts +267 -0
  119. package/test/registry/openapi-e2e.test.ts +154 -0
  120. package/test/registry/passthrough-e2e.test.ts +109 -0
  121. package/test/registry/resolver-peer.test.ts +299 -0
  122. package/test/registry/resolver.test.ts +228 -0
  123. package/test/registry/rpc-compiler.test.ts +112 -0
  124. package/test/registry/skill-parser.test.ts +151 -0
  125. package/test/registry/skill-to-config.test.ts +151 -0
  126. package/test/registry/skill-to-rpc-config.test.ts +142 -0
  127. package/test/registry/source-refresher.test.ts +90 -0
  128. package/test/registry/virtual-files.test.ts +96 -0
  129. package/tsconfig.json +4 -0
  130. 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
+ });