@clef-sh/cli 0.1.1 → 0.1.2

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.
@@ -1,643 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.createApiRouter = createApiRouter;
37
- const path = __importStar(require("path"));
38
- const express_1 = require("express");
39
- const core_1 = require("@clef-sh/core");
40
- function createApiRouter(deps) {
41
- const router = (0, express_1.Router)();
42
- const parser = new core_1.ManifestParser();
43
- const matrix = new core_1.MatrixManager();
44
- const sops = new core_1.SopsClient(deps.runner);
45
- const diffEngine = new core_1.DiffEngine();
46
- const schemaValidator = new core_1.SchemaValidator();
47
- const lintRunner = new core_1.LintRunner(matrix, schemaValidator, sops);
48
- const git = new core_1.GitIntegration(deps.runner);
49
- const scanRunner = new core_1.ScanRunner(deps.runner);
50
- const recipientManager = new core_1.RecipientManager(sops, matrix);
51
- // In-session scan cache
52
- let lastScanResult = null;
53
- let lastScanAt = null;
54
- function loadManifest() {
55
- const manifestPath = `${deps.repoRoot}/clef.yaml`;
56
- return parser.parse(manifestPath);
57
- }
58
- function setNoCacheHeaders(res) {
59
- res.set({
60
- "Cache-Control": "no-store, no-cache, must-revalidate",
61
- Pragma: "no-cache",
62
- Expires: "0",
63
- });
64
- }
65
- // GET /api/manifest
66
- router.get("/manifest", (_req, res) => {
67
- try {
68
- const manifest = loadManifest();
69
- res.json(manifest);
70
- }
71
- catch (err) {
72
- const message = err instanceof Error ? err.message : "Failed to load manifest";
73
- res.status(500).json({ error: message, code: "MANIFEST_ERROR" });
74
- }
75
- });
76
- // GET /api/matrix
77
- router.get("/matrix", async (_req, res) => {
78
- try {
79
- const manifest = loadManifest();
80
- const statuses = await matrix.getMatrixStatus(manifest, deps.repoRoot, sops);
81
- res.json(statuses);
82
- }
83
- catch (err) {
84
- const message = err instanceof Error ? err.message : "Failed to get matrix status";
85
- res.status(500).json({ error: message, code: "MATRIX_ERROR" });
86
- }
87
- });
88
- // GET /api/namespace/:ns/:env
89
- router.get("/namespace/:ns/:env", async (req, res) => {
90
- setNoCacheHeaders(res);
91
- try {
92
- const manifest = loadManifest();
93
- const { ns, env } = req.params;
94
- const nsExists = manifest.namespaces.some((n) => n.name === ns);
95
- const envExists = manifest.environments.some((e) => e.name === env);
96
- if (!nsExists || !envExists) {
97
- res.status(404).json({
98
- error: `Namespace '${ns}' or environment '${env}' not found in manifest.`,
99
- code: "NOT_FOUND",
100
- });
101
- return;
102
- }
103
- const filePath = `${deps.repoRoot}/${manifest.file_pattern.replace("{namespace}", ns).replace("{environment}", env)}`;
104
- const decrypted = await sops.decrypt(filePath);
105
- // Read pending keys from metadata (plaintext sidecar)
106
- let pending = [];
107
- try {
108
- pending = await (0, core_1.getPendingKeys)(filePath);
109
- }
110
- catch {
111
- // Metadata unreadable — no pending info
112
- }
113
- res.json({ ...decrypted, pending });
114
- }
115
- catch (err) {
116
- const message = err instanceof Error ? err.message : "Failed to decrypt namespace";
117
- res.status(500).json({ error: message, code: "DECRYPT_ERROR" });
118
- }
119
- });
120
- // PUT /api/namespace/:ns/:env/:key
121
- // body: { value: string } — set a specific value
122
- // body: { random: true } — generate random value server-side and mark pending
123
- router.put("/namespace/:ns/:env/:key", async (req, res) => {
124
- setNoCacheHeaders(res);
125
- try {
126
- const manifest = loadManifest();
127
- const { ns, env, key } = req.params;
128
- const { value, random, confirmed } = req.body;
129
- if (!random && (value === undefined || value === null)) {
130
- res.status(400).json({
131
- error: "Request body must include 'value' or 'random: true'.",
132
- code: "BAD_REQUEST",
133
- });
134
- return;
135
- }
136
- const nsExists = manifest.namespaces.some((n) => n.name === ns);
137
- const envExists = manifest.environments.some((e) => e.name === env);
138
- if (!nsExists || !envExists) {
139
- res.status(404).json({
140
- error: `Namespace '${ns}' or environment '${env}' not found in manifest.`,
141
- code: "NOT_FOUND",
142
- });
143
- return;
144
- }
145
- if (matrix.isProtectedEnvironment(manifest, env) && !confirmed) {
146
- res.status(409).json({
147
- error: "Protected environment requires confirmation",
148
- code: "PROTECTED_ENV",
149
- protected: true,
150
- });
151
- return;
152
- }
153
- const filePath = `${deps.repoRoot}/${manifest.file_pattern.replace("{namespace}", ns).replace("{environment}", env)}`;
154
- const decrypted = await sops.decrypt(filePath);
155
- if (random) {
156
- // Generate random value server-side and mark as pending
157
- const randomValue = (0, core_1.generateRandomValue)();
158
- const previousValue = decrypted.values[key];
159
- decrypted.values[key] = randomValue;
160
- await sops.encrypt(filePath, decrypted.values, manifest, env);
161
- try {
162
- await (0, core_1.markPendingWithRetry)(filePath, [key], "clef ui");
163
- }
164
- catch {
165
- // Both retry attempts failed — roll back the encrypt
166
- try {
167
- if (previousValue !== undefined) {
168
- decrypted.values[key] = previousValue;
169
- }
170
- else {
171
- delete decrypted.values[key];
172
- }
173
- await sops.encrypt(filePath, decrypted.values, manifest, env);
174
- }
175
- catch {
176
- // Rollback also failed — return 500 with context
177
- return res.status(500).json({
178
- error: "Partial failure",
179
- message: "Value was encrypted but pending state could not be recorded. " +
180
- "Rollback also failed. The key may have a random placeholder value. " +
181
- "Check the file manually.",
182
- code: "PARTIAL_FAILURE",
183
- });
184
- }
185
- return res.status(500).json({
186
- error: "Pending state could not be recorded",
187
- message: "The operation was rolled back. No changes were made.",
188
- code: "PENDING_FAILURE",
189
- });
190
- }
191
- res.json({ success: true, key, pending: true });
192
- }
193
- else {
194
- decrypted.values[key] = String(value);
195
- await sops.encrypt(filePath, decrypted.values, manifest, env);
196
- // Validate against schema if defined (B1)
197
- const nsDef = manifest.namespaces.find((n) => n.name === ns);
198
- if (nsDef?.schema) {
199
- try {
200
- const schema = schemaValidator.loadSchema(path.join(deps.repoRoot, nsDef.schema));
201
- const result = schemaValidator.validate({ [key]: String(value) }, schema);
202
- const violations = [...result.errors, ...result.warnings];
203
- if (violations.length > 0) {
204
- // Resolve pending state if the key was pending
205
- try {
206
- await (0, core_1.markResolved)(filePath, [key]);
207
- }
208
- catch {
209
- // Metadata update failed — non-fatal
210
- }
211
- return res.json({
212
- success: true,
213
- key,
214
- warnings: violations.map((v) => v.message),
215
- });
216
- }
217
- }
218
- catch {
219
- // Schema load failed — skip validation, not fatal
220
- }
221
- }
222
- // Resolve pending state if the key was pending
223
- try {
224
- await (0, core_1.markResolved)(filePath, [key]);
225
- }
226
- catch {
227
- // Metadata update failed — non-fatal
228
- }
229
- res.json({ success: true, key });
230
- }
231
- }
232
- catch (err) {
233
- const message = err instanceof Error ? err.message : "Failed to set value";
234
- res.status(500).json({ error: message, code: "SET_ERROR" });
235
- }
236
- });
237
- // DELETE /api/namespace/:ns/:env/:key
238
- router.delete("/namespace/:ns/:env/:key", async (req, res) => {
239
- setNoCacheHeaders(res);
240
- try {
241
- const manifest = loadManifest();
242
- const { ns, env, key } = req.params;
243
- const { confirmed } = req.body;
244
- const nsExists = manifest.namespaces.some((n) => n.name === ns);
245
- const envExists = manifest.environments.some((e) => e.name === env);
246
- if (!nsExists || !envExists) {
247
- res.status(404).json({
248
- error: `Namespace '${ns}' or environment '${env}' not found in manifest.`,
249
- code: "NOT_FOUND",
250
- });
251
- return;
252
- }
253
- if (matrix.isProtectedEnvironment(manifest, env) && !confirmed) {
254
- res.status(409).json({
255
- error: "Protected environment requires confirmation",
256
- code: "PROTECTED_ENV",
257
- protected: true,
258
- });
259
- return;
260
- }
261
- const filePath = `${deps.repoRoot}/${manifest.file_pattern.replace("{namespace}", ns).replace("{environment}", env)}`;
262
- const decrypted = await sops.decrypt(filePath);
263
- if (!(key in decrypted.values)) {
264
- res.status(404).json({
265
- error: `Key '${key}' not found in ${ns}/${env}.`,
266
- code: "KEY_NOT_FOUND",
267
- });
268
- return;
269
- }
270
- delete decrypted.values[key];
271
- await sops.encrypt(filePath, decrypted.values, manifest, env);
272
- // Clean up pending metadata if it exists
273
- try {
274
- await (0, core_1.markResolved)(filePath, [key]);
275
- }
276
- catch {
277
- // Best effort — orphaned metadata is annoying but not dangerous
278
- }
279
- res.json({ success: true, key });
280
- }
281
- catch (err) {
282
- const message = err instanceof Error ? err.message : "Failed to delete key";
283
- res.status(500).json({ error: message, code: "DELETE_ERROR" });
284
- }
285
- });
286
- // GET /api/diff/:ns/:envA/:envB
287
- router.get("/diff/:ns/:envA/:envB", async (req, res) => {
288
- setNoCacheHeaders(res);
289
- try {
290
- const manifest = loadManifest();
291
- const { ns, envA, envB } = req.params;
292
- const nsExists = manifest.namespaces.some((n) => n.name === ns);
293
- const envAExists = manifest.environments.some((e) => e.name === envA);
294
- const envBExists = manifest.environments.some((e) => e.name === envB);
295
- if (!nsExists || !envAExists || !envBExists) {
296
- res.status(404).json({
297
- error: `Namespace '${ns}', environment '${envA}', or environment '${envB}' not found.`,
298
- code: "NOT_FOUND",
299
- });
300
- return;
301
- }
302
- const result = await diffEngine.diffFiles(ns, envA, envB, manifest, sops, deps.repoRoot);
303
- res.json(result);
304
- }
305
- catch (err) {
306
- const message = err instanceof Error ? err.message : "Failed to compute diff";
307
- res.status(500).json({ error: message, code: "DIFF_ERROR" });
308
- }
309
- });
310
- // GET /api/lint/:namespace
311
- router.get("/lint/:namespace", async (req, res) => {
312
- try {
313
- const manifest = loadManifest();
314
- const { namespace } = req.params;
315
- const nsExists = manifest.namespaces.some((n) => n.name === namespace);
316
- if (!nsExists) {
317
- res.status(404).json({
318
- error: `Namespace '${namespace}' not found in manifest.`,
319
- code: "NOT_FOUND",
320
- });
321
- return;
322
- }
323
- const result = await lintRunner.run(manifest, deps.repoRoot);
324
- const filtered = result.issues.filter((issue) => {
325
- const issueNs = issue.file.split("/")[0];
326
- return issueNs === namespace;
327
- });
328
- res.json({ issues: filtered, fileCount: result.fileCount });
329
- }
330
- catch (err) {
331
- const message = err instanceof Error ? err.message : "Failed to run lint";
332
- res.status(500).json({ error: message, code: "LINT_ERROR" });
333
- }
334
- });
335
- // GET /api/lint
336
- router.get("/lint", async (_req, res) => {
337
- try {
338
- const manifest = loadManifest();
339
- const result = await lintRunner.run(manifest, deps.repoRoot);
340
- res.json(result);
341
- }
342
- catch (err) {
343
- const message = err instanceof Error ? err.message : "Failed to run lint";
344
- res.status(500).json({ error: message, code: "LINT_ERROR" });
345
- }
346
- });
347
- // POST /api/lint/fix
348
- router.post("/lint/fix", async (_req, res) => {
349
- try {
350
- const manifest = loadManifest();
351
- const result = await lintRunner.fix(manifest, deps.repoRoot);
352
- res.json(result);
353
- }
354
- catch (err) {
355
- const message = err instanceof Error ? err.message : "Failed to run lint fix";
356
- res.status(500).json({ error: message, code: "LINT_FIX_ERROR" });
357
- }
358
- });
359
- // POST /api/git/commit
360
- router.post("/git/commit", async (req, res) => {
361
- try {
362
- const { message } = req.body;
363
- if (!message || typeof message !== "string") {
364
- res.status(400).json({
365
- error: "Request body must include a 'message' string.",
366
- code: "BAD_REQUEST",
367
- });
368
- return;
369
- }
370
- // Stage all modified encrypted files and metadata files
371
- const status = await git.getStatus(deps.repoRoot);
372
- const clefFiles = [...status.staged, ...status.unstaged, ...status.untracked].filter((f) => f.endsWith(".enc.yaml") || f.endsWith(".enc.json") || f.endsWith(".clef-meta.yaml"));
373
- if (clefFiles.length === 0) {
374
- res.status(400).json({
375
- error: "No changes to commit",
376
- code: "NOTHING_TO_COMMIT",
377
- });
378
- return;
379
- }
380
- await git.stageFiles(clefFiles, deps.repoRoot);
381
- const hash = await git.commit(message, deps.repoRoot);
382
- res.json({ hash });
383
- }
384
- catch (err) {
385
- const message = err instanceof Error ? err.message : "Failed to commit";
386
- res.status(500).json({ error: message, code: "GIT_ERROR" });
387
- }
388
- });
389
- // GET /api/git/status
390
- router.get("/git/status", async (_req, res) => {
391
- try {
392
- const status = await git.getStatus(deps.repoRoot);
393
- res.json(status);
394
- }
395
- catch (err) {
396
- const message = err instanceof Error ? err.message : "Failed to get git status";
397
- res.status(500).json({ error: message, code: "GIT_ERROR" });
398
- }
399
- });
400
- // GET /api/git/diff
401
- router.get("/git/diff", async (_req, res) => {
402
- setNoCacheHeaders(res);
403
- try {
404
- const diff = await git.getDiff(deps.repoRoot);
405
- res.json({ diff });
406
- }
407
- catch (err) {
408
- const message = err instanceof Error ? err.message : "Could not get diff";
409
- res.status(500).json({ error: message, code: "GIT_DIFF_ERROR" });
410
- }
411
- });
412
- // GET /api/git/log/:ns/:env
413
- router.get("/git/log/:ns/:env", async (req, res) => {
414
- try {
415
- const manifest = loadManifest();
416
- const { ns, env } = req.params;
417
- const nsExists = manifest.namespaces.some((n) => n.name === ns);
418
- const envExists = manifest.environments.some((e) => e.name === env);
419
- if (!nsExists || !envExists) {
420
- res.status(404).json({
421
- error: `Namespace '${ns}' or environment '${env}' not found in manifest.`,
422
- code: "NOT_FOUND",
423
- });
424
- return;
425
- }
426
- const filePath = manifest.file_pattern
427
- .replace("{namespace}", ns)
428
- .replace("{environment}", env);
429
- const log = await git.getLog(filePath, deps.repoRoot);
430
- res.json({ log });
431
- }
432
- catch (err) {
433
- const message = err instanceof Error ? err.message : "Could not get log";
434
- res.status(500).json({ error: message, code: "GIT_LOG_ERROR" });
435
- }
436
- });
437
- // POST /api/scan
438
- router.post("/scan", async (req, res) => {
439
- try {
440
- const manifest = loadManifest();
441
- const { severity, paths } = req.body;
442
- const result = await scanRunner.scan(deps.repoRoot, manifest, {
443
- severity: severity === "high" ? "high" : "all",
444
- paths: paths && paths.length > 0 ? paths : undefined,
445
- });
446
- lastScanResult = result;
447
- lastScanAt = new Date().toISOString();
448
- res.json(result);
449
- }
450
- catch (err) {
451
- const message = err instanceof Error ? err.message : "Scan failed";
452
- res.status(500).json({ error: message, code: "SCAN_ERROR" });
453
- }
454
- });
455
- // GET /api/scan/status — last scan result for this session
456
- router.get("/scan/status", (_req, res) => {
457
- res.json({ lastRun: lastScanResult, lastRunAt: lastScanAt });
458
- });
459
- // POST /api/editor/open — open a file in the OS default editor
460
- router.post("/editor/open", async (req, res) => {
461
- try {
462
- const { file } = req.body;
463
- if (!file || typeof file !== "string") {
464
- res
465
- .status(400)
466
- .json({ error: "Request body must include a 'file' string.", code: "BAD_REQUEST" });
467
- return;
468
- }
469
- const resolved = path.resolve(deps.repoRoot, file);
470
- if (!resolved.startsWith(deps.repoRoot + path.sep) && resolved !== deps.repoRoot) {
471
- res.status(400).json({
472
- error: "File path must be within the repository.",
473
- code: "BAD_REQUEST",
474
- });
475
- return;
476
- }
477
- const editor = process.env.EDITOR || (process.env.TERM_PROGRAM === "vscode" ? "code" : "");
478
- if (!editor) {
479
- res.status(500).json({
480
- error: "No editor configured. Set the EDITOR environment variable.",
481
- code: "NO_EDITOR",
482
- });
483
- return;
484
- }
485
- await deps.runner.run(editor, [file], { cwd: deps.repoRoot });
486
- res.json({ success: true });
487
- }
488
- catch (err) {
489
- const message = err instanceof Error ? err.message : "Failed to open editor";
490
- res.status(500).json({ error: message, code: "EDITOR_ERROR" });
491
- }
492
- });
493
- // POST /api/import/preview — dry run import
494
- router.post("/import/preview", async (req, res) => {
495
- try {
496
- const manifest = loadManifest();
497
- const { target, content, format, overwriteKeys } = req.body;
498
- if (!target || typeof content !== "string") {
499
- res.status(400).json({
500
- error: "Request body must include 'target' and 'content'.",
501
- code: "BAD_REQUEST",
502
- });
503
- return;
504
- }
505
- const parts = target.split("/");
506
- if (parts.length !== 2 || !parts[0] || !parts[1]) {
507
- res.status(400).json({
508
- error: "Invalid target format. Use 'namespace/environment'.",
509
- code: "BAD_REQUEST",
510
- });
511
- return;
512
- }
513
- const importRunner = new core_1.ImportRunner(sops);
514
- const result = await importRunner.import(target, null, content, manifest, deps.repoRoot, {
515
- format,
516
- dryRun: true,
517
- });
518
- // Classify keys using overwriteKeys from the request
519
- const overwriteSet = new Set(overwriteKeys ?? []);
520
- const wouldImport = result.imported.filter((k) => !overwriteSet.has(k));
521
- const wouldOverwrite = result.imported.filter((k) => overwriteSet.has(k));
522
- const wouldSkip = result.skipped.map((k) => ({ key: k, reason: "already exists" }));
523
- res.json({
524
- wouldImport,
525
- wouldSkip,
526
- wouldOverwrite,
527
- warnings: result.warnings,
528
- totalKeys: result.imported.length + result.skipped.length,
529
- });
530
- }
531
- catch (err) {
532
- const message = err instanceof Error ? err.message : "Preview failed";
533
- res.status(500).json({ error: message, code: "IMPORT_PREVIEW_ERROR" });
534
- }
535
- });
536
- // POST /api/import/apply — run actual import
537
- router.post("/import/apply", async (req, res) => {
538
- try {
539
- const manifest = loadManifest();
540
- const { target, content, format, keys, overwriteKeys } = req.body;
541
- if (!target || typeof content !== "string") {
542
- res.status(400).json({
543
- error: "Request body must include 'target' and 'content'.",
544
- code: "BAD_REQUEST",
545
- });
546
- return;
547
- }
548
- if (!Array.isArray(keys)) {
549
- res
550
- .status(400)
551
- .json({ error: "Request body must include 'keys' array.", code: "BAD_REQUEST" });
552
- return;
553
- }
554
- const parts = target.split("/");
555
- if (parts.length !== 2 || !parts[0] || !parts[1]) {
556
- res.status(400).json({
557
- error: "Invalid target format. Use 'namespace/environment'.",
558
- code: "BAD_REQUEST",
559
- });
560
- return;
561
- }
562
- if (keys.length === 0) {
563
- res.json({ imported: [], skipped: [], failed: [] });
564
- return;
565
- }
566
- const importRunner = new core_1.ImportRunner(sops);
567
- const result = await importRunner.import(target, null, content, manifest, deps.repoRoot, {
568
- format,
569
- keys,
570
- overwrite: (overwriteKeys ?? []).length > 0,
571
- });
572
- res.json({
573
- imported: result.imported,
574
- skipped: result.skipped,
575
- failed: result.failed,
576
- });
577
- }
578
- catch (err) {
579
- const message = err instanceof Error ? err.message : "Import failed";
580
- res.status(500).json({ error: message, code: "IMPORT_APPLY_ERROR" });
581
- }
582
- });
583
- // GET /api/recipients
584
- router.get("/recipients", async (_req, res) => {
585
- try {
586
- const manifest = loadManifest();
587
- const recipients = await recipientManager.list(manifest, deps.repoRoot);
588
- const cells = matrix.resolveMatrix(manifest, deps.repoRoot);
589
- const totalFiles = cells.filter((c) => c.exists).length;
590
- res.json({ recipients, totalFiles });
591
- }
592
- catch (err) {
593
- const message = err instanceof Error ? err.message : "Failed to list recipients";
594
- res.status(500).json({ error: message, code: "RECIPIENTS_ERROR" });
595
- }
596
- });
597
- // GET /api/recipients/validate?key=age1...
598
- router.get("/recipients/validate", (req, res) => {
599
- const key = req.query.key;
600
- if (!key) {
601
- res.status(400).json({ valid: false, error: "Missing 'key' query parameter." });
602
- return;
603
- }
604
- const result = (0, core_1.validateAgePublicKey)(key);
605
- res.json(result);
606
- });
607
- // POST /api/recipients/add
608
- router.post("/recipients/add", async (req, res) => {
609
- try {
610
- const manifest = loadManifest();
611
- const { key, label } = req.body;
612
- const result = await recipientManager.add(key, label, manifest, deps.repoRoot);
613
- res.json(result);
614
- }
615
- catch (err) {
616
- const message = err instanceof Error ? err.message : "Failed to add recipient";
617
- res.status(500).json({ error: message, code: "RECIPIENTS_ADD_ERROR" });
618
- }
619
- });
620
- // POST /api/recipients/remove
621
- router.post("/recipients/remove", async (req, res) => {
622
- try {
623
- const manifest = loadManifest();
624
- const { key } = req.body;
625
- const result = await recipientManager.remove(key, manifest, deps.repoRoot);
626
- const cells = matrix.resolveMatrix(manifest, deps.repoRoot);
627
- const targets = cells.filter((c) => c.exists).map((c) => `${c.namespace}/${c.environment}`);
628
- res.json({ ...result, rotationReminder: targets });
629
- }
630
- catch (err) {
631
- const message = err instanceof Error ? err.message : "Failed to remove recipient";
632
- res.status(500).json({ error: message, code: "RECIPIENTS_REMOVE_ERROR" });
633
- }
634
- });
635
- function dispose() {
636
- lastScanResult = null;
637
- lastScanAt = null;
638
- }
639
- // Attach dispose to the router for cleanup
640
- router.dispose = dispose;
641
- return router;
642
- }
643
- //# sourceMappingURL=api.js.map