@simonsbs/keylore 1.0.0-rc4

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 (81) hide show
  1. package/.env.example +64 -0
  2. package/LICENSE +176 -0
  3. package/NOTICE +5 -0
  4. package/README.md +424 -0
  5. package/bin/keylore-http.js +3 -0
  6. package/bin/keylore-stdio.js +3 -0
  7. package/data/auth-clients.json +54 -0
  8. package/data/catalog.json +53 -0
  9. package/data/policies.json +25 -0
  10. package/dist/adapters/adapter-registry.js +143 -0
  11. package/dist/adapters/aws-secrets-manager-adapter.js +99 -0
  12. package/dist/adapters/command-runner.js +17 -0
  13. package/dist/adapters/env-secret-adapter.js +42 -0
  14. package/dist/adapters/gcp-secret-manager-adapter.js +129 -0
  15. package/dist/adapters/local-secret-adapter.js +54 -0
  16. package/dist/adapters/onepassword-secret-adapter.js +83 -0
  17. package/dist/adapters/reference-utils.js +44 -0
  18. package/dist/adapters/types.js +1 -0
  19. package/dist/adapters/vault-secret-adapter.js +103 -0
  20. package/dist/app.js +132 -0
  21. package/dist/cli/args.js +51 -0
  22. package/dist/cli/run.js +483 -0
  23. package/dist/cli.js +18 -0
  24. package/dist/config.js +295 -0
  25. package/dist/domain/types.js +967 -0
  26. package/dist/http/admin-ui.js +3010 -0
  27. package/dist/http/server.js +1210 -0
  28. package/dist/index.js +40 -0
  29. package/dist/mcp/create-server.js +388 -0
  30. package/dist/mcp/stdio.js +7 -0
  31. package/dist/repositories/credential-repository.js +109 -0
  32. package/dist/repositories/interfaces.js +1 -0
  33. package/dist/repositories/json-file.js +20 -0
  34. package/dist/repositories/pg-access-token-repository.js +118 -0
  35. package/dist/repositories/pg-approval-repository.js +157 -0
  36. package/dist/repositories/pg-audit-log.js +62 -0
  37. package/dist/repositories/pg-auth-client-repository.js +98 -0
  38. package/dist/repositories/pg-authorization-code-repository.js +95 -0
  39. package/dist/repositories/pg-break-glass-repository.js +174 -0
  40. package/dist/repositories/pg-credential-repository.js +163 -0
  41. package/dist/repositories/pg-oauth-client-assertion-repository.js +25 -0
  42. package/dist/repositories/pg-policy-repository.js +62 -0
  43. package/dist/repositories/pg-refresh-token-repository.js +125 -0
  44. package/dist/repositories/pg-rotation-run-repository.js +127 -0
  45. package/dist/repositories/pg-tenant-repository.js +56 -0
  46. package/dist/repositories/policy-repository.js +24 -0
  47. package/dist/runtime/sandbox-runner.js +114 -0
  48. package/dist/services/access-fingerprint.js +13 -0
  49. package/dist/services/approval-service.js +148 -0
  50. package/dist/services/audit-log.js +38 -0
  51. package/dist/services/auth-context.js +43 -0
  52. package/dist/services/auth-secrets.js +14 -0
  53. package/dist/services/auth-service.js +784 -0
  54. package/dist/services/backup-service.js +610 -0
  55. package/dist/services/break-glass-service.js +207 -0
  56. package/dist/services/broker-service.js +557 -0
  57. package/dist/services/core-mode-service.js +154 -0
  58. package/dist/services/egress-policy.js +119 -0
  59. package/dist/services/local-secret-store.js +119 -0
  60. package/dist/services/maintenance-service.js +99 -0
  61. package/dist/services/notification-service.js +83 -0
  62. package/dist/services/policy-engine.js +85 -0
  63. package/dist/services/rate-limit-service.js +80 -0
  64. package/dist/services/rotation-service.js +271 -0
  65. package/dist/services/telemetry.js +149 -0
  66. package/dist/services/tenant-service.js +127 -0
  67. package/dist/services/trace-export-service.js +126 -0
  68. package/dist/services/trace-service.js +87 -0
  69. package/dist/storage/bootstrap.js +68 -0
  70. package/dist/storage/database.js +39 -0
  71. package/dist/storage/in-memory-database.js +40 -0
  72. package/dist/storage/migrations.js +27 -0
  73. package/migrations/001_init.sql +49 -0
  74. package/migrations/002_phase2_auth.sql +53 -0
  75. package/migrations/003_v05_operations.sql +9 -0
  76. package/migrations/004_v07_security.sql +28 -0
  77. package/migrations/005_v08_reviews.sql +11 -0
  78. package/migrations/006_v09_auth_trace_rotation.sql +51 -0
  79. package/migrations/007_v010_multi_tenant.sql +32 -0
  80. package/migrations/008_v011_auth_tenant_ops.sql +95 -0
  81. package/package.json +78 -0
