@clef-sh/ui 0.1.28 → 0.1.29

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.
@@ -9,8 +9,8 @@
9
9
  <link rel="shortcut icon" href="/favicon.ico" />
10
10
  <!-- Fonts are bundled via @clef-sh/design/fonts.css (imported from
11
11
  styles.css) — no runtime dependency on Google Fonts. -->
12
- <script type="module" crossorigin src="/assets/index-DXZTFIp7.js"></script>
13
- <link rel="stylesheet" crossorigin href="/assets/index-DbfsjTUx.css">
12
+ <script type="module" crossorigin src="/assets/index-CGTwPvL1.js"></script>
13
+ <link rel="stylesheet" crossorigin href="/assets/index-BmfzUjLY.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/server/api.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AASpD,OAAO,EASL,gBAAgB,EA6BjB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,gBAAgB,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAgwDrD"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/server/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AAGpD,OAAO,EASL,gBAAgB,EA4BjB,MAAM,eAAe,CAAC;AAIvB,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,gBAAgB,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CA0wDrD"}
@@ -36,97 +36,35 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createApiRouter = createApiRouter;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- const os = __importStar(require("os"));
40
- const child_process_1 = require("child_process");
41
39
  const express_1 = require("express");
42
40
  const express_rate_limit_1 = __importStar(require("express-rate-limit"));
43
41
  const YAML = __importStar(require("yaml"));
44
- // On Linux, libuv creates socketpairs for child stdio. Go's os.Open on
45
- // /dev/stdin re-opens /proc/self/fd/0 which fails with ENXIO on socketpairs.
46
- // Use a FIFO workaround on Linux, but not inside Jest (where the runner is
47
- // mocked and real subprocesses are never spawned).
48
- const _useStdinFifo = process.platform === "linux" && !process.env.JEST_WORKER_ID;
49
42
  const core_1 = require("@clef-sh/core");
50
43
  const envelope_1 = require("./envelope");
