@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,290 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { MemoryPeerStore } from "../../../federation/peer-store.js";
|
|
4
|
+
import { adminAuth } from "../../middleware/admin-auth.js";
|
|
5
|
+
import { peerRoutes } from "../peers.js";
|
|
6
|
+
|
|
7
|
+
const ADMIN_TOKEN = "test-admin-token";
|
|
8
|
+
|
|
9
|
+
function createTestApp(peerStore?: MemoryPeerStore) {
|
|
10
|
+
const store = peerStore ?? new MemoryPeerStore();
|
|
11
|
+
const app = new Hono();
|
|
12
|
+
app.use("/admin/federation/*", adminAuth(ADMIN_TOKEN));
|
|
13
|
+
app.route("/admin/federation", peerRoutes({ peerStore: store }));
|
|
14
|
+
return { app, peerStore: store };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function adminHeaders() {
|
|
18
|
+
return {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
Authorization: `Bearer ${ADMIN_TOKEN}`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("peer admin routes", () => {
|
|
25
|
+
describe("PUT /admin/federation/peers/:id", () => {
|
|
26
|
+
it("creates a new peer", async () => {
|
|
27
|
+
const { app, peerStore } = createTestApp();
|
|
28
|
+
|
|
29
|
+
const res = await app.request("/admin/federation/peers/peer-1", {
|
|
30
|
+
method: "PUT",
|
|
31
|
+
headers: adminHeaders(),
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
name: "Peer One",
|
|
34
|
+
url: "https://peer1.example.com",
|
|
35
|
+
sharedSecret: "secret-123",
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(res.status).toBe(200);
|
|
40
|
+
const body = await res.json();
|
|
41
|
+
expect(body).toEqual({ ok: true, id: "peer-1" });
|
|
42
|
+
|
|
43
|
+
const stored = await peerStore.getPeer("peer-1");
|
|
44
|
+
expect(stored).not.toBeNull();
|
|
45
|
+
expect(stored!.name).toBe("Peer One");
|
|
46
|
+
expect(stored!.url).toBe("https://peer1.example.com");
|
|
47
|
+
expect(stored!.sharedSecret).toBe("secret-123");
|
|
48
|
+
expect(stored!.status).toBe("active");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("updates an existing peer", async () => {
|
|
52
|
+
const peerStore = new MemoryPeerStore();
|
|
53
|
+
await peerStore.putPeer({
|
|
54
|
+
id: "peer-1",
|
|
55
|
+
name: "Old Name",
|
|
56
|
+
url: "https://old.example.com",
|
|
57
|
+
sharedSecret: "old-secret",
|
|
58
|
+
status: "active",
|
|
59
|
+
advertisedDomains: ["api.example.com"],
|
|
60
|
+
lastSeen: 1000,
|
|
61
|
+
createdAt: 500,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const { app } = createTestApp(peerStore);
|
|
65
|
+
|
|
66
|
+
const res = await app.request("/admin/federation/peers/peer-1", {
|
|
67
|
+
method: "PUT",
|
|
68
|
+
headers: adminHeaders(),
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
name: "New Name",
|
|
71
|
+
url: "https://new.example.com",
|
|
72
|
+
sharedSecret: "new-secret",
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(res.status).toBe(200);
|
|
77
|
+
|
|
78
|
+
const stored = await peerStore.getPeer("peer-1");
|
|
79
|
+
expect(stored!.name).toBe("New Name");
|
|
80
|
+
expect(stored!.url).toBe("https://new.example.com");
|
|
81
|
+
// Preserves existing fields
|
|
82
|
+
expect(stored!.advertisedDomains).toEqual(["api.example.com"]);
|
|
83
|
+
expect(stored!.createdAt).toBe(500);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("rejects when missing required fields", async () => {
|
|
87
|
+
const { app } = createTestApp();
|
|
88
|
+
|
|
89
|
+
const res = await app.request("/admin/federation/peers/peer-1", {
|
|
90
|
+
method: "PUT",
|
|
91
|
+
headers: adminHeaders(),
|
|
92
|
+
body: JSON.stringify({ name: "Missing url and secret" }),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(res.status).toBe(400);
|
|
96
|
+
const body = await res.json();
|
|
97
|
+
expect(body.error).toContain("Missing required fields");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("rejects without admin auth", async () => {
|
|
101
|
+
const { app } = createTestApp();
|
|
102
|
+
|
|
103
|
+
const res = await app.request("/admin/federation/peers/peer-1", {
|
|
104
|
+
method: "PUT",
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
name: "Peer",
|
|
108
|
+
url: "https://peer.example.com",
|
|
109
|
+
sharedSecret: "secret",
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(res.status).toBe(401);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("GET /admin/federation/peers", () => {
|
|
118
|
+
it("lists peers without sharedSecret", async () => {
|
|
119
|
+
const peerStore = new MemoryPeerStore();
|
|
120
|
+
await peerStore.putPeer({
|
|
121
|
+
id: "peer-1",
|
|
122
|
+
name: "Peer One",
|
|
123
|
+
url: "https://peer1.example.com",
|
|
124
|
+
sharedSecret: "secret-should-not-appear",
|
|
125
|
+
status: "active",
|
|
126
|
+
advertisedDomains: [],
|
|
127
|
+
lastSeen: 0,
|
|
128
|
+
createdAt: Date.now(),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const { app } = createTestApp(peerStore);
|
|
132
|
+
|
|
133
|
+
const res = await app.request("/admin/federation/peers", {
|
|
134
|
+
headers: adminHeaders(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(res.status).toBe(200);
|
|
138
|
+
const body = await res.json();
|
|
139
|
+
expect(body.peers).toHaveLength(1);
|
|
140
|
+
expect(body.peers[0].id).toBe("peer-1");
|
|
141
|
+
expect(body.peers[0].name).toBe("Peer One");
|
|
142
|
+
expect(body.peers[0]).not.toHaveProperty("sharedSecret");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("DELETE /admin/federation/peers/:id", () => {
|
|
147
|
+
it("removes a peer", async () => {
|
|
148
|
+
const peerStore = new MemoryPeerStore();
|
|
149
|
+
await peerStore.putPeer({
|
|
150
|
+
id: "peer-1",
|
|
151
|
+
name: "Peer One",
|
|
152
|
+
url: "https://peer1.example.com",
|
|
153
|
+
sharedSecret: "secret",
|
|
154
|
+
status: "active",
|
|
155
|
+
advertisedDomains: [],
|
|
156
|
+
lastSeen: 0,
|
|
157
|
+
createdAt: Date.now(),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const { app } = createTestApp(peerStore);
|
|
161
|
+
|
|
162
|
+
const res = await app.request("/admin/federation/peers/peer-1", {
|
|
163
|
+
method: "DELETE",
|
|
164
|
+
headers: adminHeaders(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(res.status).toBe(200);
|
|
168
|
+
const body = await res.json();
|
|
169
|
+
expect(body).toEqual({ ok: true, id: "peer-1" });
|
|
170
|
+
|
|
171
|
+
const stored = await peerStore.getPeer("peer-1");
|
|
172
|
+
expect(stored).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("PUT /admin/federation/rules/:domain", () => {
|
|
177
|
+
it("creates a lending rule", async () => {
|
|
178
|
+
const { app, peerStore } = createTestApp();
|
|
179
|
+
|
|
180
|
+
const res = await app.request(
|
|
181
|
+
"/admin/federation/rules/api.example.com",
|
|
182
|
+
{
|
|
183
|
+
method: "PUT",
|
|
184
|
+
headers: adminHeaders(),
|
|
185
|
+
body: JSON.stringify({
|
|
186
|
+
allow: true,
|
|
187
|
+
peers: ["peer-1", "peer-2"],
|
|
188
|
+
pricing: { mode: "per-request", amount: 100 },
|
|
189
|
+
rateLimit: { requests: 60, window: "minute" },
|
|
190
|
+
}),
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(res.status).toBe(200);
|
|
195
|
+
const body = await res.json();
|
|
196
|
+
expect(body).toEqual({ ok: true, domain: "api.example.com" });
|
|
197
|
+
|
|
198
|
+
const rule = await peerStore.getRule("api.example.com");
|
|
199
|
+
expect(rule).not.toBeNull();
|
|
200
|
+
expect(rule!.allow).toBe(true);
|
|
201
|
+
expect(rule!.peers).toEqual(["peer-1", "peer-2"]);
|
|
202
|
+
expect(rule!.pricing).toEqual({ mode: "per-request", amount: 100 });
|
|
203
|
+
expect(rule!.rateLimit).toEqual({ requests: 60, window: "minute" });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("rejects when missing allow field", async () => {
|
|
207
|
+
const { app } = createTestApp();
|
|
208
|
+
|
|
209
|
+
const res = await app.request(
|
|
210
|
+
"/admin/federation/rules/api.example.com",
|
|
211
|
+
{
|
|
212
|
+
method: "PUT",
|
|
213
|
+
headers: adminHeaders(),
|
|
214
|
+
body: JSON.stringify({ peers: "*" }),
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
expect(res.status).toBe(400);
|
|
219
|
+
const body = await res.json();
|
|
220
|
+
expect(body.error).toContain("allow");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("GET /admin/federation/rules", () => {
|
|
225
|
+
it("lists all lending rules", async () => {
|
|
226
|
+
const peerStore = new MemoryPeerStore();
|
|
227
|
+
await peerStore.putRule({
|
|
228
|
+
domain: "api.example.com",
|
|
229
|
+
allow: true,
|
|
230
|
+
peers: "*",
|
|
231
|
+
pricing: { mode: "free" },
|
|
232
|
+
createdAt: Date.now(),
|
|
233
|
+
updatedAt: Date.now(),
|
|
234
|
+
});
|
|
235
|
+
await peerStore.putRule({
|
|
236
|
+
domain: "github.com",
|
|
237
|
+
allow: false,
|
|
238
|
+
peers: ["peer-1"],
|
|
239
|
+
pricing: { mode: "per-token", amount: 50 },
|
|
240
|
+
createdAt: Date.now(),
|
|
241
|
+
updatedAt: Date.now(),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const { app } = createTestApp(peerStore);
|
|
245
|
+
|
|
246
|
+
const res = await app.request("/admin/federation/rules", {
|
|
247
|
+
headers: adminHeaders(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(res.status).toBe(200);
|
|
251
|
+
const body = await res.json();
|
|
252
|
+
expect(body.rules).toHaveLength(2);
|
|
253
|
+
expect(body.rules.map((r: any) => r.domain).sort()).toEqual([
|
|
254
|
+
"api.example.com",
|
|
255
|
+
"github.com",
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("DELETE /admin/federation/rules/:domain", () => {
|
|
261
|
+
it("removes a lending rule", async () => {
|
|
262
|
+
const peerStore = new MemoryPeerStore();
|
|
263
|
+
await peerStore.putRule({
|
|
264
|
+
domain: "api.example.com",
|
|
265
|
+
allow: true,
|
|
266
|
+
peers: "*",
|
|
267
|
+
pricing: { mode: "free" },
|
|
268
|
+
createdAt: Date.now(),
|
|
269
|
+
updatedAt: Date.now(),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const { app } = createTestApp(peerStore);
|
|
273
|
+
|
|
274
|
+
const res = await app.request(
|
|
275
|
+
"/admin/federation/rules/api.example.com",
|
|
276
|
+
{
|
|
277
|
+
method: "DELETE",
|
|
278
|
+
headers: adminHeaders(),
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(res.status).toBe(200);
|
|
283
|
+
const body = await res.json();
|
|
284
|
+
expect(body).toEqual({ ok: true, domain: "api.example.com" });
|
|
285
|
+
|
|
286
|
+
const rule = await peerStore.getRule("api.example.com");
|
|
287
|
+
expect(rule).toBeNull();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { MemoryCredentialVault } from "../../../credential/memory-vault.js";
|
|
4
|
+
import {
|
|
5
|
+
ToolRegistry,
|
|
6
|
+
createDefaultToolRegistry,
|
|
7
|
+
} from "../../../proxy/tool-registry.js";
|
|
8
|
+
import { proxyRoutes, type ExecResult } from "../proxy.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Helper: create a test app with agent middleware stubbed out
|
|
12
|
+
* so we can test the proxy routes in isolation.
|
|
13
|
+
*/
|
|
14
|
+
function createTestApp(options?: {
|
|
15
|
+
vault?: MemoryCredentialVault;
|
|
16
|
+
toolRegistry?: ToolRegistry;
|
|
17
|
+
exec?: (tool: string, args: string[], env: Record<string, string>) => Promise<ExecResult>;
|
|
18
|
+
}) {
|
|
19
|
+
const vault = options?.vault ?? new MemoryCredentialVault();
|
|
20
|
+
const toolRegistry = options?.toolRegistry ?? createDefaultToolRegistry();
|
|
21
|
+
const exec = options?.exec ?? vi.fn(async () => ({ stdout: "", stderr: "", exitCode: 0 }));
|
|
22
|
+
|
|
23
|
+
type Env = {
|
|
24
|
+
Variables: {
|
|
25
|
+
agent: { id: string; roles: string[] };
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const app = new Hono<Env>();
|
|
30
|
+
|
|
31
|
+
// Stub agent auth — always set a fixed agent identity
|
|
32
|
+
app.use("*", async (c, next) => {
|
|
33
|
+
c.set("agent", { id: "agent-1", roles: ["read"] });
|
|
34
|
+
await next();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.route("/", proxyRoutes({ vault, toolRegistry, exec }));
|
|
38
|
+
|
|
39
|
+
return { app, vault, toolRegistry, exec };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("proxy routes", () => {
|
|
43
|
+
describe("POST /exec", () => {
|
|
44
|
+
it("executes a tool with valid credential and returns stdout", async () => {
|
|
45
|
+
const vault = new MemoryCredentialVault();
|
|
46
|
+
await vault.putPool("github.com", { type: "bearer", token: "ghp_test123" });
|
|
47
|
+
|
|
48
|
+
const exec = vi.fn(async (_tool: string, _args: string[], _env: Record<string, string>) => ({
|
|
49
|
+
stdout: "Hello from gh\n",
|
|
50
|
+
stderr: "",
|
|
51
|
+
exitCode: 0,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const { app } = createTestApp({ vault, exec });
|
|
55
|
+
|
|
56
|
+
const res = await app.request("/exec", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify({ tool: "gh", args: ["auth", "status"] }),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(res.status).toBe(200);
|
|
63
|
+
const body = await res.json();
|
|
64
|
+
expect(body).toEqual({
|
|
65
|
+
stdout: "Hello from gh\n",
|
|
66
|
+
stderr: "",
|
|
67
|
+
exitCode: 0,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Verify exec was called with correct env injection
|
|
71
|
+
expect(exec).toHaveBeenCalledWith(
|
|
72
|
+
"gh",
|
|
73
|
+
["auth", "status"],
|
|
74
|
+
{ GH_TOKEN: "ghp_test123" },
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns 404 for an unknown tool", async () => {
|
|
79
|
+
const { app } = createTestApp();
|
|
80
|
+
|
|
81
|
+
const res = await app.request("/exec", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/json" },
|
|
84
|
+
body: JSON.stringify({ tool: "nonexistent", args: [] }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(res.status).toBe(404);
|
|
88
|
+
const body = await res.json();
|
|
89
|
+
expect(body.error).toContain("Unknown tool");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns 401 when no credential is available", async () => {
|
|
93
|
+
// Vault is empty — no credential for github.com
|
|
94
|
+
const vault = new MemoryCredentialVault();
|
|
95
|
+
const { app } = createTestApp({ vault });
|
|
96
|
+
|
|
97
|
+
const res = await app.request("/exec", {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "Content-Type": "application/json" },
|
|
100
|
+
body: JSON.stringify({ tool: "gh", args: ["pr", "list"] }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(res.status).toBe(401);
|
|
104
|
+
const body = await res.json();
|
|
105
|
+
expect(body.error).toContain("No credential");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns 400 when tool field is missing", async () => {
|
|
109
|
+
const { app } = createTestApp();
|
|
110
|
+
|
|
111
|
+
const res = await app.request("/exec", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json" },
|
|
114
|
+
body: JSON.stringify({ args: ["foo"] }),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(res.status).toBe(400);
|
|
118
|
+
const body = await res.json();
|
|
119
|
+
expect(body.error).toContain("Missing 'tool' field");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("defaults args to empty array when not provided", async () => {
|
|
123
|
+
const vault = new MemoryCredentialVault();
|
|
124
|
+
await vault.putPool("github.com", { type: "bearer", token: "ghp_x" });
|
|
125
|
+
|
|
126
|
+
const exec = vi.fn(async () => ({ stdout: "", stderr: "", exitCode: 0 }));
|
|
127
|
+
const { app } = createTestApp({ vault, exec });
|
|
128
|
+
|
|
129
|
+
const res = await app.request("/exec", {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: { "Content-Type": "application/json" },
|
|
132
|
+
body: JSON.stringify({ tool: "gh" }),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(res.status).toBe(200);
|
|
136
|
+
expect(exec).toHaveBeenCalledWith("gh", [], { GH_TOKEN: "ghp_x" });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("GET /tools", () => {
|
|
141
|
+
it("returns the list of available tools", async () => {
|
|
142
|
+
const { app } = createTestApp();
|
|
143
|
+
|
|
144
|
+
const res = await app.request("/tools", { method: "GET" });
|
|
145
|
+
|
|
146
|
+
expect(res.status).toBe(200);
|
|
147
|
+
const body = await res.json();
|
|
148
|
+
expect(body.tools).toBeInstanceOf(Array);
|
|
149
|
+
expect(body.tools.length).toBe(5);
|
|
150
|
+
|
|
151
|
+
const names = body.tools.map((t: { name: string }) => t.name).sort();
|
|
152
|
+
expect(names).toEqual(["anthropic", "aws", "gh", "openai", "stripe"]);
|
|
153
|
+
|
|
154
|
+
// Each tool should expose name and credentialDomain only
|
|
155
|
+
const gh = body.tools.find((t: { name: string }) => t.name === "gh");
|
|
156
|
+
expect(gh).toEqual({ name: "gh", credentialDomain: "github.com" });
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { signJwt } from "@nkmc/core";
|
|
3
|
+
import type { JWK } from "jose";
|
|
4
|
+
import type { Env } from "../app.js";
|
|
5
|
+
|
|
6
|
+
export interface AuthRouteOptions {
|
|
7
|
+
privateKey: JWK;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function authRoutes(options: AuthRouteOptions) {
|
|
11
|
+
const app = new Hono<Env>();
|
|
12
|
+
|
|
13
|
+
app.post("/token", async (c) => {
|
|
14
|
+
const body = await c.req.json<{
|
|
15
|
+
sub: string;
|
|
16
|
+
roles?: string[];
|
|
17
|
+
svc: string;
|
|
18
|
+
expiresIn?: string;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
if (!body.sub || !body.svc) {
|
|
22
|
+
return c.json({ error: "Missing required fields: sub, svc" }, 400);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const token = await signJwt(
|
|
26
|
+
options.privateKey,
|
|
27
|
+
{
|
|
28
|
+
sub: body.sub,
|
|
29
|
+
roles: body.roles ?? ["agent"],
|
|
30
|
+
svc: body.svc,
|
|
31
|
+
},
|
|
32
|
+
body.expiresIn ? { expiresIn: body.expiresIn } : undefined,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return c.json({ token });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return app;
|
|
39
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { CredentialVault } from "../../credential/types.js";
|
|
3
|
+
import type { Env } from "../app.js";
|
|
4
|
+
|
|
5
|
+
export interface ByokRouteOptions {
|
|
6
|
+
vault: CredentialVault;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function byokRoutes(options: ByokRouteOptions) {
|
|
10
|
+
const { vault } = options;
|
|
11
|
+
const app = new Hono<Env>();
|
|
12
|
+
|
|
13
|
+
// PUT /credentials/byok/:domain — upload BYOK credential (agent JWT required)
|
|
14
|
+
app.put("/:domain", async (c) => {
|
|
15
|
+
const domain = c.req.param("domain");
|
|
16
|
+
const agent = c.get("agent");
|
|
17
|
+
const body = await c.req.json<{
|
|
18
|
+
auth: {
|
|
19
|
+
type: string;
|
|
20
|
+
token?: string;
|
|
21
|
+
header?: string;
|
|
22
|
+
key?: string;
|
|
23
|
+
username?: string;
|
|
24
|
+
password?: string;
|
|
25
|
+
};
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
if (!body.auth?.type) {
|
|
29
|
+
return c.json({ error: "Missing auth.type" }, 400);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await vault.putByok(domain, agent.id, body.auth as any);
|
|
33
|
+
return c.json({ ok: true, domain });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// GET /credentials/byok — list agent's BYOK domains
|
|
37
|
+
app.get("/", async (c) => {
|
|
38
|
+
const agent = c.get("agent");
|
|
39
|
+
const allDomains = await vault.listDomains();
|
|
40
|
+
|
|
41
|
+
// Filter to only domains where this agent has BYOK credentials
|
|
42
|
+
const byokDomains: string[] = [];
|
|
43
|
+
for (const domain of allDomains) {
|
|
44
|
+
const cred = await vault.get(domain, agent.id);
|
|
45
|
+
if (cred && cred.scope === "byok" && cred.developerId === agent.id) {
|
|
46
|
+
byokDomains.push(domain);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return c.json({ domains: byokDomains });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// DELETE /credentials/byok/:domain — delete agent's BYOK credential
|
|
54
|
+
app.delete("/:domain", async (c) => {
|
|
55
|
+
const domain = c.req.param("domain");
|
|
56
|
+
const agent = c.get("agent");
|
|
57
|
+
await vault.delete(domain, agent.id);
|
|
58
|
+
return c.json({ ok: true, domain });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return app;
|
|
62
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { CredentialVault } from "../../credential/types.js";
|
|
3
|
+
import type { Env } from "../app.js";
|
|
4
|
+
|
|
5
|
+
export interface CredentialRouteOptions {
|
|
6
|
+
vault: CredentialVault;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function credentialRoutes(options: CredentialRouteOptions) {
|
|
10
|
+
const { vault } = options;
|
|
11
|
+
const app = new Hono<Env>();
|
|
12
|
+
|
|
13
|
+
// PUT /credentials/:domain — set pool credential
|
|
14
|
+
app.put("/:domain", async (c) => {
|
|
15
|
+
const domain = c.req.param("domain");
|
|
16
|
+
const body = await c.req.json<{ auth: { type: string; token?: string; header?: string; key?: string; username?: string; password?: string } }>();
|
|
17
|
+
|
|
18
|
+
if (!body.auth?.type) {
|
|
19
|
+
return c.json({ error: "Missing auth.type" }, 400);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await vault.putPool(domain, body.auth as any);
|
|
23
|
+
return c.json({ ok: true, domain });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// GET /credentials — list domains with credentials
|
|
27
|
+
app.get("/", async (c) => {
|
|
28
|
+
const domains = await vault.listDomains();
|
|
29
|
+
return c.json({ domains });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// DELETE /credentials/:domain — delete pool credential
|
|
33
|
+
app.delete("/:domain", async (c) => {
|
|
34
|
+
const domain = c.req.param("domain");
|
|
35
|
+
await vault.delete(domain);
|
|
36
|
+
return c.json({ ok: true, domain });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return app;
|
|
40
|
+
}
|