@@ -0,0 +1,1210 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import http from "node:http";
3
+ import { URL } from "node:url";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
6
+ import * as z from "zod/v4";
7
+ import { accessRequestInputSchema, backupInspectOutputSchema, breakGlassRequestInputSchema, breakGlassReviewInputSchema, authorizationRequestInputSchema, authClientCreateInputSchema, authClientRotateSecretInputSchema, authClientUpdateInputSchema, authTokenListQuerySchema, approvalReviewInputSchema, catalogSearchInputSchema, coreCredentialCreateInputSchema, coreCredentialContextUpdateInputSchema, createCredentialInputSchema, rotationCompleteInputSchema, rotationCreateInputSchema, rotationPlanInputSchema, rotationRunListOutputSchema, rotationTransitionInputSchema, runtimeExecutionInputSchema, tenantBootstrapInputSchema, tenantCreateInputSchema, tenantUpdateInputSchema, traceExportStatusOutputSchema, traceListOutputSchema, tokenIssueInputSchema, updateCredentialInputSchema, } from "../domain/types.js";
8
+ import { renderAdminPage } from "./admin-ui.js";
9
+ import { createKeyLoreMcpServer } from "../mcp/create-server.js";
10
+ import { authContextFromToken } from "../services/auth-context.js";
11
+ function respondJson(res, statusCode, payload) {
12
+ res.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
13
+ res.end(`${JSON.stringify(payload, null, 2)}\n`);
14
+ }
15
+ function respondText(res, statusCode, body) {
16
+ res.writeHead(statusCode, { "content-type": "text/plain; charset=utf-8" });
17
+ res.end(body);
18
+ }
19
+ function respondHtml(res, statusCode, body) {
20
+ res.writeHead(statusCode, { "content-type": "text/html; charset=utf-8" });
21
+ res.end(body);
22
+ }
23
+ function respondRedirect(res, location) {
24
+ res.writeHead(302, { location });
25
+ res.end();
26
+ }
27
+ async function readBody(req, maxBytes) {
28
+ const contentLengthHeader = req.headers["content-length"];
29
+ const contentLength = typeof contentLengthHeader === "string" ? Number.parseInt(contentLengthHeader, 10) : undefined;
30
+ if (contentLength && contentLength > maxBytes) {
31
+ throw new Error(`Request body exceeds the ${maxBytes} byte limit.`);
32
+ }
33
+ const chunks = [];
34
+ let totalBytes = 0;
35
+ for await (const chunk of req) {
36
+ const buffer = Buffer.from(chunk);
37
+ totalBytes += buffer.byteLength;
38
+ if (totalBytes > maxBytes) {
39
+ throw new Error(`Request body exceeds the ${maxBytes} byte limit.`);
40
+ }
41
+ chunks.push(buffer);
42
+ }
43
+ return Buffer.concat(chunks).toString("utf8").trim();
44
+ }
45
+ async function readJsonBody(req, maxBytes) {
46
+ const raw = await readBody(req, maxBytes);
47
+ if (raw.length === 0) {
48
+ return undefined;
49
+ }
50
+ return JSON.parse(raw);
51
+ }
52
+ async function readFormBody(req, maxBytes) {
53
+ const raw = await readBody(req, maxBytes);
54
+ return new URLSearchParams(raw);
55
+ }
56
+ function routeParam(pathname, prefix) {
57
+ if (!pathname.startsWith(prefix)) {
58
+ return undefined;
59
+ }
60
+ const value = pathname.slice(prefix.length);
61
+ return value.length > 0 ? decodeURIComponent(value) : undefined;
62
+ }
63
+ function clientKey(req) {
64
+ return req.socket.remoteAddress ?? "unknown";
65
+ }
66
+ function isLoopbackRequest(req) {
67
+ const remoteAddress = req.socket.remoteAddress;
68
+ return (remoteAddress === "127.0.0.1" ||
69
+ remoteAddress === "::1" ||
70
+ remoteAddress === "::ffff:127.0.0.1");
71
+ }
72
+ function traceIdFromRequest(req) {
73
+ const header = req.headers["x-trace-id"];
74
+ if (typeof header === "string" && header.trim().length > 0) {
75
+ return header.trim();
76
+ }
77
+ if (Array.isArray(header) && header[0]?.trim()) {
78
+ return header[0].trim();
79
+ }
80
+ return randomUUID();
81
+ }
82
+ function normalizeRoute(pathname) {
83
+ if (pathname.startsWith("/v1/catalog/credentials/") && pathname.endsWith("/report")) {
84
+ return "/v1/catalog/credentials/:id/report";
85
+ }
86
+ if (pathname.startsWith("/v1/catalog/credentials/")) {
87
+ return "/v1/catalog/credentials/:id";
88
+ }
89
+ if (pathname.startsWith("/v1/approvals/") && pathname.endsWith("/approve")) {
90
+ return "/v1/approvals/:id/approve";
91
+ }
92
+ if (pathname.startsWith("/v1/approvals/") && pathname.endsWith("/deny")) {
93
+ return "/v1/approvals/:id/deny";
94
+ }
95
+ if (pathname.startsWith("/v1/break-glass/") && pathname.endsWith("/approve")) {
96
+ return "/v1/break-glass/:id/approve";
97
+ }
98
+ if (pathname.startsWith("/v1/break-glass/") && pathname.endsWith("/deny")) {
99
+ return "/v1/break-glass/:id/deny";
100
+ }
101
+ if (pathname.startsWith("/v1/break-glass/") && pathname.endsWith("/revoke")) {
102
+ return "/v1/break-glass/:id/revoke";
103
+ }
104
+ if (pathname.startsWith("/v1/auth/clients/") && pathname.endsWith("/rotate-secret")) {
105
+ return "/v1/auth/clients/:id/rotate-secret";
106
+ }
107
+ if (pathname.startsWith("/v1/auth/clients/") && pathname.endsWith("/enable")) {
108
+ return "/v1/auth/clients/:id/enable";
109
+ }
110
+ if (pathname.startsWith("/v1/auth/clients/") && pathname.endsWith("/disable")) {
111
+ return "/v1/auth/clients/:id/disable";
112
+ }
113
+ if (pathname.startsWith("/v1/auth/clients/")) {
114
+ return "/v1/auth/clients/:id";
115
+ }
116
+ if (pathname.startsWith("/v1/auth/tokens/") && pathname.endsWith("/revoke")) {
117
+ return "/v1/auth/tokens/:id/revoke";
118
+ }
119
+ if (pathname.startsWith("/v1/auth/refresh-tokens/") && pathname.endsWith("/revoke")) {
120
+ return "/v1/auth/refresh-tokens/:id/revoke";
121
+ }
122
+ if (pathname === "/v1/tenants/bootstrap") {
123
+ return "/v1/tenants/bootstrap";
124
+ }
125
+ if (pathname.startsWith("/v1/tenants/")) {
126
+ return "/v1/tenants/:id";
127
+ }
128
+ if (pathname.startsWith("/v1/system/backups/")) {
129
+ return "/v1/system/backups/:action";
130
+ }
131
+ if (pathname === "/v1/system/trace-exporter") {
132
+ return "/v1/system/trace-exporter";
133
+ }
134
+ if (pathname === "/v1/system/trace-exporter/flush") {
135
+ return "/v1/system/trace-exporter/flush";
136
+ }
137
+ if (pathname.startsWith("/v1/system/rotations/") && pathname.endsWith("/start")) {
138
+ return "/v1/system/rotations/:id/start";
139
+ }
140
+ if (pathname.startsWith("/v1/system/rotations/") && pathname.endsWith("/complete")) {
141
+ return "/v1/system/rotations/:id/complete";
142
+ }
143
+ if (pathname.startsWith("/v1/system/rotations/") && pathname.endsWith("/fail")) {
144
+ return "/v1/system/rotations/:id/fail";
145
+ }
146
+ if (pathname === "/v1/system/rotations/plan") {
147
+ return "/v1/system/rotations/plan";
148
+ }
149
+ if (pathname.startsWith("/v1/system/rotations/")) {
150
+ return "/v1/system/rotations/:id";
151
+ }
152
+ if (pathname.startsWith("/mcp")) {
153
+ return "/mcp";
154
+ }
155
+ return pathname;
156
+ }
157
+ function bearerChallenge(app, target) {
158
+ const suffix = target === "mcp" ? "mcp" : "api";
159
+ return `Bearer resource_metadata="${app.config.publicBaseUrl}/.well-known/oauth-protected-resource/${suffix}"`;
160
+ }
161
+ async function authenticateRequest(app, req, res, requiredScopes, target, requestedResource) {
162
+ const authorization = req.headers.authorization;
163
+ if (!authorization?.startsWith("Bearer ")) {
164
+ res.setHeader("www-authenticate", bearerChallenge(app, target));
165
+ respondJson(res, 401, { error: "Missing bearer token." });
166
+ return undefined;
167
+ }
168
+ const token = authorization.slice("Bearer ".length);
169
+ try {
170
+ const context = await app.auth.authenticateBearerToken(token, requestedResource);
171
+ app.auth.requireScopes(context, requiredScopes);
172
+ req.auth = {
173
+ token,
174
+ clientId: context.clientId,
175
+ scopes: context.scopes,
176
+ resource: new URL(requestedResource),
177
+ extra: {
178
+ principal: context.principal,
179
+ roles: context.roles,
180
+ tenantId: context.tenantId,
181
+ },
182
+ };
183
+ return context;
184
+ }
185
+ catch (error) {
186
+ const message = error instanceof Error ? error.message : "Unauthorized";
187
+ const statusCode = message.startsWith("Missing required scopes") || message.startsWith("Missing one of the required scopes")
188
+ ? 403
189
+ : 401;
190
+ if (statusCode === 401) {
191
+ res.setHeader("www-authenticate", bearerChallenge(app, target));
192
+ }
193
+ respondJson(res, statusCode, { error: message });
194
+ return undefined;
195
+ }
196
+ }
197
+ function parseBasicAuthHeader(req) {
198
+ const authorization = req.headers.authorization;
199
+ if (!authorization?.startsWith("Basic ")) {
200
+ return undefined;
201
+ }
202
+ const decoded = Buffer.from(authorization.slice("Basic ".length), "base64").toString("utf8");
203
+ const separator = decoded.indexOf(":");
204
+ if (separator === -1) {
205
+ return undefined;
206
+ }
207
+ return {
208
+ clientId: decoded.slice(0, separator),
209
+ clientSecret: decoded.slice(separator + 1),
210
+ };
211
+ }
212
+ function parseAuthContextFromRequest(req) {
213
+ const principal = typeof req.auth?.extra?.principal === "string" ? req.auth.extra.principal : req.auth?.clientId;
214
+ const roles = Array.isArray(req.auth?.extra?.roles) ? req.auth?.extra?.roles : [];
215
+ const tenantId = typeof req.auth?.extra?.tenantId === "string" ? req.auth.extra.tenantId : undefined;
216
+ return authContextFromToken({
217
+ principal: principal ?? "unknown",
218
+ clientId: req.auth?.clientId ?? "unknown",
219
+ tenantId,
220
+ scopes: (req.auth?.scopes ?? []),
221
+ roles: roles,
222
+ resource: req.auth?.resource?.href,
223
+ });
224
+ }
225
+ export async function startHttpServer(app) {
226
+ const transports = new Map();
227
+ const server = http.createServer(async (req, res) => {
228
+ const startedAt = Date.now();
229
+ let routeLabel = req.url ?? "/";
230
+ app.telemetry.adjustGauge("keylore_http_inflight_requests", {}, 1);
231
+ const requestId = randomUUID();
232
+ const traceId = traceIdFromRequest(req);
233
+ res.setHeader("x-request-id", requestId);
234
+ res.setHeader("x-trace-id", traceId);
235
+ res.on("finish", () => {
236
+ app.telemetry.adjustGauge("keylore_http_inflight_requests", {}, -1);
237
+ app.telemetry.recordHttpRequest(routeLabel, req.method ?? "UNKNOWN", res.statusCode, Date.now() - startedAt);
238
+ });
239
+ await app.traces.runWithTrace(traceId, async () => {
240
+ await app.traces.withSpan("http.request", { method: req.method ?? "UNKNOWN", route: routeLabel }, async () => {
241
+ try {
242
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? `${app.config.httpHost}:${app.config.httpPort}`}`);
243
+ routeLabel = normalizeRoute(url.pathname);
244
+ const rateLimitExempt = url.pathname === "/healthz" || url.pathname === "/readyz" || url.pathname === "/metrics";
245
+ if (!rateLimitExempt) {
246
+ const limited = await app.rateLimits.check(clientKey(req));
247
+ if (limited.limited) {
248
+ if (limited.retryAfterSeconds) {
249
+ res.setHeader("retry-after", String(limited.retryAfterSeconds));
250
+ }
251
+ respondJson(res, 429, { error: "Rate limit exceeded." });
252
+ return;
253
+ }
254
+ }
255
+ if (url.pathname === "/healthz" && req.method === "GET") {
256
+ respondJson(res, 200, { status: "ok", service: app.config.appName });
257
+ return;
258
+ }
259
+ if (url.pathname === "/readyz" && req.method === "GET") {
260
+ respondJson(res, 200, await app.health.readiness());
261
+ return;
262
+ }
263
+ if (url.pathname === "/metrics" && req.method === "GET") {
264
+ res.writeHead(200, {
265
+ "content-type": "text/plain; version=0.0.4; charset=utf-8",
266
+ });
267
+ res.end(app.telemetry.renderPrometheus());
268
+ return;
269
+ }
270
+ if (url.pathname === "/" && req.method === "GET") {
271
+ respondRedirect(res, "/admin");
272
+ return;
273
+ }
274
+ if ((url.pathname === "/admin" || url.pathname === "/admin/") && req.method === "GET") {
275
+ respondHtml(res, 200, renderAdminPage(app));
276
+ return;
277
+ }
278
+ if (url.pathname === "/.well-known/oauth-authorization-server" && req.method === "GET") {
279
+ respondJson(res, 200, app.auth.oauthMetadata());
280
+ return;
281
+ }
282
+ if (url.pathname === "/.well-known/oauth-protected-resource/mcp" &&
283
+ req.method === "GET") {
284
+ respondJson(res, 200, app.auth.protectedResourceMetadata("/mcp"));
285
+ return;
286
+ }
287
+ if (url.pathname === "/.well-known/oauth-protected-resource/api" &&
288
+ req.method === "GET") {
289
+ respondJson(res, 200, app.auth.protectedResourceMetadata("/v1"));
290
+ return;
291
+ }
292
+ if (url.pathname === "/oauth/token" && req.method === "POST") {
293
+ await handleOAuthToken(app, req, res);
294
+ return;
295
+ }
296
+ if (url.pathname === "/oauth/authorize" && req.method === "POST") {
297
+ await handleOAuthAuthorize(app, req, res);
298
+ return;
299
+ }
300
+ if (url.pathname.startsWith("/v1/")) {
301
+ await handleApiRequest(app, req, res, url);
302
+ return;
303
+ }
304
+ if (url.pathname === "/mcp") {
305
+ const authContext = await authenticateRequest(app, req, res, ["mcp:use"], "mcp", `${app.config.publicBaseUrl}/mcp`);
306
+ if (!authContext) {
307
+ return;
308
+ }
309
+ await handleMcpRequest(app, req, res, transports, authContext);
310
+ return;
311
+ }
312
+ respondJson(res, 404, { error: "Not found" });
313
+ }
314
+ catch (error) {
315
+ const message = error instanceof z.ZodError
316
+ ? error.issues.map((issue) => issue.message).join(" ")
317
+ : error instanceof Error
318
+ ? error.message
319
+ : "Internal server error";
320
+ const statusCode = error instanceof z.ZodError
321
+ ? 400
322
+ :
323
+ message.includes("Request body exceeds")
324
+ ? 413
325
+ : message.includes("JSON")
326
+ ? 400
327
+ : message === "Invalid client credentials." || message === "Invalid access token."
328
+ ? 401
329
+ : message === "Access token expired."
330
+ ? 401
331
+ : message === "Access token resource does not match this protected resource."
332
+ ? 401
333
+ : message === "Invalid authorization code." ||
334
+ message === "Invalid code verifier." ||
335
+ message === "Invalid redirect URI." ||
336
+ message === "Invalid refresh token." ||
337
+ message === "Refresh token expired." ||
338
+ message === "Refresh token resource does not match this protected resource."
339
+ ? 401
340
+ : message.startsWith("Client assertion replay detected") ||
341
+ message.startsWith("An open rotation already exists") ||
342
+ message.startsWith("Client already exists") ||
343
+ message.startsWith("Tenant already exists")
344
+ ? 409
345
+ : message.startsWith("Invalid client assertion") ||
346
+ message.startsWith("Client assertion ") ||
347
+ message === "Missing private_key_jwt client assertion."
348
+ ? 401
349
+ : message === "No valid scopes were granted."
350
+ ? 400
351
+ : message.startsWith("private_key_jwt clients do not support") ||
352
+ message.startsWith("none clients do not support") ||
353
+ message === "Unknown authorization client." ||
354
+ message === "Client does not support authorization_code." ||
355
+ message === "Unsupported grant type for client." ||
356
+ message === "Requested resource exceeds the caller resource binding." ||
357
+ message === "No valid roles were granted." ||
358
+ message === "Unsupported code challenge method."
359
+ ? 400
360
+ : message.startsWith("Unknown tenant:") ||
361
+ message.startsWith("Tenant is disabled:") ||
362
+ message.startsWith("Tenant-scoped restore payload is missing tenant metadata:") ||
363
+ message.startsWith("Tenant-scoped restore payload includes foreign tenant data:")
364
+ ? 403
365
+ : message.startsWith("private_key_jwt clients do not support")
366
+ ? 400
367
+ : message.startsWith("Reviewer has already reviewed")
368
+ ? 409
369
+ : message.startsWith("Missing required role")
370
+ ? 403
371
+ : message === "Tenant access denied."
372
+ ? 403
373
+ : message.startsWith("Missing required scopes") ||
374
+ message.startsWith("Missing one of the required scopes")
375
+ ? 403
376
+ : message === "Credential not found."
377
+ ? 404
378
+ : message.startsWith("Sandbox env variable")
379
+ ? 400
380
+ : 500;
381
+ app.logger.error({ err: error, requestId, traceId, route: routeLabel }, "http_request_failed");
382
+ respondJson(res, statusCode, { error: message });
383
+ return;
384
+ }
385
+ });
386
+ });
387
+ });
388
+ await new Promise((resolve, reject) => {
389
+ server.once("error", reject);
390
+ server.listen(app.config.httpPort, app.config.httpHost, resolve);
391
+ });
392
+ app.logger.info({
393
+ host: app.config.httpHost,
394
+ port: app.config.httpPort,
395
+ }, "keylore_http_server_started");
396
+ return {
397
+ close: async () => {
398
+ for (const [sessionId, entry] of transports.entries()) {
399
+ await entry.closeServer();
400
+ transports.delete(sessionId);
401
+ }
402
+ await new Promise((resolve, reject) => {
403
+ server.close((error) => (error ? reject(error) : resolve()));
404
+ });
405
+ },
406
+ };
407
+ }
408
+ async function handleOAuthToken(app, req, res) {
409
+ const basicAuth = parseBasicAuthHeader(req);
410
+ const form = await readFormBody(req, app.config.maxRequestBytes);
411
+ const scope = form.get("scope")?.split(/\s+/).filter(Boolean);
412
+ const payload = tokenIssueInputSchema.parse({
413
+ clientId: basicAuth?.clientId ?? form.get("client_id") ?? undefined,
414
+ clientSecret: basicAuth?.clientSecret ?? form.get("client_secret") ?? undefined,
415
+ grantType: form.get("grant_type") ?? undefined,
416
+ scope,
417
+ resource: form.get("resource") ?? undefined,
418
+ code: form.get("code") ?? undefined,
419
+ codeVerifier: form.get("code_verifier") ?? undefined,
420
+ redirectUri: form.get("redirect_uri") ?? undefined,
421
+ refreshToken: form.get("refresh_token") ?? undefined,
422
+ clientAssertionType: form.get("client_assertion_type") ?? undefined,
423
+ clientAssertion: form.get("client_assertion") ?? undefined,
424
+ });
425
+ const token = await app.auth.issueToken(payload);
426
+ respondJson(res, 200, token);
427
+ }
428
+ async function handleOAuthAuthorize(app, req, res) {
429
+ const context = await authenticateRequest(app, req, res, [], "api", `${app.config.publicBaseUrl}/v1`);
430
+ if (!context) {
431
+ return;
432
+ }
433
+ const body = authorizationRequestInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
434
+ const authorization = await app.auth.authorize(context, body);
435
+ respondJson(res, 200, authorization);
436
+ }
437
+ async function handleApiRequest(app, req, res, url) {
438
+ if (url.pathname === "/v1/core/local-session" && req.method === "POST") {
439
+ if (!app.config.localQuickstartEnabled || !app.config.localQuickstartBootstrap) {
440
+ respondJson(res, 404, { error: "Local quickstart is not enabled." });
441
+ return;
442
+ }
443
+ if (!isLoopbackRequest(req)) {
444
+ respondJson(res, 403, { error: "Local quickstart is only available from loopback." });
445
+ return;
446
+ }
447
+ const token = await app.auth.issueToken({
448
+ clientId: app.config.localQuickstartBootstrap.clientId,
449
+ clientSecret: app.config.localQuickstartBootstrap.clientSecret,
450
+ grantType: "client_credentials",
451
+ scope: [...app.config.localQuickstartBootstrap.scopes],
452
+ resource: `${app.config.publicBaseUrl}/v1`,
453
+ });
454
+ respondJson(res, 200, {
455
+ ...token,
456
+ clientId: app.config.localQuickstartBootstrap.clientId,
457
+ resource: `${app.config.publicBaseUrl}/v1`,
458
+ quickstart: true,
459
+ });
460
+ return;
461
+ }
462
+ if (url.pathname === "/v1/core/mcp/check" && req.method === "POST") {
463
+ const context = await authenticateRequest(app, req, res, ["catalog:read"], "api", `${app.config.publicBaseUrl}/v1`);
464
+ if (!context) {
465
+ return;
466
+ }
467
+ app.auth.requireRoles(context, ["admin", "operator"]);
468
+ const body = z
469
+ .object({
470
+ token: z.string().min(1),
471
+ })
472
+ .parse(await readJsonBody(req, app.config.maxRequestBytes));
473
+ const mcpContext = await app.auth.authenticateBearerToken(body.token, `${app.config.publicBaseUrl}/mcp`);
474
+ respondJson(res, 200, {
475
+ ok: true,
476
+ clientId: mcpContext.clientId,
477
+ principal: mcpContext.principal,
478
+ scopes: mcpContext.scopes,
479
+ resource: `${app.config.publicBaseUrl}/mcp`,
480
+ });
481
+ return;
482
+ }
483
+ if (url.pathname === "/v1/catalog/credentials" && req.method === "GET") {
484
+ const context = await authenticateRequest(app, req, res, ["catalog:read"], "api", `${app.config.publicBaseUrl}/v1`);
485
+ if (!context) {
486
+ return;
487
+ }
488
+ const credentials = await app.broker.listCredentials(context);
489
+ respondJson(res, 200, { credentials });
490
+ return;
491
+ }
492
+ if (url.pathname === "/v1/catalog/credentials" && req.method === "POST") {
493
+ const context = await authenticateRequest(app, req, res, ["catalog:write"], "api", `${app.config.publicBaseUrl}/v1`);
494
+ if (!context) {
495
+ return;
496
+ }
497
+ app.auth.requireRoles(context, ["admin", "operator"]);
498
+ const body = createCredentialInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
499
+ const credential = await app.broker.createCredential(context, body);
500
+ respondJson(res, 201, { credential });
501
+ return;
502
+ }
503
+ if (url.pathname === "/v1/core/credentials" && req.method === "POST") {
504
+ const context = await authenticateRequest(app, req, res, ["catalog:write"], "api", `${app.config.publicBaseUrl}/v1`);
505
+ if (!context) {
506
+ return;
507
+ }
508
+ app.auth.requireRoles(context, ["admin", "operator"]);
509
+ const body = coreCredentialCreateInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
510
+ const credential = await app.coreMode.createCredential(context, body);
511
+ respondJson(res, 201, { credential });
512
+ return;
513
+ }
514
+ const coreCredentialContextId = routeParam(url.pathname, "/v1/core/credentials/");
515
+ if (coreCredentialContextId && url.pathname.endsWith("/context") && req.method === "GET") {
516
+ const context = await authenticateRequest(app, req, res, ["catalog:read"], "api", `${app.config.publicBaseUrl}/v1`);
517
+ if (!context) {
518
+ return;
519
+ }
520
+ app.auth.requireRoles(context, ["admin", "operator"]);
521
+ const credentialId = coreCredentialContextId.replace(/\/context$/, "");
522
+ const credential = await app.broker.getCredential(context, credentialId);
523
+ if (!credential) {
524
+ respondJson(res, 404, { error: "Credential not found" });
525
+ return;
526
+ }
527
+ respondJson(res, 200, { credential });
528
+ return;
529
+ }
530
+ if (coreCredentialContextId && url.pathname.endsWith("/context") && req.method === "PATCH") {
531
+ const context = await authenticateRequest(app, req, res, ["catalog:write"], "api", `${app.config.publicBaseUrl}/v1`);
532
+ if (!context) {
533
+ return;
534
+ }
535
+ app.auth.requireRoles(context, ["admin", "operator"]);
536
+ const credentialId = coreCredentialContextId.replace(/\/context$/, "");
537
+ const patch = coreCredentialContextUpdateInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
538
+ const credential = await app.coreMode.updateCredentialContext(context, credentialId, patch);
539
+ respondJson(res, 200, { credential });
540
+ return;
541
+ }
542
+ if (coreCredentialContextId && !url.pathname.endsWith("/context") && req.method === "DELETE") {
543
+ const context = await authenticateRequest(app, req, res, ["catalog:write"], "api", `${app.config.publicBaseUrl}/v1`);
544
+ if (!context) {
545
+ return;
546
+ }
547
+ app.auth.requireRoles(context, ["admin", "operator"]);
548
+ const deleted = await app.coreMode.deleteCredential(context, coreCredentialContextId);
549
+ respondJson(res, deleted ? 200 : 404, { deleted });
550
+ return;
551
+ }
552
+ if (url.pathname === "/v1/catalog/search" && req.method === "POST") {
553
+ const context = await authenticateRequest(app, req, res, ["catalog:read"], "api", `${app.config.publicBaseUrl}/v1`);
554
+ if (!context) {
555
+ return;
556
+ }
557
+ const body = catalogSearchInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
558
+ const credentials = await app.broker.searchCatalog(context, body);
559
+ respondJson(res, 200, { credentials });
560
+ return;
561
+ }
562
+ if (url.pathname === "/v1/access/request" && req.method === "POST") {
563
+ const context = await authenticateRequest(app, req, res, ["broker:use"], "api", `${app.config.publicBaseUrl}/v1`);
564
+ if (!context) {
565
+ return;
566
+ }
567
+ const body = accessRequestInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
568
+ const decision = await app.broker.requestAccess(context, body);
569
+ respondJson(res, 200, decision);
570
+ return;
571
+ }
572
+ if (url.pathname === "/v1/access/simulate" && req.method === "POST") {
573
+ const context = await authenticateRequest(app, req, res, ["broker:use"], "api", `${app.config.publicBaseUrl}/v1`);
574
+ if (!context) {
575
+ return;
576
+ }
577
+ const body = accessRequestInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
578
+ const decision = await app.broker.simulateAccess(context, body);
579
+ respondJson(res, 200, decision);
580
+ return;
581
+ }
582
+ if (url.pathname === "/v1/runtime/sandbox" && req.method === "POST") {
583
+ const context = await authenticateRequest(app, req, res, ["sandbox:run"], "api", `${app.config.publicBaseUrl}/v1`);
584
+ if (!context) {
585
+ return;
586
+ }
587
+ app.auth.requireRoles(context, ["admin", "operator"]);
588
+ const body = runtimeExecutionInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
589
+ const result = await app.broker.runSandboxed(context, body);
590
+ respondJson(res, 200, { result });
591
+ return;
592
+ }
593
+ if (url.pathname === "/v1/audit/events" && req.method === "GET") {
594
+ const context = await authenticateRequest(app, req, res, ["audit:read"], "api", `${app.config.publicBaseUrl}/v1`);
595
+ if (!context) {
596
+ return;
597
+ }
598
+ app.auth.requireRoles(context, ["admin", "auditor"]);
599
+ const limit = Number.parseInt(url.searchParams.get("limit") ?? "20", 10);
600
+ const events = await app.broker.listRecentAuditEvents(context, limit);
601
+ respondJson(res, 200, { events });
602
+ return;
603
+ }
604
+ if (url.pathname === "/v1/auth/clients" && req.method === "GET") {
605
+ const context = await authenticateRequest(app, req, res, ["auth:read"], "api", `${app.config.publicBaseUrl}/v1`);
606
+ if (!context) {
607
+ return;
608
+ }
609
+ app.auth.requireAnyScope(context, ["auth:read", "admin:read"]);
610
+ app.auth.requireRoles(context, ["admin", "auth_admin"]);
611
+ const clients = (await app.auth.listClients()).filter((client) => !context.tenantId || client.tenantId === context.tenantId);
612
+ respondJson(res, 200, { clients });
613
+ return;
614
+ }
615
+ if (url.pathname === "/v1/auth/clients" && req.method === "POST") {
616
+ const context = await authenticateRequest(app, req, res, ["auth:write"], "api", `${app.config.publicBaseUrl}/v1`);
617
+ if (!context) {
618
+ return;
619
+ }
620
+ app.auth.requireAnyScope(context, ["auth:write", "admin:write"]);
621
+ app.auth.requireRoles(context, ["admin", "auth_admin"]);
622
+ const body = authClientCreateInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
623
+ const client = await app.auth.createClient(context, body);
624
+ respondJson(res, 201, client);
625
+ return;
626
+ }
627
+ if (url.pathname === "/v1/auth/tokens" && req.method === "GET") {
628
+ const context = await authenticateRequest(app, req, res, ["auth:read"], "api", `${app.config.publicBaseUrl}/v1`);
629
+ if (!context) {
630
+ return;
631
+ }
632
+ app.auth.requireAnyScope(context, ["auth:read", "admin:read"]);
633
+ app.auth.requireRoles(context, ["admin", "auth_admin"]);
634
+ const query = authTokenListQuerySchema.parse({
635
+ clientId: url.searchParams.get("clientId") ?? undefined,
636
+ status: url.searchParams.get("status") ?? undefined,
637
+ });
638
+ const tokens = await app.auth.listTokens({
639
+ ...query,
640
+ tenantId: context.tenantId,
641
+ });
642
+ respondJson(res, 200, { tokens });
643
+ return;
644
+ }
645
+ if (url.pathname === "/v1/auth/refresh-tokens" && req.method === "GET") {
646
+ const context = await authenticateRequest(app, req, res, ["auth:read"], "api", `${app.config.publicBaseUrl}/v1`);
647
+ if (!context) {
648
+ return;
649
+ }
650
+ app.auth.requireAnyScope(context, ["auth:read", "admin:read"]);
651
+ app.auth.requireRoles(context, ["admin", "auth_admin"]);
652
+ const query = authTokenListQuerySchema.parse({
653
+ clientId: url.searchParams.get("clientId") ?? undefined,
654
+ status: url.searchParams.get("status") ?? undefined,
655
+ });
656
+ const tokens = await app.auth.listRefreshTokens({
657
+ ...query,
658
+ tenantId: context.tenantId,
659
+ });
660
+ respondJson(res, 200, { tokens });
661
+ return;
662
+ }
663
+ if (url.pathname === "/v1/tenants" && req.method === "GET") {
664
+ const context = await authenticateRequest(app, req, res, ["admin:read"], "api", `${app.config.publicBaseUrl}/v1`);
665
+ if (!context) {
666
+ return;
667
+ }
668
+ app.auth.requireRoles(context, ["admin"]);
669
+ const tenants = await app.tenants.list(context);
670
+ respondJson(res, 200, { tenants });
671
+ return;
672
+ }
673
+ if (url.pathname === "/v1/tenants" && req.method === "POST") {
674
+ const context = await authenticateRequest(app, req, res, ["admin:write"], "api", `${app.config.publicBaseUrl}/v1`);
675
+ if (!context) {
676
+ return;
677
+ }
678
+ app.auth.requireRoles(context, ["admin"]);
679
+ const body = tenantCreateInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
680
+ const tenant = await app.tenants.create(context, body);
681
+ respondJson(res, 201, { tenant });
682
+ return;
683
+ }
684
+ if (url.pathname === "/v1/tenants/bootstrap" && req.method === "POST") {
685
+ const context = await authenticateRequest(app, req, res, ["admin:write"], "api", `${app.config.publicBaseUrl}/v1`);
686
+ if (!context) {
687
+ return;
688
+ }
689
+ app.auth.requireRoles(context, ["admin"]);
690
+ const body = tenantBootstrapInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
691
+ const result = await app.tenants.bootstrap(context, body);
692
+ respondJson(res, 201, result);
693
+ return;
694
+ }
695
+ if (url.pathname === "/v1/system/adapters" && req.method === "GET") {
696
+ const context = await authenticateRequest(app, req, res, ["system:read"], "api", `${app.config.publicBaseUrl}/v1`);
697
+ if (!context) {
698
+ return;
699
+ }
700
+ app.auth.requireAnyScope(context, ["system:read", "admin:read"]);
701
+ app.auth.requireRoles(context, ["admin", "maintenance_operator", "auditor"]);
702
+ const adapters = await app.broker.adapterHealth();
703
+ respondJson(res, 200, { adapters });
704
+ return;
705
+ }
706
+ if (url.pathname === "/v1/system/maintenance" && req.method === "GET") {
707
+ const context = await authenticateRequest(app, req, res, ["system:read"], "api", `${app.config.publicBaseUrl}/v1`);
708
+ if (!context) {
709
+ return;
710
+ }
711
+ app.auth.requireAnyScope(context, ["system:read", "admin:read"]);
712
+ app.auth.requireRoles(context, ["admin", "maintenance_operator", "auditor"]);
713
+ respondJson(res, 200, { maintenance: app.maintenance.status() });
714
+ return;
715
+ }
716
+ if (url.pathname === "/v1/system/trace-exporter" && req.method === "GET") {
717
+ const context = await authenticateRequest(app, req, res, ["system:read"], "api", `${app.config.publicBaseUrl}/v1`);
718
+ if (!context) {
719
+ return;
720
+ }
721
+ app.auth.requireAnyScope(context, ["system:read", "admin:read"]);
722
+ app.auth.requireRoles(context, ["admin", "maintenance_operator", "auditor"]);
723
+ respondJson(res, 200, traceExportStatusOutputSchema.parse({ exporter: app.traceExports.status() }));
724
+ return;
725
+ }
726
+ if (url.pathname === "/v1/system/trace-exporter/flush" && req.method === "POST") {
727
+ const context = await authenticateRequest(app, req, res, ["system:write"], "api", `${app.config.publicBaseUrl}/v1`);
728
+ if (!context) {
729
+ return;
730
+ }
731
+ app.auth.requireAnyScope(context, ["system:write", "admin:write"]);
732
+ app.auth.requireRoles(context, ["admin", "maintenance_operator"]);
733
+ respondJson(res, 200, traceExportStatusOutputSchema.parse({ exporter: await app.traceExports.flushNow() }));
734
+ return;
735
+ }
736
+ if (url.pathname === "/v1/system/traces" && req.method === "GET") {
737
+ const context = await authenticateRequest(app, req, res, ["system:read"], "api", `${app.config.publicBaseUrl}/v1`);
738
+ if (!context) {
739
+ return;
740
+ }
741
+ app.auth.requireAnyScope(context, ["system:read", "admin:read"]);
742
+ app.auth.requireRoles(context, ["admin", "maintenance_operator", "auditor"]);
743
+ const limit = Number.parseInt(url.searchParams.get("limit") ?? "20", 10);
744
+ const traceId = url.searchParams.get("traceId") ?? undefined;
745
+ respondJson(res, 200, traceListOutputSchema.parse({
746
+ traces: app.traces.recent(limit, traceId),
747
+ }));
748
+ return;
749
+ }
750
+ if (url.pathname === "/v1/system/maintenance/run" && req.method === "POST") {
751
+ const context = await authenticateRequest(app, req, res, ["system:write"], "api", `${app.config.publicBaseUrl}/v1`);
752
+ if (!context) {
753
+ return;
754
+ }
755
+ app.auth.requireAnyScope(context, ["system:write", "admin:write"]);
756
+ app.auth.requireRoles(context, ["admin", "maintenance_operator"]);
757
+ const result = await app.maintenance.runOnce();
758
+ respondJson(res, 200, { maintenance: app.maintenance.status(), result });
759
+ return;
760
+ }
761
+ if (url.pathname === "/v1/system/rotations" && req.method === "GET") {
762
+ const context = await authenticateRequest(app, req, res, ["system:read"], "api", `${app.config.publicBaseUrl}/v1`);
763
+ if (!context) {
764
+ return;
765
+ }
766
+ app.auth.requireAnyScope(context, ["system:read", "admin:read"]);
767
+ app.auth.requireRoles(context, ["admin", "operator", "maintenance_operator", "auditor"]);
768
+ const rotations = await app.rotations.list({
769
+ tenantId: context.tenantId,
770
+ status: (url.searchParams.get("status") ?? undefined),
771
+ credentialId: url.searchParams.get("credentialId") ?? undefined,
772
+ });
773
+ respondJson(res, 200, rotationRunListOutputSchema.parse({ rotations }));
774
+ return;
775
+ }
776
+ if (url.pathname === "/v1/system/rotations" && req.method === "POST") {
777
+ const context = await authenticateRequest(app, req, res, ["system:write"], "api", `${app.config.publicBaseUrl}/v1`);
778
+ if (!context) {
779
+ return;
780
+ }
781
+ app.auth.requireAnyScope(context, ["system:write", "admin:write"]);
782
+ app.auth.requireRoles(context, ["admin", "operator", "maintenance_operator"]);
783
+ const body = rotationCreateInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
784
+ const rotation = await app.rotations.createManual(context, body);
785
+ respondJson(res, 201, { rotation });
786
+ return;
787
+ }
788
+ if (url.pathname === "/v1/system/rotations/plan" && req.method === "POST") {
789
+ const context = await authenticateRequest(app, req, res, ["system:write"], "api", `${app.config.publicBaseUrl}/v1`);
790
+ if (!context) {
791
+ return;
792
+ }
793
+ app.auth.requireAnyScope(context, ["system:write", "admin:write"]);
794
+ app.auth.requireRoles(context, ["admin", "operator", "maintenance_operator"]);
795
+ const body = rotationPlanInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
796
+ const rotations = await app.rotations.planDue(context, body);
797
+ respondJson(res, 200, rotationRunListOutputSchema.parse({ rotations }));
798
+ return;
799
+ }
800
+ const rotationId = routeParam(url.pathname, "/v1/system/rotations/");
801
+ if (rotationId && req.method === "POST" && url.pathname.endsWith("/start")) {
802
+ const context = await authenticateRequest(app, req, res, ["system:write"], "api", `${app.config.publicBaseUrl}/v1`);
803
+ if (!context) {
804
+ return;
805
+ }
806
+ app.auth.requireAnyScope(context, ["system:write", "admin:write"]);
807
+ app.auth.requireRoles(context, ["admin", "operator", "maintenance_operator"]);
808
+ const body = rotationTransitionInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
809
+ const id = rotationId.replace(/\/start$/, "");
810
+ const rotation = await app.rotations.start(id, context, body.note);
811
+ respondJson(res, rotation ? 200 : 404, { rotation: rotation ?? null });
812
+ return;
813
+ }
814
+ if (rotationId && req.method === "POST" && url.pathname.endsWith("/complete")) {
815
+ const context = await authenticateRequest(app, req, res, ["system:write"], "api", `${app.config.publicBaseUrl}/v1`);
816
+ if (!context) {
817
+ return;
818
+ }
819
+ app.auth.requireAnyScope(context, ["system:write", "admin:write"]);
820
+ app.auth.requireRoles(context, ["admin", "operator", "maintenance_operator"]);
821
+ const body = rotationCompleteInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
822
+ const id = rotationId.replace(/\/complete$/, "");
823
+ const rotation = await app.rotations.complete(id, context, body);
824
+ respondJson(res, rotation ? 200 : 404, { rotation: rotation ?? null });
825
+ return;
826
+ }
827
+ if (rotationId && req.method === "POST" && url.pathname.endsWith("/fail")) {
828
+ const context = await authenticateRequest(app, req, res, ["system:write"], "api", `${app.config.publicBaseUrl}/v1`);
829
+ if (!context) {
830
+ return;
831
+ }
832
+ app.auth.requireAnyScope(context, ["system:write", "admin:write"]);
833
+ app.auth.requireRoles(context, ["admin", "operator", "maintenance_operator"]);
834
+ const body = rotationTransitionInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
835
+ const id = rotationId.replace(/\/fail$/, "");
836
+ const rotation = await app.rotations.fail(id, context, body.note);
837
+ respondJson(res, rotation ? 200 : 404, { rotation: rotation ?? null });
838
+ return;
839
+ }
840
+ if (url.pathname === "/v1/system/backups/export" && req.method === "POST") {
841
+ const context = await authenticateRequest(app, req, res, ["backup:read"], "api", `${app.config.publicBaseUrl}/v1`);
842
+ if (!context) {
843
+ return;
844
+ }
845
+ app.auth.requireRoles(context, ["admin", "backup_operator"]);
846
+ const backup = await app.backup.exportBackup(context);
847
+ respondJson(res, 200, { backup, summary: app.backup.summarizeBackup(backup) });
848
+ return;
849
+ }
850
+ if (url.pathname === "/v1/system/backups/inspect" && req.method === "POST") {
851
+ const context = await authenticateRequest(app, req, res, ["backup:read"], "api", `${app.config.publicBaseUrl}/v1`);
852
+ if (!context) {
853
+ return;
854
+ }
855
+ app.auth.requireRoles(context, ["admin", "backup_operator"]);
856
+ const body = z.object({ backup: z.unknown() }).parse(await readJsonBody(req, app.config.maxRequestBytes));
857
+ const backup = app.backup.parseBackupPayload(body.backup);
858
+ respondJson(res, 200, backupInspectOutputSchema.parse({ backup: app.backup.summarizeBackup(backup) }));
859
+ return;
860
+ }
861
+ if (url.pathname === "/v1/system/backups/restore" && req.method === "POST") {
862
+ const context = await authenticateRequest(app, req, res, ["backup:write"], "api", `${app.config.publicBaseUrl}/v1`);
863
+ if (!context) {
864
+ return;
865
+ }
866
+ app.auth.requireRoles(context, ["admin", "backup_operator"]);
867
+ const body = z
868
+ .object({
869
+ confirm: z.literal(true),
870
+ backup: z.unknown(),
871
+ })
872
+ .parse(await readJsonBody(req, app.config.maxRequestBytes));
873
+ const backup = app.backup.parseBackupPayload(body.backup);
874
+ const restored = await app.backup.restoreBackupPayload(backup, context);
875
+ respondJson(res, 200, {
876
+ restored: true,
877
+ backup: app.backup.summarizeBackup(restored),
878
+ });
879
+ return;
880
+ }
881
+ if (url.pathname === "/v1/approvals" && req.method === "GET") {
882
+ const context = await authenticateRequest(app, req, res, ["approval:read"], "api", `${app.config.publicBaseUrl}/v1`);
883
+ if (!context) {
884
+ return;
885
+ }
886
+ app.auth.requireRoles(context, ["admin", "approver"]);
887
+ const status = (url.searchParams.get("status") ?? undefined);
888
+ const approvals = await app.broker.listApprovalRequests(context, status);
889
+ respondJson(res, 200, { approvals });
890
+ return;
891
+ }
892
+ const approvalId = routeParam(url.pathname, "/v1/approvals/");
893
+ if (approvalId && req.method === "POST" && url.pathname.endsWith("/approve")) {
894
+ const context = await authenticateRequest(app, req, res, ["approval:review"], "api", `${app.config.publicBaseUrl}/v1`);
895
+ if (!context) {
896
+ return;
897
+ }
898
+ app.auth.requireRoles(context, ["admin", "approver"]);
899
+ const body = approvalReviewInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
900
+ const id = approvalId.replace(/\/approve$/, "");
901
+ const approval = await app.broker.reviewApprovalRequest(context, id, "approved", body.note);
902
+ respondJson(res, approval ? 200 : 404, { approval: approval ?? null });
903
+ return;
904
+ }
905
+ if (approvalId && req.method === "POST" && url.pathname.endsWith("/deny")) {
906
+ const context = await authenticateRequest(app, req, res, ["approval:review"], "api", `${app.config.publicBaseUrl}/v1`);
907
+ if (!context) {
908
+ return;
909
+ }
910
+ app.auth.requireRoles(context, ["admin", "approver"]);
911
+ const body = approvalReviewInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
912
+ const id = approvalId.replace(/\/deny$/, "");
913
+ const approval = await app.broker.reviewApprovalRequest(context, id, "denied", body.note);
914
+ respondJson(res, approval ? 200 : 404, { approval: approval ?? null });
915
+ return;
916
+ }
917
+ if (url.pathname === "/v1/break-glass" && req.method === "GET") {
918
+ const context = await authenticateRequest(app, req, res, ["breakglass:read"], "api", `${app.config.publicBaseUrl}/v1`);
919
+ if (!context) {
920
+ return;
921
+ }
922
+ app.auth.requireRoles(context, ["admin", "approver", "auditor", "breakglass_operator"]);
923
+ const requests = await app.broker.listBreakGlassRequests(context, {
924
+ status: (url.searchParams.get("status") ?? undefined),
925
+ requestedBy: url.searchParams.get("requestedBy") ?? undefined,
926
+ });
927
+ respondJson(res, 200, { requests });
928
+ return;
929
+ }
930
+ if (url.pathname === "/v1/break-glass" && req.method === "POST") {
931
+ const context = await authenticateRequest(app, req, res, ["breakglass:request"], "api", `${app.config.publicBaseUrl}/v1`);
932
+ if (!context) {
933
+ return;
934
+ }
935
+ app.auth.requireRoles(context, ["admin", "breakglass_operator"]);
936
+ const body = breakGlassRequestInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
937
+ const request = await app.broker.createBreakGlassRequest(context, body);
938
+ respondJson(res, 201, { request });
939
+ return;
940
+ }
941
+ const breakGlassId = routeParam(url.pathname, "/v1/break-glass/");
942
+ if (breakGlassId && req.method === "POST" && url.pathname.endsWith("/approve")) {
943
+ const context = await authenticateRequest(app, req, res, ["breakglass:review"], "api", `${app.config.publicBaseUrl}/v1`);
944
+ if (!context) {
945
+ return;
946
+ }
947
+ app.auth.requireRoles(context, ["admin", "approver"]);
948
+ const body = breakGlassReviewInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
949
+ const id = breakGlassId.replace(/\/approve$/, "");
950
+ const request = await app.broker.reviewBreakGlassRequest(context, id, "active", body.note);
951
+ respondJson(res, request ? 200 : 404, { request: request ?? null });
952
+ return;
953
+ }
954
+ if (breakGlassId && req.method === "POST" && url.pathname.endsWith("/deny")) {
955
+ const context = await authenticateRequest(app, req, res, ["breakglass:review"], "api", `${app.config.publicBaseUrl}/v1`);
956
+ if (!context) {
957
+ return;
958
+ }
959
+ app.auth.requireRoles(context, ["admin", "approver"]);
960
+ const body = breakGlassReviewInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
961
+ const id = breakGlassId.replace(/\/deny$/, "");
962
+ const request = await app.broker.reviewBreakGlassRequest(context, id, "denied", body.note);
963
+ respondJson(res, request ? 200 : 404, { request: request ?? null });
964
+ return;
965
+ }
966
+ if (breakGlassId && req.method === "POST" && url.pathname.endsWith("/revoke")) {
967
+ const context = await authenticateRequest(app, req, res, ["breakglass:review"], "api", `${app.config.publicBaseUrl}/v1`);
968
+ if (!context) {
969
+ return;
970
+ }
971
+ app.auth.requireRoles(context, ["admin", "approver", "breakglass_operator"]);
972
+ const body = breakGlassReviewInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
973
+ const id = breakGlassId.replace(/\/revoke$/, "");
974
+ const request = await app.broker.revokeBreakGlassRequest(context, id, body.note);
975
+ respondJson(res, request ? 200 : 404, { request: request ?? null });
976
+ return;
977
+ }
978
+ const authClientId = routeParam(url.pathname, "/v1/auth/clients/");
979
+ if (authClientId && req.method === "PATCH") {
980
+ const context = await authenticateRequest(app, req, res, ["auth:write"], "api", `${app.config.publicBaseUrl}/v1`);
981
+ if (!context) {
982
+ return;
983
+ }
984
+ app.auth.requireAnyScope(context, ["auth:write", "admin:write"]);
985
+ app.auth.requireRoles(context, ["admin", "auth_admin"]);
986
+ const patch = authClientUpdateInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
987
+ const client = await app.auth.updateClient(context, authClientId, patch);
988
+ respondJson(res, client ? 200 : 404, { client: client ?? null });
989
+ return;
990
+ }
991
+ if (authClientId && req.method === "POST" && url.pathname.endsWith("/rotate-secret")) {
992
+ const context = await authenticateRequest(app, req, res, ["auth:write"], "api", `${app.config.publicBaseUrl}/v1`);
993
+ if (!context) {
994
+ return;
995
+ }
996
+ app.auth.requireAnyScope(context, ["auth:write", "admin:write"]);
997
+ app.auth.requireRoles(context, ["admin", "auth_admin"]);
998
+ const body = authClientRotateSecretInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
999
+ const clientId = authClientId.replace(/\/rotate-secret$/, "");
1000
+ const client = await app.auth.rotateClientSecret(context, clientId, body.clientSecret);
1001
+ respondJson(res, client ? 200 : 404, client ?? { client: null });
1002
+ return;
1003
+ }
1004
+ if (authClientId && req.method === "POST" && url.pathname.endsWith("/enable")) {
1005
+ const context = await authenticateRequest(app, req, res, ["auth:write"], "api", `${app.config.publicBaseUrl}/v1`);
1006
+ if (!context) {
1007
+ return;
1008
+ }
1009
+ app.auth.requireAnyScope(context, ["auth:write", "admin:write"]);
1010
+ app.auth.requireRoles(context, ["admin", "auth_admin"]);
1011
+ const clientId = authClientId.replace(/\/enable$/, "");
1012
+ const client = await app.auth.updateClient(context, clientId, { status: "active" });
1013
+ respondJson(res, client ? 200 : 404, { client: client ?? null });
1014
+ return;
1015
+ }
1016
+ if (authClientId && req.method === "POST" && url.pathname.endsWith("/disable")) {
1017
+ const context = await authenticateRequest(app, req, res, ["auth:write"], "api", `${app.config.publicBaseUrl}/v1`);
1018
+ if (!context) {
1019
+ return;
1020
+ }
1021
+ app.auth.requireAnyScope(context, ["auth:write", "admin:write"]);
1022
+ app.auth.requireRoles(context, ["admin", "auth_admin"]);
1023
+ const clientId = authClientId.replace(/\/disable$/, "");
1024
+ const client = await app.auth.updateClient(context, clientId, { status: "disabled" });
1025
+ respondJson(res, client ? 200 : 404, { client: client ?? null });
1026
+ return;
1027
+ }
1028
+ const tokenId = routeParam(url.pathname, "/v1/auth/tokens/");
1029
+ if (tokenId && req.method === "POST" && url.pathname.endsWith("/revoke")) {
1030
+ const context = await authenticateRequest(app, req, res, ["auth:write"], "api", `${app.config.publicBaseUrl}/v1`);
1031
+ if (!context) {
1032
+ return;
1033
+ }
1034
+ app.auth.requireAnyScope(context, ["auth:write", "admin:write"]);
1035
+ app.auth.requireRoles(context, ["admin", "auth_admin"]);
1036
+ const id = tokenId.replace(/\/revoke$/, "");
1037
+ const token = await app.auth.revokeToken(context, id);
1038
+ respondJson(res, token ? 200 : 404, { token: token ?? null });
1039
+ return;
1040
+ }
1041
+ const refreshTokenId = routeParam(url.pathname, "/v1/auth/refresh-tokens/");
1042
+ if (refreshTokenId && req.method === "POST" && url.pathname.endsWith("/revoke")) {
1043
+ const context = await authenticateRequest(app, req, res, ["auth:write"], "api", `${app.config.publicBaseUrl}/v1`);
1044
+ if (!context) {
1045
+ return;
1046
+ }
1047
+ app.auth.requireAnyScope(context, ["auth:write", "admin:write"]);
1048
+ app.auth.requireRoles(context, ["admin", "auth_admin"]);
1049
+ const id = refreshTokenId.replace(/\/revoke$/, "");
1050
+ const token = await app.auth.revokeRefreshToken(context, id);
1051
+ respondJson(res, token ? 200 : 404, { token: token ?? null });
1052
+ return;
1053
+ }
1054
+ const tenantId = routeParam(url.pathname, "/v1/tenants/");
1055
+ if (tenantId && req.method === "GET") {
1056
+ const context = await authenticateRequest(app, req, res, ["admin:read"], "api", `${app.config.publicBaseUrl}/v1`);
1057
+ if (!context) {
1058
+ return;
1059
+ }
1060
+ app.auth.requireRoles(context, ["admin"]);
1061
+ const tenant = await app.tenants.get(context, tenantId);
1062
+ respondJson(res, tenant ? 200 : 404, { tenant: tenant ?? null });
1063
+ return;
1064
+ }
1065
+ if (tenantId && req.method === "PATCH") {
1066
+ const context = await authenticateRequest(app, req, res, ["admin:write"], "api", `${app.config.publicBaseUrl}/v1`);
1067
+ if (!context) {
1068
+ return;
1069
+ }
1070
+ app.auth.requireRoles(context, ["admin"]);
1071
+ const patch = tenantUpdateInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
1072
+ const tenant = await app.tenants.update(context, tenantId, patch);
1073
+ respondJson(res, tenant ? 200 : 404, { tenant: tenant ?? null });
1074
+ return;
1075
+ }
1076
+ const credentialId = routeParam(url.pathname, "/v1/catalog/credentials/");
1077
+ if (credentialId && req.method === "GET" && url.pathname.endsWith("/report")) {
1078
+ const context = await authenticateRequest(app, req, res, ["catalog:read"], "api", `${app.config.publicBaseUrl}/v1`);
1079
+ if (!context) {
1080
+ return;
1081
+ }
1082
+ app.auth.requireRoles(context, ["admin", "operator", "auditor"]);
1083
+ const id = credentialId.replace(/\/report$/, "");
1084
+ const reports = await app.broker.listCredentialReports(context, id);
1085
+ respondJson(res, reports.length > 0 ? 200 : 404, { reports });
1086
+ return;
1087
+ }
1088
+ if (url.pathname === "/v1/catalog/reports" && req.method === "GET") {
1089
+ const context = await authenticateRequest(app, req, res, ["catalog:read"], "api", `${app.config.publicBaseUrl}/v1`);
1090
+ if (!context) {
1091
+ return;
1092
+ }
1093
+ app.auth.requireRoles(context, ["admin", "operator", "auditor"]);
1094
+ const reports = await app.broker.listCredentialReports(context);
1095
+ respondJson(res, 200, { reports });
1096
+ return;
1097
+ }
1098
+ if (credentialId && req.method === "GET") {
1099
+ const context = await authenticateRequest(app, req, res, ["catalog:read"], "api", `${app.config.publicBaseUrl}/v1`);
1100
+ if (!context) {
1101
+ return;
1102
+ }
1103
+ const credential = await app.broker.getCredential(context, credentialId);
1104
+ if (!credential) {
1105
+ respondJson(res, 404, { error: "Credential not found" });
1106
+ return;
1107
+ }
1108
+ respondJson(res, 200, { credential });
1109
+ return;
1110
+ }
1111
+ if (credentialId && req.method === "PATCH") {
1112
+ const context = await authenticateRequest(app, req, res, ["catalog:write"], "api", `${app.config.publicBaseUrl}/v1`);
1113
+ if (!context) {
1114
+ return;
1115
+ }
1116
+ app.auth.requireRoles(context, ["admin", "operator"]);
1117
+ const patch = updateCredentialInputSchema.parse(await readJsonBody(req, app.config.maxRequestBytes));
1118
+ const credential = await app.broker.updateCredential(context, credentialId, patch);
1119
+ respondJson(res, 200, { credential });
1120
+ return;
1121
+ }
1122
+ if (credentialId && req.method === "DELETE") {
1123
+ const context = await authenticateRequest(app, req, res, ["catalog:write"], "api", `${app.config.publicBaseUrl}/v1`);
1124
+ if (!context) {
1125
+ return;
1126
+ }
1127
+ app.auth.requireRoles(context, ["admin", "operator"]);
1128
+ const deleted = await app.broker.deleteCredential(context, credentialId);
1129
+ respondJson(res, deleted ? 200 : 404, { deleted });
1130
+ return;
1131
+ }
1132
+ respondJson(res, 404, { error: "Not found" });
1133
+ }
1134
+ async function handleMcpRequest(app, req, res, transports, _context) {
1135
+ const sessionIdHeader = req.headers["mcp-session-id"];
1136
+ const sessionId = typeof sessionIdHeader === "string" ? sessionIdHeader : sessionIdHeader?.[0];
1137
+ if (req.method === "GET") {
1138
+ if (!sessionId) {
1139
+ respondText(res, 400, "Missing MCP session identifier.");
1140
+ return;
1141
+ }
1142
+ const existing = transports.get(sessionId);
1143
+ if (!existing) {
1144
+ respondText(res, 404, "Unknown MCP session.");
1145
+ return;
1146
+ }
1147
+ await existing.transport.handleRequest(req, res);
1148
+ return;
1149
+ }
1150
+ if (req.method === "DELETE") {
1151
+ if (!sessionId) {
1152
+ respondText(res, 400, "Missing MCP session identifier.");
1153
+ return;
1154
+ }
1155
+ const existing = transports.get(sessionId);
1156
+ if (!existing) {
1157
+ respondText(res, 404, "Unknown MCP session.");
1158
+ return;
1159
+ }
1160
+ await existing.transport.handleRequest(req, res);
1161
+ await existing.closeServer();
1162
+ transports.delete(sessionId);
1163
+ return;
1164
+ }
1165
+ if (req.method !== "POST") {
1166
+ respondText(res, 405, "Method not allowed.");
1167
+ return;
1168
+ }
1169
+ const body = await readJsonBody(req, app.config.maxRequestBytes);
1170
+ if (sessionId) {
1171
+ const existing = transports.get(sessionId);
1172
+ if (!existing) {
1173
+ respondText(res, 404, "Unknown MCP session.");
1174
+ return;
1175
+ }
1176
+ await existing.transport.handleRequest(req, res, body);
1177
+ return;
1178
+ }
1179
+ if (!isInitializeRequest(body)) {
1180
+ respondJson(res, 400, {
1181
+ jsonrpc: "2.0",
1182
+ error: {
1183
+ code: -32000,
1184
+ message: "Initialization request required.",
1185
+ },
1186
+ id: null,
1187
+ });
1188
+ return;
1189
+ }
1190
+ const connectedServer = createKeyLoreMcpServer(app);
1191
+ const transport = new StreamableHTTPServerTransport({
1192
+ sessionIdGenerator: () => randomUUID(),
1193
+ onsessioninitialized: (newSessionId) => {
1194
+ transports.set(newSessionId, {
1195
+ transport,
1196
+ closeServer: async () => {
1197
+ await connectedServer.close();
1198
+ await transport.close();
1199
+ },
1200
+ });
1201
+ },
1202
+ });
1203
+ transport.onclose = () => {
1204
+ if (transport.sessionId) {
1205
+ transports.delete(transport.sessionId);
1206
+ }
1207
+ };
1208
+ await connectedServer.connect(transport);
1209
+ await transport.handleRequest(req, res, body);
1210
+ }