51
44
  function createApiRouter(deps) {
52
45
  const router = (0, express_1.Router)();
53
46
  const parser = new core_1.ManifestParser();
54
47
  const matrix = new core_1.MatrixManager();
55
- // Wrap the runner so sops subprocesses always run from the repo root
56
- // and work around /dev/stdin failures on Linux.
57
- //
58
- // Problem: SopsClient.encrypt passes /dev/stdin as the input file.
59
- // On Linux /dev/stdin → /proc/self/fd/0 which fails with ENXIO when
60
- // the Node SEA binary was spawned with stdin detached.
61
- //
62
- // Fix: when we see /dev/stdin in the args AND stdin content in opts,
63
- // replace it with a FIFO (named pipe). A FIFO is an in-memory kernel
64
- // buffer — plaintext never touches disk. The FIFO is cleaned up after
65
- // the subprocess exits.
66
- const sopsRunner = {
67
- run: (cmd, args, opts) => {
68
- const stdinIdx = args.indexOf("/dev/stdin");
69
- // Only use the FIFO workaround in Linux SEA binaries where
70
- // /dev/stdin → /proc/self/fd/0 fails with ENXIO on socketpairs.
71
- // Normal Node.js processes (including Jest on Linux CI) work fine.
72
- const needsFifo = stdinIdx >= 0 && opts?.stdin !== undefined && _useStdinFifo;
73
- if (!needsFifo) {
74
- return deps.runner.run(cmd, args, {
75
- ...opts,
76
- cwd: opts?.cwd ?? deps.repoRoot,
77
- env: opts?.env,
78
- });
79
- }
80
- // Create a FIFO and feed stdin content through a background process
81
- const fifoDir = (0, child_process_1.execFileSync)("mktemp", ["-d", path.join(os.tmpdir(), "clef-fifo-XXXXXX")])
82
- .toString()
83
- .trim();
84
- const fifoPath = path.join(fifoDir, "input");
85
- (0, child_process_1.execFileSync)("mkfifo", [fifoPath]);
86
- // Background writer — blocks at OS level until sops opens the read end
87
- const writer = (0, child_process_1.spawn)("dd", [`of=${fifoPath}`, "status=none"], {
88
- stdio: ["pipe", "ignore", "ignore"],
89
- });
90
- writer.stdin.write(opts.stdin);
91
- writer.stdin.end();
92
- const patchedArgs = [...args];
93
- patchedArgs[stdinIdx] = fifoPath;
94
- const { stdin: _stdin, ...restOpts } = opts;
95
- return deps.runner
96
- .run(cmd, patchedArgs, {
97
- ...restOpts,
98
- cwd: restOpts?.cwd ?? deps.repoRoot,
99
- env: { SOPS_CONFIG: path.join(deps.repoRoot, ".sops.yaml"), ...restOpts?.env },
100
- })
101
- .finally(() => {
102
- try {
103
- writer.kill();
104
- }
105
- catch {
106
- /* already exited */
107
- }
108
- try {
109
- (0, child_process_1.execFileSync)("rm", ["-rf", fifoDir]);
110
- }
111
- catch {
112
- /* best effort */
113
- }
114
- });
115
- },
48
+ // Pin sops invocations to the repo root and apply the Linux FIFO
49
+ // workaround for /dev/stdin (see wrapWithLinuxStdinFifo).
50
+ const cwdRunner = {
51
+ run: (cmd, args, opts) => deps.runner.run(cmd, args, {
52
+ ...opts,
53
+ cwd: opts?.cwd ?? deps.repoRoot,
54
+ }),
116
55
  };
56
+ const sopsRunner = (0, core_1.wrapWithLinuxStdinFifo)(cwdRunner);
117
57
  const sops = new core_1.SopsClient(sopsRunner, deps.ageKeyFile, deps.ageKey, deps.sopsPath);
118
58
  const diffEngine = new core_1.DiffEngine();
119
59
  const schemaValidator = new core_1.SchemaValidator();
120
- const lintRunner = new core_1.LintRunner(matrix, schemaValidator, sops);
121
60
  const git = new core_1.GitIntegration(deps.runner);
122
61
  const tx = new core_1.TransactionManager(git);
123
62
  const scanRunner = new core_1.ScanRunner(deps.runner);
124
- const recipientManager = new core_1.RecipientManager(sops, matrix, tx);
125
- const serviceIdManager = new core_1.ServiceIdentityManager(sops, matrix, tx);
126
- const backendMigrator = new core_1.BackendMigrator(sops, matrix, tx);
127
- const resetManager = new core_1.ResetManager(matrix, sops, schemaValidator, tx);
128
- const syncManager = new core_1.SyncManager(matrix, sops, tx);
129
- const structureManager = new core_1.StructureManager(matrix, sops, tx);
63
+ // Manifest-bound managers are constructed per-request so the manifest
64
+ // edits the user makes through other UI flows are picked up without
65
+ // restarting the server.
66
+ const buildSourceFor = (manifest) => (0, core_1.composeSecretSource)(new core_1.FilesystemStorageBackend(manifest, deps.repoRoot), sops, manifest);
67
+ const structureManager = new core_1.StructureManager(matrix, buildSourceFor, tx);
130
68
  // In-session scan cache
131
69
  let lastScanResult = null;
132
70
  let lastScanAt = null;
@@ -259,6 +197,19 @@ function createApiRouter(deps) {
259
197
  store: apiRateLimitStore,
260
198
  });
261
199
  router.use(apiLimiter);
200
+ // GET /api/capabilities — boolean trait descriptor for the composed
201
+ // source. Lets the UI client gate buttons (e.g. hide "Migrate backend"
202
+ // when the source can't migrate) without per-feature probing.
203
+ router.get("/capabilities", (_req, res) => {
204
+ try {
205
+ const manifest = loadManifest();
206
+ res.json((0, core_1.describeCapabilities)(buildSourceFor(manifest)));
207
+ }
208
+ catch (err) {
209
+ const message = err instanceof Error ? err.message : "Failed to read capabilities";
210
+ res.status(500).json({ error: message, code: "CAPABILITIES_ERROR" });
211
+ }
212
+ });
262
213
  // GET /api/manifest
263
214
  router.get("/manifest", (_req, res) => {
264
215
  try {
@@ -274,7 +225,7 @@ function createApiRouter(deps) {
274
225
  router.get("/matrix", async (_req, res) => {
275
226
  try {
276
227
  const manifest = loadManifest();
277
- const statuses = await matrix.getMatrixStatus(manifest, deps.repoRoot, sops);
228
+ const statuses = await matrix.getMatrixStatus(manifest, deps.repoRoot);
278
229
  res.json(statuses);
279
230
  }
280
231
  catch (err) {
@@ -300,12 +251,14 @@ function createApiRouter(deps) {
300
251
  });
301
252
  return;
302
253
  }
303
- const filePath = `${deps.repoRoot}/${manifest.file_pattern.replace("{namespace}", ns).replace("{environment}", env)}`;
304
- const decrypted = await sops.decrypt(filePath);
254
+ const source = buildSourceFor(manifest);
255
+ const ref = { namespace: ns, environment: env };
256
+ const decrypted = await source.readCell(ref);
305
257
  // Read pending keys from metadata (plaintext sidecar)
306
258
  let pending = [];
307
259
  try {
308
- pending = await (0, core_1.getPendingKeys)(filePath);
260
+ const meta = await source.getPendingMetadata(ref);
261
+ pending = meta.pending.map((p) => p.key);
309
262
  }
310
263
  catch {
311
264
  // Metadata unreadable — no pending info
@@ -358,8 +311,9 @@ function createApiRouter(deps) {
358
311
  const relCellPath = manifest.file_pattern
359
312
  .replace("{namespace}", ns)
360
313
  .replace("{environment}", env);
361
- const filePath = `${deps.repoRoot}/${relCellPath}`;
362
- const decrypted = await sops.decrypt(filePath);
314
+ const source = buildSourceFor(manifest);
315
+ const ref = { namespace: ns, environment: env };
316
+ const decrypted = await source.readCell(ref);
363
317
  // Inside the mutate callback we do the actual encrypt + metadata
364
318
  // update. When auto-commit is enabled this runs inside tx.run; when
365
319
  // disabled (batched edit flow) it runs directly.
@@ -367,16 +321,16 @@ function createApiRouter(deps) {
367
321
  const doWork = async () => {
368
322
  if (random) {
369
323
  decrypted.values[key] = (0, core_1.generateRandomValue)();
370
- await sops.encrypt(filePath, decrypted.values, manifest, env);
324
+ await source.writeCell(ref, decrypted.values);
371
325
  // Metadata update failure used to trigger an in-method rollback
372
326
  // here. Inside tx.run, that rollback comes for free via git
373
327
  // reset, so we just let the throw propagate.
374
- await (0, core_1.markPendingWithRetry)(filePath, [key], "clef ui");
328
+ await source.markPending(ref, [key], "clef ui");
375
329
  response = { success: true, key, pending: true };
376
330
  }
377
331
  else {
378
332
  decrypted.values[key] = String(value);
379
- await sops.encrypt(filePath, decrypted.values, manifest, env);
333
+ await source.writeCell(ref, decrypted.values);
380
334
  // Validate against schema if defined
381
335
  const nsDef = manifest.namespaces.find((n) => n.name === ns);
382
336
  if (nsDef?.schema) {
@@ -396,11 +350,11 @@ function createApiRouter(deps) {
396
350
  // Schema load failed — skip validation, not fatal
397
351
  }
398
352
  }
399
- // Real value set is a rotation event. recordRotation also
353
+ // Real value set is a rotation event. recordRotation also
400
354
  // strips any matching pending entry, so this one call replaces
401
355
  // the old markResolved + implicit pending cleanup.
402
356
  try {
403
- await (0, core_1.recordRotation)(filePath, [key], "clef ui");
357
+ await source.recordRotation(ref, [key], "clef ui");
404
358
  }
405
359
  catch {
406
360
  // Metadata update failed — non-fatal
@@ -452,8 +406,9 @@ function createApiRouter(deps) {
452
406
  const relCellPath = manifest.file_pattern
453
407
  .replace("{namespace}", ns)
454
408
  .replace("{environment}", env);
455
- const filePath = `${deps.repoRoot}/${relCellPath}`;
456
- const decrypted = await sops.decrypt(filePath);
409
+ const source = buildSourceFor(manifest);
410
+ const ref = { namespace: ns, environment: env };
411
+ const decrypted = await source.readCell(ref);
457
412
  if (!(key in decrypted.values)) {
458
413
  res.status(404).json({
459
414
  error: `Key '${key}' not found in ${ns}/${env}.`,
@@ -463,12 +418,12 @@ function createApiRouter(deps) {
463
418
  }
464
419
  const doWork = async () => {
465
420
  delete decrypted.values[key];
466
- await sops.encrypt(filePath, decrypted.values, manifest, env);
421
+ await source.writeCell(ref, decrypted.values);
467
422
  // Strip both pending and rotation records — the key no longer
468
423
  // exists, so stale metadata would mislead policy.
469
424
  try {
470
- await (0, core_1.markResolved)(filePath, [key]);
471
- await (0, core_1.removeRotation)(filePath, [key]);
425
+ await source.markResolved(ref, [key]);
426
+ await source.removeRotation(ref, [key]);
472
427
  }
473
428
  catch {
474
429
  // Best effort — orphaned metadata is annoying but not dangerous
@@ -509,8 +464,9 @@ function createApiRouter(deps) {
509
464
  const relCellPath = manifest.file_pattern
510
465
  .replace("{namespace}", ns)
511
466
  .replace("{environment}", env);
512
- const filePath = `${deps.repoRoot}/${relCellPath}`;
513
- const decrypted = await sops.decrypt(filePath);
467
+ const source = buildSourceFor(manifest);
468
+ const ref = { namespace: ns, environment: env };
469
+ const decrypted = await source.readCell(ref);
514
470
  const value = key in decrypted.values ? String(decrypted.values[key]) : undefined;
515
471
  await tx.run(deps.repoRoot, {
516
472
  description: `clef ui: accept ${ns}/${env}/${key}`,
@@ -521,7 +477,7 @@ function createApiRouter(deps) {
521
477
  // rotation — it establishes a point-in-time "this is the
522
478
  // current secret" assertion. recordRotation also strips the
523
479
  // pending entry.
524
- await (0, core_1.recordRotation)(filePath, [key], "clef ui (accept)");
480
+ await source.recordRotation(ref, [key], "clef ui (accept)");
525
481
  },
526
482
  });
527
483
  res.json({ success: true, key, value });
@@ -561,8 +517,11 @@ function createApiRouter(deps) {
561
517
  });
562
518
  return;
563
519
  }
564
- const source = await sops.decrypt(fromCell.filePath);
565
- if (!(key in source.values)) {
520
+ const cellSource = buildSourceFor(manifest);
521
+ const fromRef = { namespace: fromNs, environment: fromEnv };
522
+ const toRef = { namespace: toNs, environment: toEnv };
523
+ const sourceCell = await cellSource.readCell(fromRef);
524
+ if (!(key in sourceCell.values)) {
566
525
  res.status(404).json({
567
526
  error: `Key '${key}' not found in ${fromNs}/${fromEnv}.`,
568
527
  code: "KEY_NOT_FOUND",
@@ -574,19 +533,19 @@ function createApiRouter(deps) {
574
533
  description: `clef ui: copy ${key} from ${fromNs}/${fromEnv} to ${toNs}/${toEnv}`,
575
534
  paths: [relToPath, relToPath.replace(/\.enc\.(yaml|json)$/, ".clef-meta.yaml")],
576
535
  mutate: async () => {
577
- const dest = await sops.decrypt(toCell.filePath);
578
- const valueChanged = dest.values[key] !== source.values[key];
579
- dest.values[key] = source.values[key];
580
- await sops.encrypt(toCell.filePath, dest.values, manifest, toCell.environment);
536
+ const dest = await cellSource.readCell(toRef);
537
+ const valueChanged = dest.values[key] !== sourceCell.values[key];
538
+ dest.values[key] = sourceCell.values[key];
539
+ await cellSource.writeCell(toRef, dest.values);
581
540
  // Only a real value change is a rotation. Copying an identical
582
541
  // value re-encrypts the ciphertext but does not rotate — matches
583
542
  // the import semantics agreed in the design.
584
543
  try {
585
544
  if (valueChanged) {
586
- await (0, core_1.recordRotation)(toCell.filePath, [key], "clef ui (copy)");
545
+ await cellSource.recordRotation(toRef, [key], "clef ui (copy)");
587
546
  }
588
547
  else {
589
- await (0, core_1.markResolved)(toCell.filePath, [key]);
548
+ await cellSource.markResolved(toRef, [key]);
590
549
  }
591
550
  }
592
551
  catch {
@@ -617,7 +576,13 @@ function createApiRouter(deps) {
617
576
  });
618
577
  return;
619
578
  }
620
- const result = await diffEngine.diffFiles(ns, envA, envB, manifest, sops, deps.repoRoot);
579
+ // Compose a per-request SecretSource. The manifest is parsed
580
+ // per-request (it can change while the server runs), so the
581
+ // source is built here rather than at router construction.
582
+ const storage = new core_1.FilesystemStorageBackend(manifest, deps.repoRoot);
583
+ const encryption = sops;
584
+ const source = (0, core_1.composeSecretSource)(storage, encryption, manifest);
585
+ const result = await diffEngine.diffCells(ns, envA, envB, source);
621
586
  // Mask values by default — only reveal when client explicitly requests it
622
587
  if (req.query.showValues !== "true") {
623
588
  for (const row of result.rows) {
@@ -646,6 +611,8 @@ function createApiRouter(deps) {
646
611
  });
647
612
  return;
648
613
  }
614
+ const lintSource = (0, core_1.composeSecretSource)(new core_1.FilesystemStorageBackend(manifest, deps.repoRoot), sops, manifest);
615
+ const lintRunner = new core_1.LintRunner(matrix, schemaValidator, lintSource);
649
616
  const result = await lintRunner.run(manifest, deps.repoRoot);
650
617
  const filtered = result.issues.filter((issue) => {
651
618
  const issueNs = issue.file.split("/")[0];
@@ -782,6 +749,8 @@ function createApiRouter(deps) {
782
749
  router.get("/lint", async (_req, res) => {
783
750
  try {
784
751
  const manifest = loadManifest();
752
+ const lintSource = (0, core_1.composeSecretSource)(new core_1.FilesystemStorageBackend(manifest, deps.repoRoot), sops, manifest);
753
+ const lintRunner = new core_1.LintRunner(matrix, schemaValidator, lintSource);
785
754
  const result = await lintRunner.run(manifest, deps.repoRoot);
786
755
  res.json(result);
787
756
  }
@@ -794,6 +763,8 @@ function createApiRouter(deps) {
794
763
  router.post("/lint/fix", async (_req, res) => {
795
764
  try {
796
765
  const manifest = loadManifest();
766
+ const lintSource = (0, core_1.composeSecretSource)(new core_1.FilesystemStorageBackend(manifest, deps.repoRoot), sops, manifest);
767
+ const lintRunner = new core_1.LintRunner(matrix, schemaValidator, lintSource);
797
768
  const result = await lintRunner.fix(manifest, deps.repoRoot);
798
769
  res.json(result);
799
770
  }
@@ -1014,7 +985,8 @@ function createApiRouter(deps) {
1014
985
  });
1015
986
  return;
1016
987
  }
1017
- const importRunner = new core_1.ImportRunner(sops, tx);
988
+ const importSource = (0, core_1.composeSecretSource)(new core_1.FilesystemStorageBackend(manifest, deps.repoRoot), sops, manifest);
989
+ const importRunner = new core_1.ImportRunner(importSource, tx);
1018
990
  const result = await importRunner.import(target, null, content, manifest, deps.repoRoot, {
1019
991
  format,
1020
992
  dryRun: true,
@@ -1067,7 +1039,8 @@ function createApiRouter(deps) {
1067
1039
  res.json({ imported: [], skipped: [], failed: [] });
1068
1040
  return;
1069
1041
  }
1070
- const importRunner = new core_1.ImportRunner(sops, tx);
1042
+ const importSource = (0, core_1.composeSecretSource)(new core_1.FilesystemStorageBackend(manifest, deps.repoRoot), sops, manifest);
1043
+ const importRunner = new core_1.ImportRunner(importSource, tx);
1071
1044
  const result = await importRunner.import(target, null, content, manifest, deps.repoRoot, {
1072
1045
  format,
1073
1046
  keys,
@@ -1088,6 +1061,7 @@ function createApiRouter(deps) {
1088
1061
  router.get("/recipients", async (_req, res) => {
1089
1062
  try {
1090
1063
  const manifest = loadManifest();
1064
+ const recipientManager = new core_1.RecipientManager(buildSourceFor(manifest), matrix, tx);
1091
1065
  const recipients = await recipientManager.list(manifest, deps.repoRoot);
1092
1066
  const cells = matrix.resolveMatrix(manifest, deps.repoRoot);
1093
1067
  const totalFiles = cells.filter((c) => c.exists).length;
@@ -1113,6 +1087,7 @@ function createApiRouter(deps) {
1113
1087
  try {
1114
1088
  const manifest = loadManifest();
1115
1089
  const { key, label } = req.body;
1090
+ const recipientManager = new core_1.RecipientManager(buildSourceFor(manifest), matrix, tx);
1116
1091
  const result = await recipientManager.add(key, label, manifest, deps.repoRoot);
1117
1092
  res.json(result);
1118
1093
  }
@@ -1126,6 +1101,7 @@ function createApiRouter(deps) {
1126
1101
  try {
1127
1102
  const manifest = loadManifest();
1128
1103
  const { key } = req.body;
1104
+ const recipientManager = new core_1.RecipientManager(buildSourceFor(manifest), matrix, tx);
1129
1105
  const result = await recipientManager.remove(key, manifest, deps.repoRoot);
1130
1106
  const cells = matrix.resolveMatrix(manifest, deps.repoRoot);
1131
1107
  const targets = cells.filter((c) => c.exists).map((c) => `${c.namespace}/${c.environment}`);
@@ -1209,6 +1185,7 @@ function createApiRouter(deps) {
1209
1185
  };
1210
1186
  }
1211
1187
  }
1188
+ const serviceIdManager = new core_1.ServiceIdentityManager(buildSourceFor(manifest), matrix, tx);
1212
1189
  const result = await serviceIdManager.create(name, namespaces, description ?? "", manifest, deps.repoRoot, {
1213
1190
  kmsEnvConfigs: typedKmsConfigs,
1214
1191
  sharedRecipient: sharedRecipient === true,
@@ -1238,6 +1215,7 @@ function createApiRouter(deps) {
1238
1215
  res.status(404).json({ error: `Service identity '${name}' not found.`, code: "NOT_FOUND" });
1239
1216
  return;
1240
1217
  }
1218
+ const serviceIdManager = new core_1.ServiceIdentityManager(buildSourceFor(manifest), matrix, tx);
1241
1219
  await serviceIdManager.delete(name, manifest, deps.repoRoot);
1242
1220
  res.json({ ok: true });
1243
1221
  }
@@ -1269,6 +1247,7 @@ function createApiRouter(deps) {
1269
1247
  }
1270
1248
  typedKmsConfigs[envName] = { provider: cfg.provider, keyId: cfg.keyId };
1271
1249
  }
1250
+ const serviceIdManager = new core_1.ServiceIdentityManager(buildSourceFor(manifest), matrix, tx);
1272
1251
  await serviceIdManager.updateEnvironments(name, typedKmsConfigs, manifest, deps.repoRoot);
1273
1252
  res.json({ ok: true });
1274
1253
  }
@@ -1283,6 +1262,7 @@ function createApiRouter(deps) {
1283
1262
  const name = req.params.name;
1284
1263
  const { environment } = req.body;
1285
1264
  const manifest = loadManifest();
1265
+ const serviceIdManager = new core_1.ServiceIdentityManager(buildSourceFor(manifest), matrix, tx);
1286
1266
  const privateKeys = await serviceIdManager.rotateKey(name, manifest, deps.repoRoot, environment);
1287
1267
  setNoCacheHeaders(res);
1288
1268
  res.json({ privateKeys });
@@ -1471,6 +1451,7 @@ function createApiRouter(deps) {
1471
1451
  return;
1472
1452
  }
1473
1453
  const events = [];
1454
+ const backendMigrator = new core_1.BackendMigrator(buildSourceFor, matrix, tx);
1474
1455
  const result = await backendMigrator.migrate(manifest, deps.repoRoot, { target, environment, dryRun: true }, (event) => events.push(event));
1475
1456
  res.json({ success: !result.rolledBack, result, events });
1476
1457
  }
@@ -1501,6 +1482,7 @@ function createApiRouter(deps) {
1501
1482
  return;
1502
1483
  }
1503
1484
  const events = [];
1485
+ const backendMigrator = new core_1.BackendMigrator(buildSourceFor, matrix, tx);
1504
1486
  const result = await backendMigrator.migrate(manifest, deps.repoRoot, { target, environment, dryRun: false }, (event) => events.push(event));
1505
1487
  res.json({ success: !result.rolledBack, result, events });
1506
1488
  }
@@ -1539,6 +1521,7 @@ function createApiRouter(deps) {
1539
1521
  res.status(404).json({ error: message, code: "NOT_FOUND" });
1540
1522
  return;
1541
1523
  }
1524
+ const resetManager = new core_1.ResetManager(matrix, buildSourceFor, schemaValidator, tx);
1542
1525
  const result = await resetManager.reset({ scope, backend, key, keys }, manifest, deps.repoRoot);
1543
1526
  res.json({ success: true, result });
1544
1527
  }
@@ -1570,6 +1553,7 @@ function createApiRouter(deps) {
1570
1553
  return;
1571
1554
  }
1572
1555
  }
1556
+ const syncManager = new core_1.SyncManager(matrix, buildSourceFor(manifest), tx);
1573
1557
  const plan = await syncManager.plan(manifest, deps.repoRoot, { namespace });
1574
1558
  res.json(plan);
1575
1559
  }
@@ -1593,6 +1577,7 @@ function createApiRouter(deps) {
1593
1577
  return;
1594
1578
  }
1595
1579
  }
1580
+ const syncManager = new core_1.SyncManager(matrix, buildSourceFor(manifest), tx);
1596
1581
  const result = await syncManager.sync(manifest, deps.repoRoot, { namespace });
1597
1582
  res.json({ success: true, result });
1598
1583
  }