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