@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,483 @@
1
+ import fs from "node:fs/promises";
2
+ import { accessRequestInputSchema, approvalReviewInputSchema, authorizationRequestInputSchema, authClientCreateInputSchema, authClientRotateSecretInputSchema, authClientUpdateInputSchema, breakGlassRequestInputSchema, breakGlassReviewInputSchema, catalogSearchInputSchema, createCredentialInputSchema, rotationCompleteInputSchema, rotationCreateInputSchema, rotationTransitionInputSchema, runtimeExecutionInputSchema, tenantBootstrapInputSchema, tenantCreateInputSchema, tenantUpdateInputSchema, updateCredentialInputSchema, } from "../domain/types.js";
3
+ import { localOperatorContext } from "../services/auth-context.js";
4
+ import { parseCliArgs, readBooleanFlag, readNumberFlag, readStringFlag, } from "./args.js";
5
+ function helpText() {
6
+ return `KeyLore CLI
7
+
8
+ Usage:
9
+ keylore help
10
+ keylore version
11
+ keylore catalog list [--principal name] [--limit 20]
12
+ keylore catalog search [--query text] [--service name] [--owner name] [--scope-tier tier] [--sensitivity level] [--status active|disabled] [--tag tag] [--limit 20]
13
+ keylore catalog get <credential-id> [--principal name]
14
+ keylore catalog report [credential-id] [--principal name]
15
+ keylore catalog create --file /path/to/credential.json [--principal name]
16
+ keylore catalog update <credential-id> --file /path/to/patch.json [--principal name]
17
+ keylore catalog delete <credential-id> [--principal name]
18
+ keylore access request --file /path/to/request.json [--dry-run] [--principal name]
19
+ keylore access simulate --file /path/to/request.json [--principal name]
20
+ keylore auth clients list
21
+ keylore auth clients create --file /path/to/client.json
22
+ keylore auth clients update <client-id> --file /path/to/patch.json
23
+ keylore auth clients enable <client-id>
24
+ keylore auth clients disable <client-id>
25
+ keylore auth clients rotate-secret <client-id> [--secret value]
26
+ keylore auth authorize --file /path/to/authorize.json
27
+ keylore auth tokens list [--client-id id] [--status active|revoked]
28
+ keylore auth tokens revoke <token-id>
29
+ keylore auth refresh-tokens list [--client-id id] [--status active|revoked]
30
+ keylore auth refresh-tokens revoke <refresh-token-id>
31
+ keylore tenants list
32
+ keylore tenants get <tenant-id>
33
+ keylore tenants create --file /path/to/tenant.json
34
+ keylore tenants update <tenant-id> --file /path/to/patch.json
35
+ keylore tenants bootstrap --file /path/to/bootstrap.json
36
+ keylore runtime run --file /path/to/runtime.json [--principal name]
37
+ keylore system adapters
38
+ keylore system maintenance
39
+ keylore system maintenance run
40
+ keylore system traces [--limit 20] [--trace-id uuid]
41
+ keylore system trace-exporter
42
+ keylore system trace-exporter flush
43
+ keylore system rotations list [--status pending|in_progress|completed|failed|cancelled] [--credential-id id]
44
+ keylore system rotations plan [--horizon-days 14]
45
+ keylore system rotations create --file /path/to/rotation.json
46
+ keylore system rotations start <rotation-id> [--note text]
47
+ keylore system rotations complete <rotation-id> [--note text] [--target-ref ref] [--expires-at iso8601] [--last-validated-at iso8601]
48
+ keylore system rotations fail <rotation-id> [--note text]
49
+ keylore system backup create --file /path/to/backup.json
50
+ keylore system backup inspect --file /path/to/backup.json
51
+ keylore system backup restore --file /path/to/backup.json --yes
52
+ keylore breakglass list [--status pending|active|denied|expired|revoked] [--requested-by name]
53
+ keylore breakglass request --file /path/to/request.json [--principal name]
54
+ keylore breakglass approve <request-id> [--note text] [--principal name]
55
+ keylore breakglass deny <request-id> [--note text] [--principal name]
56
+ keylore breakglass revoke <request-id> [--note text] [--principal name]
57
+ keylore audit recent [--principal name] [--limit 20]
58
+ keylore approvals list [--status pending|approved|denied|expired]
59
+ keylore approvals approve <approval-id> [--note text]
60
+ keylore approvals deny <approval-id> [--note text]
61
+
62
+ Flags:
63
+ --json Force JSON output. This is the default.
64
+ `;
65
+ }
66
+ function output(value) {
67
+ return `${JSON.stringify(value, null, 2)}\n`;
68
+ }
69
+ async function readJsonFile(filePath) {
70
+ const raw = await fs.readFile(filePath, "utf8");
71
+ return JSON.parse(raw);
72
+ }
73
+ function principalFor(app, flags) {
74
+ return readStringFlag(flags, "principal") ?? app.config.defaultPrincipal;
75
+ }
76
+ export async function runCli(app, argv) {
77
+ const parsed = parseCliArgs(argv);
78
+ const [resource, action, subject] = parsed.positionals;
79
+ const context = localOperatorContext(principalFor(app, parsed.flags));
80
+ void readBooleanFlag(parsed.flags, "json");
81
+ if (!resource || resource === "help" || parsed.flags.get("help") === true) {
82
+ return helpText();
83
+ }
84
+ if (resource === "version") {
85
+ return `${app.config.version}\n`;
86
+ }
87
+ if (resource === "catalog" && action === "list") {
88
+ const limit = readNumberFlag(parsed.flags, "limit") ?? 50;
89
+ const results = await app.broker.searchCatalog(context, { limit });
90
+ return output({ credentials: results });
91
+ }
92
+ if (resource === "catalog" && action === "search") {
93
+ const input = catalogSearchInputSchema.parse({
94
+ query: readStringFlag(parsed.flags, "query"),
95
+ service: readStringFlag(parsed.flags, "service"),
96
+ owner: readStringFlag(parsed.flags, "owner"),
97
+ scopeTier: readStringFlag(parsed.flags, "scope-tier"),
98
+ sensitivity: readStringFlag(parsed.flags, "sensitivity"),
99
+ status: readStringFlag(parsed.flags, "status"),
100
+ tag: readStringFlag(parsed.flags, "tag"),
101
+ limit: readNumberFlag(parsed.flags, "limit") ?? 10,
102
+ });
103
+ const results = await app.broker.searchCatalog(context, input);
104
+ return output({ credentials: results });
105
+ }
106
+ if (resource === "catalog" && action === "get") {
107
+ if (!subject) {
108
+ throw new Error("catalog get requires a credential id.");
109
+ }
110
+ const result = await app.broker.getCredential(context, subject);
111
+ return output({ credential: result ?? null });
112
+ }
113
+ if (resource === "catalog" && action === "report") {
114
+ const reports = await app.broker.listCredentialReports(context, subject);
115
+ return output({ reports });
116
+ }
117
+ if (resource === "catalog" && action === "create") {
118
+ const filePath = readStringFlag(parsed.flags, "file");
119
+ if (!filePath) {
120
+ throw new Error("catalog create requires --file.");
121
+ }
122
+ const payload = createCredentialInputSchema.parse(await readJsonFile(filePath));
123
+ const created = await app.broker.createCredential(context, payload);
124
+ return output({ credential: created });
125
+ }
126
+ if (resource === "catalog" && action === "update") {
127
+ if (!subject) {
128
+ throw new Error("catalog update requires a credential id.");
129
+ }
130
+ const filePath = readStringFlag(parsed.flags, "file");
131
+ if (!filePath) {
132
+ throw new Error("catalog update requires --file.");
133
+ }
134
+ const payload = updateCredentialInputSchema.parse(await readJsonFile(filePath));
135
+ const updated = await app.broker.updateCredential(context, subject, payload);
136
+ return output({ credential: updated });
137
+ }
138
+ if (resource === "catalog" && action === "delete") {
139
+ if (!subject) {
140
+ throw new Error("catalog delete requires a credential id.");
141
+ }
142
+ const deleted = await app.broker.deleteCredential(context, subject);
143
+ return output({ deleted, credentialId: subject });
144
+ }
145
+ if (resource === "access" && (action === "request" || action === "simulate")) {
146
+ const filePath = readStringFlag(parsed.flags, "file");
147
+ if (!filePath) {
148
+ throw new Error(`access ${action} requires --file.`);
149
+ }
150
+ const payload = accessRequestInputSchema.parse(await readJsonFile(filePath));
151
+ if (action === "simulate") {
152
+ const decision = await app.broker.simulateAccess(context, payload);
153
+ return output({ decision });
154
+ }
155
+ const decision = await app.broker.requestAccess(context, {
156
+ ...payload,
157
+ dryRun: readBooleanFlag(parsed.flags, "dry-run") || payload.dryRun,
158
+ });
159
+ return output({ decision });
160
+ }
161
+ if (resource === "auth" && action === "clients" && subject === "list") {
162
+ const clients = (await app.auth.listClients()).filter((client) => !context.tenantId || client.tenantId === context.tenantId);
163
+ return output({ clients });
164
+ }
165
+ if (resource === "auth" && action === "clients" && subject === "create") {
166
+ const filePath = readStringFlag(parsed.flags, "file");
167
+ if (!filePath) {
168
+ throw new Error("auth clients create requires --file.");
169
+ }
170
+ const payload = authClientCreateInputSchema.parse(await readJsonFile(filePath));
171
+ const client = await app.auth.createClient(context, payload);
172
+ return output(client);
173
+ }
174
+ if (resource === "auth" && action === "clients" && subject === "update") {
175
+ const clientId = parsed.positionals[3];
176
+ if (!clientId) {
177
+ throw new Error("auth clients update requires a client id.");
178
+ }
179
+ const filePath = readStringFlag(parsed.flags, "file");
180
+ if (!filePath) {
181
+ throw new Error("auth clients update requires --file.");
182
+ }
183
+ const payload = authClientUpdateInputSchema.parse(await readJsonFile(filePath));
184
+ const client = await app.auth.updateClient(context, clientId, payload);
185
+ return output({ client: client ?? null });
186
+ }
187
+ if (resource === "auth" && action === "clients" && (subject === "enable" || subject === "disable")) {
188
+ const clientId = parsed.positionals[3];
189
+ if (!clientId) {
190
+ throw new Error(`auth clients ${subject} requires a client id.`);
191
+ }
192
+ const client = await app.auth.updateClient(context, clientId, {
193
+ status: subject === "enable" ? "active" : "disabled",
194
+ });
195
+ return output({ client: client ?? null });
196
+ }
197
+ if (resource === "auth" && action === "clients" && subject === "rotate-secret") {
198
+ const clientId = parsed.positionals[3];
199
+ if (!clientId) {
200
+ throw new Error("auth clients rotate-secret requires a client id.");
201
+ }
202
+ const secret = authClientRotateSecretInputSchema.parse({
203
+ clientSecret: readStringFlag(parsed.flags, "secret"),
204
+ }).clientSecret;
205
+ const result = await app.auth.rotateClientSecret(context, clientId, secret);
206
+ return output(result ? result : { client: null });
207
+ }
208
+ if (resource === "auth" && action === "authorize") {
209
+ const filePath = readStringFlag(parsed.flags, "file");
210
+ if (!filePath) {
211
+ throw new Error("auth authorize requires --file.");
212
+ }
213
+ const payload = authorizationRequestInputSchema.parse(await readJsonFile(filePath));
214
+ const authorization = await app.auth.authorize(context, payload);
215
+ return output({ authorization });
216
+ }
217
+ if (resource === "auth" && action === "tokens" && subject === "list") {
218
+ const tokens = await app.auth.listTokens({
219
+ clientId: readStringFlag(parsed.flags, "client-id"),
220
+ tenantId: context.tenantId,
221
+ status: readStringFlag(parsed.flags, "status"),
222
+ });
223
+ return output({ tokens });
224
+ }
225
+ if (resource === "auth" && action === "tokens" && subject === "revoke") {
226
+ const tokenId = parsed.positionals[3];
227
+ if (!tokenId) {
228
+ throw new Error("auth tokens revoke requires a token id.");
229
+ }
230
+ const token = await app.auth.revokeToken(context, tokenId);
231
+ return output({ token: token ?? null });
232
+ }
233
+ if (resource === "auth" && action === "refresh-tokens" && subject === "list") {
234
+ const tokens = await app.auth.listRefreshTokens({
235
+ clientId: readStringFlag(parsed.flags, "client-id"),
236
+ tenantId: context.tenantId,
237
+ status: readStringFlag(parsed.flags, "status") ?? undefined,
238
+ });
239
+ return output({ tokens });
240
+ }
241
+ if (resource === "auth" && action === "refresh-tokens" && subject === "revoke") {
242
+ const refreshTokenId = parsed.positionals[3];
243
+ if (!refreshTokenId) {
244
+ throw new Error("auth refresh-tokens revoke requires a refresh token id.");
245
+ }
246
+ const token = await app.auth.revokeRefreshToken(context, refreshTokenId);
247
+ return output({ token: token ?? null });
248
+ }
249
+ if (resource === "tenants" && action === "list") {
250
+ const tenants = await app.tenants.list(context);
251
+ return output({ tenants });
252
+ }
253
+ if (resource === "tenants" && action === "get") {
254
+ if (!subject) {
255
+ throw new Error("tenants get requires a tenant id.");
256
+ }
257
+ const tenant = await app.tenants.get(context, subject);
258
+ return output({ tenant: tenant ?? null });
259
+ }
260
+ if (resource === "tenants" && action === "create") {
261
+ const filePath = readStringFlag(parsed.flags, "file");
262
+ if (!filePath) {
263
+ throw new Error("tenants create requires --file.");
264
+ }
265
+ const payload = tenantCreateInputSchema.parse(await readJsonFile(filePath));
266
+ const tenant = await app.tenants.create(context, payload);
267
+ return output({ tenant });
268
+ }
269
+ if (resource === "tenants" && action === "update") {
270
+ if (!subject) {
271
+ throw new Error("tenants update requires a tenant id.");
272
+ }
273
+ const filePath = readStringFlag(parsed.flags, "file");
274
+ if (!filePath) {
275
+ throw new Error("tenants update requires --file.");
276
+ }
277
+ const payload = tenantUpdateInputSchema.parse(await readJsonFile(filePath));
278
+ const tenant = await app.tenants.update(context, subject, payload);
279
+ return output({ tenant: tenant ?? null });
280
+ }
281
+ if (resource === "tenants" && action === "bootstrap") {
282
+ const filePath = readStringFlag(parsed.flags, "file");
283
+ if (!filePath) {
284
+ throw new Error("tenants bootstrap requires --file.");
285
+ }
286
+ const payload = tenantBootstrapInputSchema.parse(await readJsonFile(filePath));
287
+ const result = await app.tenants.bootstrap(context, payload);
288
+ return output(result);
289
+ }
290
+ if (resource === "runtime" && action === "run") {
291
+ const filePath = readStringFlag(parsed.flags, "file");
292
+ if (!filePath) {
293
+ throw new Error("runtime run requires --file.");
294
+ }
295
+ const payload = runtimeExecutionInputSchema.parse(await readJsonFile(filePath));
296
+ const result = await app.broker.runSandboxed(context, payload);
297
+ return output({ result });
298
+ }
299
+ if (resource === "system" && action === "adapters") {
300
+ const adapters = await app.broker.adapterHealth();
301
+ return output({ adapters });
302
+ }
303
+ if (resource === "system" && action === "maintenance" && !subject) {
304
+ return output({ maintenance: app.maintenance.status() });
305
+ }
306
+ if (resource === "system" && action === "maintenance" && subject === "run") {
307
+ const result = await app.maintenance.runOnce();
308
+ return output({ maintenance: app.maintenance.status(), result });
309
+ }
310
+ if (resource === "system" && action === "traces") {
311
+ const limit = readNumberFlag(parsed.flags, "limit") ?? 20;
312
+ const traceId = readStringFlag(parsed.flags, "trace-id");
313
+ return output({ traces: app.traces.recent(limit, traceId) });
314
+ }
315
+ if (resource === "system" && action === "trace-exporter" && !subject) {
316
+ return output({ exporter: app.traceExports.status() });
317
+ }
318
+ if (resource === "system" && action === "trace-exporter" && subject === "flush") {
319
+ return output({ exporter: await app.traceExports.flushNow() });
320
+ }
321
+ if (resource === "system" && action === "rotations" && subject === "list") {
322
+ const rotations = await app.rotations.list({
323
+ tenantId: context.tenantId,
324
+ status: readStringFlag(parsed.flags, "status"),
325
+ credentialId: readStringFlag(parsed.flags, "credential-id"),
326
+ });
327
+ return output({ rotations });
328
+ }
329
+ if (resource === "system" && action === "rotations" && subject === "plan") {
330
+ const rotations = await app.rotations.planDue(context, {
331
+ horizonDays: readNumberFlag(parsed.flags, "horizon-days") ?? app.config.rotationPlanningHorizonDays,
332
+ });
333
+ return output({ rotations });
334
+ }
335
+ if (resource === "system" && action === "rotations" && subject === "create") {
336
+ const filePath = readStringFlag(parsed.flags, "file");
337
+ if (!filePath) {
338
+ throw new Error("system rotations create requires --file.");
339
+ }
340
+ const payload = rotationCreateInputSchema.parse(await readJsonFile(filePath));
341
+ return output({ rotation: await app.rotations.createManual(context, payload) });
342
+ }
343
+ if (resource === "system" && action === "rotations" && (subject === "start" || subject === "fail")) {
344
+ const rotationId = parsed.positionals[3];
345
+ if (!rotationId) {
346
+ throw new Error(`system rotations ${subject} requires a rotation id.`);
347
+ }
348
+ const payload = rotationTransitionInputSchema.parse({
349
+ note: readStringFlag(parsed.flags, "note"),
350
+ });
351
+ const rotation = subject === "start"
352
+ ? await app.rotations.start(rotationId, context, payload.note)
353
+ : await app.rotations.fail(rotationId, context, payload.note);
354
+ return output({ rotation: rotation ?? null });
355
+ }
356
+ if (resource === "system" && action === "rotations" && subject === "complete") {
357
+ const rotationId = parsed.positionals[3];
358
+ if (!rotationId) {
359
+ throw new Error("system rotations complete requires a rotation id.");
360
+ }
361
+ const payload = rotationCompleteInputSchema.parse({
362
+ note: readStringFlag(parsed.flags, "note"),
363
+ targetRef: readStringFlag(parsed.flags, "target-ref"),
364
+ expiresAt: readStringFlag(parsed.flags, "expires-at"),
365
+ lastValidatedAt: readStringFlag(parsed.flags, "last-validated-at"),
366
+ });
367
+ const rotation = await app.rotations.complete(rotationId, context, payload);
368
+ return output({ rotation: rotation ?? null });
369
+ }
370
+ if (resource === "system" && action === "backup" && subject === "create") {
371
+ const filePath = readStringFlag(parsed.flags, "file");
372
+ if (!filePath) {
373
+ throw new Error("system backup create requires --file.");
374
+ }
375
+ const backup = await app.backup.writeBackup(filePath, context);
376
+ return output({
377
+ file: filePath,
378
+ createdAt: backup.createdAt,
379
+ credentials: backup.credentials.length,
380
+ authClients: backup.authClients.length,
381
+ accessTokens: backup.accessTokens.length,
382
+ approvals: backup.approvals.length,
383
+ breakGlassRequests: backup.breakGlassRequests.length,
384
+ rotationRuns: backup.rotationRuns.length,
385
+ auditEvents: backup.auditEvents.length,
386
+ });
387
+ }
388
+ if (resource === "system" && action === "backup" && subject === "inspect") {
389
+ const filePath = readStringFlag(parsed.flags, "file");
390
+ if (!filePath) {
391
+ throw new Error("system backup inspect requires --file.");
392
+ }
393
+ const backup = await app.backup.readBackup(filePath);
394
+ return output({
395
+ file: filePath,
396
+ format: backup.format,
397
+ version: backup.version,
398
+ sourceVersion: backup.sourceVersion,
399
+ createdAt: backup.createdAt,
400
+ credentials: backup.credentials.length,
401
+ authClients: backup.authClients.length,
402
+ accessTokens: backup.accessTokens.length,
403
+ approvals: backup.approvals.length,
404
+ breakGlassRequests: backup.breakGlassRequests.length,
405
+ rotationRuns: backup.rotationRuns.length,
406
+ auditEvents: backup.auditEvents.length,
407
+ });
408
+ }
409
+ if (resource === "system" && action === "backup" && subject === "restore") {
410
+ const filePath = readStringFlag(parsed.flags, "file");
411
+ if (!filePath) {
412
+ throw new Error("system backup restore requires --file.");
413
+ }
414
+ if (!readBooleanFlag(parsed.flags, "yes")) {
415
+ throw new Error("system backup restore requires --yes.");
416
+ }
417
+ const backup = await app.backup.restoreBackup(filePath, context);
418
+ return output({
419
+ restored: true,
420
+ file: filePath,
421
+ sourceVersion: backup.sourceVersion,
422
+ createdAt: backup.createdAt,
423
+ credentials: backup.credentials.length,
424
+ authClients: backup.authClients.length,
425
+ accessTokens: backup.accessTokens.length,
426
+ approvals: backup.approvals.length,
427
+ breakGlassRequests: backup.breakGlassRequests.length,
428
+ rotationRuns: backup.rotationRuns.length,
429
+ auditEvents: backup.auditEvents.length,
430
+ });
431
+ }
432
+ if (resource === "audit" && action === "recent") {
433
+ const limit = readNumberFlag(parsed.flags, "limit") ?? 20;
434
+ const events = await app.broker.listRecentAuditEvents(context, limit);
435
+ return output({ events });
436
+ }
437
+ if (resource === "breakglass" && action === "list") {
438
+ const requests = await app.broker.listBreakGlassRequests(context, {
439
+ status: readStringFlag(parsed.flags, "status"),
440
+ requestedBy: readStringFlag(parsed.flags, "requested-by"),
441
+ });
442
+ return output({ requests });
443
+ }
444
+ if (resource === "breakglass" && action === "request") {
445
+ const filePath = readStringFlag(parsed.flags, "file");
446
+ if (!filePath) {
447
+ throw new Error("breakglass request requires --file.");
448
+ }
449
+ const payload = breakGlassRequestInputSchema.parse(await readJsonFile(filePath));
450
+ const request = await app.broker.createBreakGlassRequest(context, payload);
451
+ return output({ request });
452
+ }
453
+ if (resource === "breakglass" && (action === "approve" || action === "deny" || action === "revoke")) {
454
+ if (!subject) {
455
+ throw new Error(`breakglass ${action} requires a request id.`);
456
+ }
457
+ const note = breakGlassReviewInputSchema.parse({
458
+ note: readStringFlag(parsed.flags, "note"),
459
+ }).note;
460
+ const request = action === "approve"
461
+ ? await app.broker.reviewBreakGlassRequest(context, subject, "active", note)
462
+ : action === "deny"
463
+ ? await app.broker.reviewBreakGlassRequest(context, subject, "denied", note)
464
+ : await app.broker.revokeBreakGlassRequest(context, subject, note);
465
+ return output({ request: request ?? null });
466
+ }
467
+ if (resource === "approvals" && action === "list") {
468
+ const status = readStringFlag(parsed.flags, "status");
469
+ const approvals = await app.broker.listApprovalRequests(context, status);
470
+ return output({ approvals });
471
+ }
472
+ if (resource === "approvals" && (action === "approve" || action === "deny")) {
473
+ if (!subject) {
474
+ throw new Error(`approvals ${action} requires an approval id.`);
475
+ }
476
+ const note = approvalReviewInputSchema.parse({
477
+ note: readStringFlag(parsed.flags, "note"),
478
+ }).note;
479
+ const approval = await app.broker.reviewApprovalRequest(context, subject, action === "approve" ? "approved" : "denied", note);
480
+ return output({ approval: approval ?? null });
481
+ }
482
+ throw new Error(`Unknown command: ${parsed.positionals.join(" ")}`);
483
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { createKeyLoreApp } from "./app.js";
3
+ import { runCli } from "./cli/run.js";
4
+ async function main() {
5
+ const app = await createKeyLoreApp();
6
+ try {
7
+ const result = await runCli(app, process.argv.slice(2));
8
+ process.stdout.write(result);
9
+ }
10
+ finally {
11
+ await app.close();
12
+ }
13
+ }
14
+ main().catch((error) => {
15
+ const message = error instanceof Error ? error.message : String(error);
16
+ process.stderr.write(`${message}\n`);
17
+ process.exit(1);
18
+ });