@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,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
|
+
});
|