@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,240 @@
1
+ import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
2
+ import { createGateway } from "../../src/http/app.js";
3
+ import { D1RegistryStore } from "../../src/registry/d1-store.js";
4
+ import { D1CredentialVault } from "../../src/credential/d1-vault.js";
5
+ import { SqliteD1 } from "../../src/testing/sqlite-d1.js";
6
+ import { generateGatewayKeyPair, createTestToken } from "@nkmc/core/testing";
7
+ import type { GatewayKeyPair } from "@nkmc/core";
8
+
9
+ const ADMIN_TOKEN = "byok-e2e-admin";
10
+
11
+ const SKILL_MD = `---
12
+ name: "Secured API"
13
+ gateway: nkmc
14
+ version: "1.0"
15
+ roles: [agent]
16
+ ---
17
+
18
+ # Secured API
19
+
20
+ An API that requires authentication.
21
+
22
+ ## Schema
23
+
24
+ ### data (read: agent)
25
+
26
+ Some data.
27
+
28
+ | field | type | description |
29
+ |-------|------|-------------|
30
+ | id | string | ID |
31
+
32
+ ## API
33
+
34
+ ### List data
35
+
36
+ \`GET /api/data\` — agent
37
+
38
+ Returns data.
39
+ `;
40
+
41
+ describe("BYOK E2E", () => {
42
+ let keys: GatewayKeyPair;
43
+ let encryptionKey: CryptoKey;
44
+
45
+ beforeAll(async () => {
46
+ keys = await generateGatewayKeyPair();
47
+ encryptionKey = await crypto.subtle.generateKey(
48
+ { name: "AES-GCM", length: 256 },
49
+ false,
50
+ ["encrypt", "decrypt"],
51
+ );
52
+ });
53
+
54
+ let db: SqliteD1;
55
+ let store: D1RegistryStore;
56
+ let vault: D1CredentialVault;
57
+
58
+ beforeEach(async () => {
59
+ db = new SqliteD1();
60
+ store = new D1RegistryStore(db);
61
+ await store.initSchema();
62
+ vault = new D1CredentialVault(db, encryptionKey);
63
+ await vault.initSchema();
64
+ });
65
+
66
+ afterEach(() => {
67
+ db.close();
68
+ });
69
+
70
+ function createApp() {
71
+ return createGateway({
72
+ store,
73
+ vault,
74
+ gatewayPrivateKey: keys.privateKey,
75
+ gatewayPublicKey: keys.publicKey,
76
+ adminToken: ADMIN_TOKEN,
77
+ });
78
+ }
79
+
80
+ it("agent uploads BYOK → resolver uses BYOK credential over pool", async () => {
81
+ const app = createApp();
82
+
83
+ // 1. Register a service
84
+ await app.request("/registry/services?domain=secured-api.com", {
85
+ method: "POST",
86
+ headers: {
87
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
88
+ "Content-Type": "text/markdown",
89
+ },
90
+ body: SKILL_MD,
91
+ });
92
+
93
+ // 2. Set a pool credential (shared fallback)
94
+ await app.request("/credentials/secured-api.com", {
95
+ method: "PUT",
96
+ headers: {
97
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
98
+ "Content-Type": "application/json",
99
+ },
100
+ body: JSON.stringify({ auth: { type: "bearer", token: "pool-token-shared" } }),
101
+ });
102
+
103
+ // 3. Agent obtains JWT
104
+ const authRes = await app.request("/auth/token", {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify({ sub: "agent-byok-1", svc: "gateway", roles: ["agent"] }),
108
+ });
109
+ const { token: agentJwt } = (await authRes.json()) as { token: string };
110
+
111
+ // 4. Agent uploads BYOK credential
112
+ const byokRes = await app.request("/byok/secured-api.com", {
113
+ method: "PUT",
114
+ headers: {
115
+ Authorization: `Bearer ${agentJwt}`,
116
+ "Content-Type": "application/json",
117
+ },
118
+ body: JSON.stringify({ auth: { type: "bearer", token: "byok-agent-secret" } }),
119
+ });
120
+ expect(byokRes.status).toBe(200);
121
+
122
+ // 5. Verify BYOK takes priority: vault.get(domain, agentId) → BYOK
123
+ const cred = await vault.get("secured-api.com", "agent-byok-1");
124
+ expect(cred).not.toBeNull();
125
+ expect(cred!.scope).toBe("byok");
126
+ expect(cred!.auth).toEqual({ type: "bearer", token: "byok-agent-secret" });
127
+
128
+ // 6. Verify pool still available for other agents
129
+ const poolCred = await vault.get("secured-api.com");
130
+ expect(poolCred!.scope).toBe("pool");
131
+ expect(poolCred!.auth).toEqual({ type: "bearer", token: "pool-token-shared" });
132
+ });
133
+
134
+ it("agent lists only their own BYOK domains", async () => {
135
+ const app = createApp();
136
+
137
+ // Two agents
138
+ const auth1 = await app.request("/auth/token", {
139
+ method: "POST",
140
+ headers: { "Content-Type": "application/json" },
141
+ body: JSON.stringify({ sub: "agent-a", svc: "gateway", roles: ["agent"] }),
142
+ });
143
+ const jwt1 = ((await auth1.json()) as { token: string }).token;
144
+
145
+ const auth2 = await app.request("/auth/token", {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: JSON.stringify({ sub: "agent-b", svc: "gateway", roles: ["agent"] }),
149
+ });
150
+ const jwt2 = ((await auth2.json()) as { token: string }).token;
151
+
152
+ // Agent A uploads 2 BYOK keys
153
+ for (const domain of ["api.openai.com", "api.github.com"]) {
154
+ await app.request(`/byok/${domain}`, {
155
+ method: "PUT",
156
+ headers: {
157
+ Authorization: `Bearer ${jwt1}`,
158
+ "Content-Type": "application/json",
159
+ },
160
+ body: JSON.stringify({ auth: { type: "bearer", token: `key-a-${domain}` } }),
161
+ });
162
+ }
163
+
164
+ // Agent B uploads 1 BYOK key
165
+ await app.request("/byok/api.stripe.com", {
166
+ method: "PUT",
167
+ headers: {
168
+ Authorization: `Bearer ${jwt2}`,
169
+ "Content-Type": "application/json",
170
+ },
171
+ body: JSON.stringify({ auth: { type: "bearer", token: "key-b-stripe" } }),
172
+ });
173
+
174
+ // Agent A lists → sees only their 2 domains
175
+ const listA = await app.request("/byok", {
176
+ headers: { Authorization: `Bearer ${jwt1}` },
177
+ });
178
+ const bodyA = (await listA.json()) as { domains: string[] };
179
+ expect(bodyA.domains.sort()).toEqual(["api.github.com", "api.openai.com"]);
180
+
181
+ // Agent B lists → sees only their 1 domain
182
+ const listB = await app.request("/byok", {
183
+ headers: { Authorization: `Bearer ${jwt2}` },
184
+ });
185
+ const bodyB = (await listB.json()) as { domains: string[] };
186
+ expect(bodyB.domains).toEqual(["api.stripe.com"]);
187
+ });
188
+
189
+ it("agent deletes BYOK → falls back to pool", async () => {
190
+ const app = createApp();
191
+
192
+ // Pool credential
193
+ await vault.putPool("api.example.com", { type: "bearer", token: "pool-tok" });
194
+
195
+ // Agent JWT
196
+ const authRes = await app.request("/auth/token", {
197
+ method: "POST",
198
+ headers: { "Content-Type": "application/json" },
199
+ body: JSON.stringify({ sub: "agent-del", svc: "gateway", roles: ["agent"] }),
200
+ });
201
+ const jwt = ((await authRes.json()) as { token: string }).token;
202
+
203
+ // Upload BYOK
204
+ await app.request("/byok/api.example.com", {
205
+ method: "PUT",
206
+ headers: {
207
+ Authorization: `Bearer ${jwt}`,
208
+ "Content-Type": "application/json",
209
+ },
210
+ body: JSON.stringify({ auth: { type: "bearer", token: "byok-tok" } }),
211
+ });
212
+
213
+ // BYOK takes priority
214
+ let cred = await vault.get("api.example.com", "agent-del");
215
+ expect(cred!.auth).toEqual({ type: "bearer", token: "byok-tok" });
216
+
217
+ // Delete BYOK
218
+ const delRes = await app.request("/byok/api.example.com", {
219
+ method: "DELETE",
220
+ headers: { Authorization: `Bearer ${jwt}` },
221
+ });
222
+ expect(delRes.status).toBe(200);
223
+
224
+ // Falls back to pool
225
+ cred = await vault.get("api.example.com", "agent-del");
226
+ expect(cred!.scope).toBe("pool");
227
+ expect(cred!.auth).toEqual({ type: "bearer", token: "pool-tok" });
228
+ });
229
+
230
+ it("BYOK requires agent auth — rejects unauthenticated requests", async () => {
231
+ const app = createApp();
232
+
233
+ const res = await app.request("/byok/api.example.com", {
234
+ method: "PUT",
235
+ headers: { "Content-Type": "application/json" },
236
+ body: JSON.stringify({ auth: { type: "bearer", token: "x" } }),
237
+ });
238
+ expect(res.status).toBe(401);
239
+ });
240
+ });
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { Hono } from "hono";
3
+ import { byokRoutes } from "../../src/http/routes/byok.js";
4
+ import { MemoryCredentialVault } from "../../src/credential/memory-vault.js";
5
+
6
+ type Env = {
7
+ Variables: {
8
+ agent: { id: string; roles: string[] };
9
+ };
10
+ };
11
+
12
+ describe("BYOK Routes", () => {
13
+ let vault: MemoryCredentialVault;
14
+ let app: Hono<Env>;
15
+ const agentId = "agent-test-1";
16
+
17
+ beforeEach(() => {
18
+ vault = new MemoryCredentialVault();
19
+ app = new Hono<Env>();
20
+ // Simulate agentAuth middleware — inject agent context
21
+ app.use("/*", async (c, next) => {
22
+ c.set("agent", { id: agentId, roles: ["agent"] });
23
+ await next();
24
+ });
25
+ app.route("/byok", byokRoutes({ vault }));
26
+ });
27
+
28
+ it("should upload BYOK credential via PUT", async () => {
29
+ const res = await app.request("/byok/api.openai.com", {
30
+ method: "PUT",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({ auth: { type: "bearer", token: "sk-test-123" } }),
33
+ });
34
+ expect(res.status).toBe(200);
35
+ const body = (await res.json()) as any;
36
+ expect(body.ok).toBe(true);
37
+
38
+ // Verify credential is stored as BYOK for this agent
39
+ const cred = await vault.get("api.openai.com", agentId);
40
+ expect(cred).not.toBeNull();
41
+ expect(cred!.scope).toBe("byok");
42
+ expect(cred!.developerId).toBe(agentId);
43
+ expect(cred!.auth).toEqual({ type: "bearer", token: "sk-test-123" });
44
+ });
45
+
46
+ it("should list only this agent's BYOK domains", async () => {
47
+ // Agent's BYOK
48
+ await vault.putByok("api.openai.com", agentId, {
49
+ type: "bearer",
50
+ token: "sk-1",
51
+ });
52
+ // Another agent's BYOK
53
+ await vault.putByok("api.github.com", "agent-other", {
54
+ type: "bearer",
55
+ token: "ghp-1",
56
+ });
57
+ // Pool credential (should not appear)
58
+ await vault.putPool("api.stripe.com", {
59
+ type: "bearer",
60
+ token: "sk_pool",
61
+ });
62
+
63
+ const res = await app.request("/byok");
64
+ expect(res.status).toBe(200);
65
+ const body = (await res.json()) as any;
66
+ expect(body.domains).toContain("api.openai.com");
67
+ expect(body.domains).not.toContain("api.github.com");
68
+ expect(body.domains).not.toContain("api.stripe.com");
69
+ });
70
+
71
+ it("should delete agent's BYOK credential", async () => {
72
+ await vault.putByok("api.openai.com", agentId, {
73
+ type: "bearer",
74
+ token: "sk-1",
75
+ });
76
+
77
+ const res = await app.request("/byok/api.openai.com", {
78
+ method: "DELETE",
79
+ });
80
+ expect(res.status).toBe(200);
81
+
82
+ // BYOK should be gone
83
+ const cred = await vault.get("api.openai.com", agentId);
84
+ // Should fall back to pool (which doesn't exist), so null
85
+ expect(cred).toBeNull();
86
+ });
87
+
88
+ it("should reject missing auth.type", async () => {
89
+ const res = await app.request("/byok/api.openai.com", {
90
+ method: "PUT",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify({ auth: {} }),
93
+ });
94
+ expect(res.status).toBe(400);
95
+ });
96
+
97
+ it("BYOK should take priority over pool in vault.get()", async () => {
98
+ await vault.putPool("api.openai.com", {
99
+ type: "bearer",
100
+ token: "pool-token",
101
+ });
102
+ await vault.putByok("api.openai.com", agentId, {
103
+ type: "bearer",
104
+ token: "byok-token",
105
+ });
106
+
107
+ // With developerId → BYOK
108
+ const byok = await vault.get("api.openai.com", agentId);
109
+ expect(byok!.auth).toEqual({ type: "bearer", token: "byok-token" });
110
+
111
+ // Without developerId → pool
112
+ const pool = await vault.get("api.openai.com");
113
+ expect(pool!.auth).toEqual({ type: "bearer", token: "pool-token" });
114
+ });
115
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { Hono } from "hono";
3
+ import { credentialRoutes } from "../../src/http/routes/credentials.js";
4
+ import { MemoryCredentialVault } from "../../src/credential/memory-vault.js";
5
+
6
+ describe("Credential Routes", () => {
7
+ let vault: MemoryCredentialVault;
8
+ let app: Hono;
9
+
10
+ beforeEach(() => {
11
+ vault = new MemoryCredentialVault();
12
+ app = new Hono();
13
+ app.route("/credentials", credentialRoutes({ vault }));
14
+ });
15
+
16
+ it("should set pool credential via PUT", async () => {
17
+ const res = await app.request("/credentials/api.example.com", {
18
+ method: "PUT",
19
+ headers: { "Content-Type": "application/json" },
20
+ body: JSON.stringify({ auth: { type: "bearer", token: "tok_123" } }),
21
+ });
22
+ expect(res.status).toBe(200);
23
+ const body = await res.json() as any;
24
+ expect(body.ok).toBe(true);
25
+
26
+ const cred = await vault.get("api.example.com");
27
+ expect(cred!.auth).toEqual({ type: "bearer", token: "tok_123" });
28
+ });
29
+
30
+ it("should list domains with credentials", async () => {
31
+ await vault.putPool("api.example.com", { type: "bearer", token: "tok" });
32
+ await vault.putPool("other.com", { type: "bearer", token: "tok2" });
33
+
34
+ const res = await app.request("/credentials");
35
+ expect(res.status).toBe(200);
36
+ const body = await res.json() as any;
37
+ expect(body.domains).toContain("api.example.com");
38
+ expect(body.domains).toContain("other.com");
39
+ });
40
+
41
+ it("should delete credential", async () => {
42
+ await vault.putPool("api.example.com", { type: "bearer", token: "tok" });
43
+ const res = await app.request("/credentials/api.example.com", { method: "DELETE" });
44
+ expect(res.status).toBe(200);
45
+
46
+ expect(await vault.get("api.example.com")).toBeNull();
47
+ });
48
+
49
+ it("should reject missing auth.type", async () => {
50
+ const res = await app.request("/credentials/api.example.com", {
51
+ method: "PUT",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({ auth: {} }),
54
+ });
55
+ expect(res.status).toBe(400);
56
+ });
57
+ });
@@ -0,0 +1,260 @@
1
+ import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
2
+ import { createGateway } from "../../src/http/app.js";
3
+ import { D1RegistryStore } from "../../src/registry/d1-store.js";
4
+ import { SqliteD1 } from "../../src/testing/sqlite-d1.js";
5
+ import { generateGatewayKeyPair, createTestToken } from "@nkmc/core/testing";
6
+ import type { GatewayKeyPair } from "@nkmc/core";
7
+
8
+ const SKILL_MD = `---
9
+ name: "Acme Store"
10
+ gateway: nkmc
11
+ version: "1.0"
12
+ roles: [agent]
13
+ ---
14
+
15
+ # Acme Store
16
+
17
+ E-commerce service for products and orders.
18
+
19
+ ## Schema
20
+
21
+ ### products (read: public / write: agent)
22
+
23
+ Product catalog
24
+
25
+ | field | type | description |
26
+ |-------|------|-------------|
27
+ | id | string | Product ID |
28
+ | name | string | Product name |
29
+ | price | number | Price in USD |
30
+
31
+ ## API
32
+
33
+ ### List products
34
+
35
+ \`GET /api/products\` — public
36
+
37
+ Returns all products.
38
+
39
+ ### Create order
40
+
41
+ \`POST /api/orders\` — 0.05 USDC / call, agent
42
+
43
+ Creates a new order.
44
+ `;
45
+
46
+ const ADMIN_TOKEN = "e2e-admin-secret";
47
+
48
+ describe("Gateway E2E", () => {
49
+ let keys: GatewayKeyPair;
50
+
51
+ beforeAll(async () => {
52
+ keys = await generateGatewayKeyPair();
53
+ });
54
+
55
+ let db: SqliteD1;
56
+ let store: D1RegistryStore;
57
+
58
+ beforeEach(async () => {
59
+ db = new SqliteD1();
60
+ store = new D1RegistryStore(db);
61
+ await store.initSchema();
62
+ });
63
+
64
+ afterEach(() => {
65
+ db.close();
66
+ });
67
+
68
+ function createApp() {
69
+ return createGateway({
70
+ store,
71
+ gatewayPrivateKey: keys.privateKey,
72
+ gatewayPublicKey: keys.publicKey,
73
+ adminToken: ADMIN_TOKEN,
74
+ });
75
+ }
76
+
77
+ it("full flow: register → list → auth → execute → fs", async () => {
78
+ const app = createApp();
79
+
80
+ // 1. Admin: Register service via skill.md
81
+ const registerRes = await app.request(
82
+ "/registry/services?domain=acme-store.com",
83
+ {
84
+ method: "POST",
85
+ headers: {
86
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
87
+ "Content-Type": "text/markdown",
88
+ },
89
+ body: SKILL_MD,
90
+ },
91
+ );
92
+ expect(registerRes.status).toBe(201);
93
+ const registerBody = await registerRes.json();
94
+ expect(registerBody).toEqual({
95
+ ok: true,
96
+ domain: "acme-store.com",
97
+ name: "Acme Store",
98
+ });
99
+
100
+ // 2. Admin: List services and verify
101
+ const listRes = await app.request("/registry/services", {
102
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
103
+ });
104
+ expect(listRes.status).toBe(200);
105
+ const list = (await listRes.json()) as Array<{ domain: string }>;
106
+ expect(list).toHaveLength(1);
107
+ expect(list[0].domain).toBe("acme-store.com");
108
+
109
+ // 3. Admin: Get service details
110
+ const detailRes = await app.request(
111
+ "/registry/services/acme-store.com",
112
+ {
113
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
114
+ },
115
+ );
116
+ expect(detailRes.status).toBe(200);
117
+ const detail = (await detailRes.json()) as {
118
+ domain: string;
119
+ name: string;
120
+ endpoints: Array<{ method: string; path: string }>;
121
+ };
122
+ expect(detail.name).toBe("Acme Store");
123
+ expect(detail.endpoints.length).toBeGreaterThan(0);
124
+
125
+ // 4. Auth: Obtain agent JWT
126
+ const authRes = await app.request("/auth/token", {
127
+ method: "POST",
128
+ headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({
130
+ sub: "agent-42",
131
+ svc: "acme-store.com",
132
+ roles: ["agent"],
133
+ }),
134
+ });
135
+ expect(authRes.status).toBe(200);
136
+ const { token } = (await authRes.json()) as { token: string };
137
+ expect(token).toBeTruthy();
138
+
139
+ // 5. Agent: Execute ls / (shows registered domains)
140
+ const execRes = await app.request("/execute", {
141
+ method: "POST",
142
+ headers: {
143
+ Authorization: `Bearer ${token}`,
144
+ "Content-Type": "application/json",
145
+ },
146
+ body: JSON.stringify({ command: "ls /" }),
147
+ });
148
+ expect(execRes.status).toBe(200);
149
+ const execBody = (await execRes.json()) as {
150
+ ok: boolean;
151
+ data: string[];
152
+ };
153
+ expect(execBody.ok).toBe(true);
154
+ // ls returns domain names with trailing slash
155
+ expect(execBody.data).toContain("acme-store.com/");
156
+
157
+ // 6. Agent: GET /fs/ (list root via REST)
158
+ const fsRootRes = await app.request("/fs/", {
159
+ headers: { Authorization: `Bearer ${token}` },
160
+ });
161
+ expect(fsRootRes.status).toBe(200);
162
+ const fsRootBody = (await fsRootRes.json()) as {
163
+ ok: boolean;
164
+ data: string[];
165
+ };
166
+ expect(fsRootBody.ok).toBe(true);
167
+ expect(fsRootBody.data).toContain("acme-store.com/");
168
+ });
169
+
170
+ it("should persist services across requests via D1", async () => {
171
+ const app = createApp();
172
+
173
+ // Register two services
174
+ for (const domain of ["svc-a.com", "svc-b.com"]) {
175
+ const md = `---\nname: ${domain}\ngateway: nkmc\nversion: "1.0"\nroles: [agent]\n---\n# ${domain}\nService ${domain}.`;
176
+ await app.request(`/registry/services?domain=${domain}`, {
177
+ method: "POST",
178
+ headers: {
179
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
180
+ "Content-Type": "text/markdown",
181
+ },
182
+ body: md,
183
+ });
184
+ }
185
+
186
+ // Verify both exist
187
+ const listRes = await app.request("/registry/services", {
188
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
189
+ });
190
+ const list = (await listRes.json()) as Array<{ domain: string }>;
191
+ expect(list).toHaveLength(2);
192
+ expect(list.map((s) => s.domain).sort()).toEqual(["svc-a.com", "svc-b.com"]);
193
+
194
+ // Delete one
195
+ await app.request("/registry/services/svc-a.com", {
196
+ method: "DELETE",
197
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
198
+ });
199
+
200
+ // Verify only one remains
201
+ const listRes2 = await app.request("/registry/services", {
202
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
203
+ });
204
+ const list2 = (await listRes2.json()) as Array<{ domain: string }>;
205
+ expect(list2).toHaveLength(1);
206
+ expect(list2[0].domain).toBe("svc-b.com");
207
+ });
208
+
209
+ it("should search services via registry", async () => {
210
+ const app = createApp();
211
+
212
+ // Register with specific description
213
+ await app.request(
214
+ "/registry/services?domain=weather.io",
215
+ {
216
+ method: "POST",
217
+ headers: {
218
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
219
+ "Content-Type": "text/markdown",
220
+ },
221
+ body: `---\nname: Weather API\ngateway: nkmc\nversion: "1.0"\nroles: [agent]\n---\n# Weather API\nWeather forecasts and climate data.`,
222
+ },
223
+ );
224
+
225
+ await app.request(
226
+ "/registry/services?domain=shop.io",
227
+ {
228
+ method: "POST",
229
+ headers: {
230
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
231
+ "Content-Type": "text/markdown",
232
+ },
233
+ body: `---\nname: Shop API\ngateway: nkmc\nversion: "1.0"\nroles: [agent]\n---\n# Shop API\nOnline shopping platform.`,
234
+ },
235
+ );
236
+
237
+ // Search for weather
238
+ const searchRes = await app.request("/registry/services?q=weather", {
239
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
240
+ });
241
+ expect(searchRes.status).toBe(200);
242
+ const results = (await searchRes.json()) as Array<{ domain: string }>;
243
+ expect(results).toHaveLength(1);
244
+ expect(results[0].domain).toBe("weather.io");
245
+ });
246
+
247
+ it("should reject agent requests with invalid JWT", async () => {
248
+ const app = createApp();
249
+
250
+ const res = await app.request("/execute", {
251
+ method: "POST",
252
+ headers: {
253
+ Authorization: "Bearer invalid-token",
254
+ "Content-Type": "application/json",
255
+ },
256
+ body: JSON.stringify({ command: "ls /" }),
257
+ });
258
+ expect(res.status).toBe(401);
259
+ });
260
+ });