@rightkit/release 0.3.0 → 0.4.1

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 (83) hide show
  1. package/README.md +74 -10
  2. package/addon-command.mjs +3 -4
  3. package/adoption-evidence.mjs +650 -0
  4. package/android-play-upload.mjs +2 -1
  5. package/apple-profile-create.mjs +113 -10
  6. package/apple-profile-renew.mjs +11 -3
  7. package/artifact-storage.mjs +35 -2
  8. package/bin/darwin/rightkit-swap +0 -0
  9. package/binary-license-notices.mjs +161 -0
  10. package/build-release.mjs +138 -47
  11. package/cargo-license-notices.mjs +111 -0
  12. package/cli/right-release.mjs +21 -2
  13. package/codesign-environment.mjs +2 -2
  14. package/content-digest.mjs +209 -0
  15. package/development.mjs +17 -6
  16. package/direct-bootstrap.mjs +5 -6
  17. package/disk-command.mjs +1 -0
  18. package/disk-policy.json +6 -2
  19. package/disk-stable-root.mjs +13 -9
  20. package/github-updater-release.mjs +70 -7
  21. package/in-place-install.mjs +17 -14
  22. package/install-app-windows.mjs +24 -7
  23. package/install-app.mjs +21 -40
  24. package/ios-asc-credentials.mjs +163 -0
  25. package/ios-build-context.mjs +129 -0
  26. package/ios-build-upload.mjs +282 -0
  27. package/ios-credentials-command.mjs +70 -0
  28. package/ios-dev.mjs +248 -0
  29. package/ios-host.mjs +76 -0
  30. package/ios-lock.mjs +75 -0
  31. package/ios-package-recipe.mjs +707 -0
  32. package/ios-testflight.mjs +266 -0
  33. package/ios-upload.mjs +313 -0
  34. package/local-publish.mjs +222 -53
  35. package/managed-cargo.mjs +8 -0
  36. package/mirror-root-artifact.mjs +5 -1
  37. package/native-cargo-layout.mjs +47 -4
  38. package/ownership-command.mjs +2 -1
  39. package/ownership-dependency-allowlist.json +3 -1
  40. package/ownership-evidence.mjs +30 -18
  41. package/ownership-key-command.mjs +69 -2
  42. package/ownership-mandatory-set.json +117 -67
  43. package/ownership-mandatory-set.mjs +141 -21
  44. package/ownership-repositories.json +1 -1
  45. package/ownership-scope.mjs +94 -0
  46. package/ownership.mjs +49 -7
  47. package/package-recipe-targets.mjs +41 -1
  48. package/package.json +1 -1
  49. package/pipeline-admission.mjs +375 -0
  50. package/pipeline-config.mjs +100 -0
  51. package/pipeline-generators.json +4 -0
  52. package/pipeline-generators.mjs +137 -0
  53. package/pipeline-scan.mjs +285 -0
  54. package/pipeline-tools.mjs +171 -0
  55. package/preflight.mjs +14 -7
  56. package/production-mode.mjs +42 -0
  57. package/project-type.mjs +3 -1
  58. package/protected-credentials.mjs +28 -1
  59. package/publication-artifacts.mjs +30 -5
  60. package/publication-gate.mjs +94 -32
  61. package/publish-cargo.mjs +117 -25
  62. package/publish-swift.mjs +12 -3
  63. package/publish-update.mjs +15 -2
  64. package/qa-build-features.mjs +16 -13
  65. package/qa-contract.mjs +30 -2
  66. package/qa-control-scan.mjs +8 -0
  67. package/release-config.mjs +330 -0
  68. package/release-invocation.mjs +118 -8
  69. package/release-switch-registry.json +126 -27
  70. package/release-switch-scan.mjs +88 -21
  71. package/release.mjs +82 -20
  72. package/rightkit-ownership.json +1 -1
  73. package/rightkit-sync.mjs +8 -0
  74. package/rightkit-versions.json +428 -194
  75. package/runtime-budget-crates.mjs +14 -0
  76. package/runtime-budgets.mjs +248 -47
  77. package/sign-updater.mjs +56 -59
  78. package/source-gate.mjs +8 -1
  79. package/stage-sidecar.mjs +207 -40
  80. package/swap-helper-bundle.mjs +7 -3
  81. package/target-distribution.mjs +5 -0
  82. package/upload-release.mjs +7 -2
  83. package/bin/darwin/rightkit-swap.sign.json +0 -6
@@ -6,6 +6,7 @@
6
6
  // right-release ownership [--app <dir>] --audit-json (Legion governance.capability-ownership provider)
7
7
  // right-release ownership trust-compare [--json] (print this host's protected-trust digest; policy-bundle drift is a warning, never a block)
8
8
  // right-release ownership trust-sync (retired: prints why and exits 0)
9
+ // right-release ownership trust-refresh [--dry-run] [--app <dir>] (elevated, key-neutral: merge new repository registrations and floors from the installed release into protected trust)
9
10
  // right-release ownership --write-manifest [--rightkit <rightkit>] [--approve-new-capabilities]
10
11
  // --approve-new-capabilities permits new ids during local generation only.
11
12
  // Existing owner changes are always refused; installation needs protected authorization.
@@ -75,7 +76,7 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
75
76
  if (args[0] === "trust-compare") return trustCompare(args, { print, trustLoader });
76
77
  if (args.includes("--staged")) return stagedMain(args, { print });
77
78
  if (args[0] === "trust-sync") { print(`[right-release] ${TRUST_SYNC_RETIRED}`); return 0; }
78
- if (["enroll", "approve-key", "revoke-key"].includes(args[0])) {
79
+ if (["enroll", "approve-key", "revoke-key", "trust-refresh"].includes(args[0])) {
79
80
  const { main: keyMain } = await import("./ownership-key-command.mjs");
80
81
  return keyMain(args, { runner: gitRunner, print });
81
82
  }
@@ -795,12 +795,14 @@
795
795
  "cargo:serde_json",
796
796
  "cargo:tauri",
797
797
  "cargo:tauri-build",
798
+ "cargo:tauri-plugin-http",
798
799
  "cargo:tauri-plugin-os",
799
800
  "cargo:tauri-plugin-process",
800
801
  "cargo:tauri-plugin-updater",
801
802
  "npm:@tailwindcss/vite",
802
803
  "npm:@tauri-apps/api",
803
804
  "npm:@tauri-apps/cli",
805
+ "npm:@tauri-apps/plugin-http",
804
806
  "npm:@tauri-apps/plugin-os",
805
807
  "npm:@tauri-apps/plugin-process",
806
808
  "npm:@tauri-apps/plugin-updater",
@@ -1036,7 +1038,7 @@
1036
1038
  ]
1037
1039
  },
1038
1040
  {
1039
- "repositoryIdentity": "https://github.com/orthic-labs/pulse",
1041
+ "repositoryIdentity": "https://github.com/bogusyogi/pulse",
1040
1042
  "dependencies": [
1041
1043
  "cargo:chrono",
1042
1044
  "cargo:dirs",
@@ -23,6 +23,7 @@ function rootContains(root, candidate) {
23
23
 
24
24
  function createEvidence(repoRoot, {
25
25
  scanRoot = repoRoot,
26
+ scope: requestedScope = null,
26
27
  manifestPath = path.join(path.dirname(fileURLToPath(import.meta.url)), MANIFEST_FILE),
27
28
  scan = checkApp,
28
29
  trustLoader = loadTrustedExternal,
@@ -33,6 +34,7 @@ function createEvidence(repoRoot, {
33
34
  policyBundleProvider = runningPolicyBundle,
34
35
  } = {}, readAuthority = readCommittedKeyAuthority, scanCommitted = checkCommittedApp) {
35
36
  const root = path.resolve(repoRoot);
37
+ const scope = requestedScope ? [...new Set(requestedScope)].sort() : null;
36
38
  const manifestBytes = readFileSync(manifestPath);
37
39
  const manifest = JSON.parse(manifestBytes.toString("utf8"));
38
40
  let trustState = trustLoader();
@@ -114,21 +116,27 @@ function createEvidence(repoRoot, {
114
116
  }
115
117
  }
116
118
  const registration = registrationCheck?.registration ?? null;
117
- const requestedRootRegistered = registration?.roots.some((registered) => rootContains(registered, requestedRoot)) ?? false;
118
- if (registration && !requestedRootRegistered) {
119
- if (!enrollmentPending) throw new Error(`release app root is outside protected ownership roots: ${requestedRoot}`);
120
- evidenceDiagnostics.push(`requested scan root is outside protected ownership roots during pending enrollment: ${requestedRoot}`);
119
+ if (scope && !useCommittedSource) throw new Error("a scoped ownership scan requires the committed-source scan; this host's trust enrollment cannot provide it");
120
+ // A scoped scan covers its scope roots; the unscoped scan covers the requested root.
121
+ const coverageTargets = scope ?? [requestedRoot];
122
+ const unregisteredTargets = registration ? coverageTargets.filter((target) => !registration.roots.some((registered) => rootContains(registered, target))) : [];
123
+ const requestedRootRegistered = unregisteredTargets.length === 0;
124
+ for (const target of unregisteredTargets) {
125
+ if (!enrollmentPending) throw new Error(`release app root is outside protected ownership roots: ${target}`);
126
+ evidenceDiagnostics.push(`requested scan root is outside protected ownership roots during pending enrollment: ${target}`);
121
127
  }
122
128
 
123
129
  // The release app's manifest/config can be nested, but the external registry
124
130
  // defines repository coverage. Scan every protected root so a nested release
125
131
  // cannot omit sibling packages or engine source by narrowing scanRoot.
126
- const scanRoots = registration?.roots ? [...registration.roots] : [];
127
- if (!requestedRootRegistered && !scanRoots.includes(requestedRoot)) scanRoots.push(requestedRoot);
128
- const scans = (scanRoots.length ? scanRoots : [requestedRoot]).map((registered) => {
132
+ const overlapsScope = (registered) => !scope || scope.some((target) => rootContains(registered, target) || rootContains(target, registered));
133
+ const scanRoots = registration?.roots ? registration.roots.filter(overlapsScope) : [];
134
+ if (scope) { for (const target of unregisteredTargets) if (!scanRoots.includes(target)) scanRoots.push(target); }
135
+ else if (!requestedRootRegistered && !scanRoots.includes(requestedRoot)) scanRoots.push(requestedRoot);
136
+ const scans = (scanRoots.length ? scanRoots : [scope ? "." : requestedRoot]).map((registered) => {
129
137
  const absolute = registered === "." ? root : path.resolve(root, ...registered.split("/"));
130
138
  const scanned = useCommittedSource
131
- ? scanCommitted(absolute, manifest, { trust: trustState.trust, expectedCommit: commit, expectedTree: tree, source })
139
+ ? scanCommitted(absolute, manifest, { trust: trustState.trust, expectedCommit: commit, expectedTree: tree, source, ...(scope ? { scope } : {}) })
132
140
  : scan(absolute, manifest, { trust: { trust: trustState.trust, enrolled: trustState.enrolled, pending: enrollmentPending }, scannerVersion, catalogVersion, ...(enrollmentPending ? { enrollmentPendingAdvisory: true } : {}) });
133
141
  if (repositoryIdentity && canonicalRepositoryIdentity(scanned.metadata?.repositoryIdentity) !== repositoryIdentity) throw new Error(`ownership evidence: scanner observed a different repository identity under ${registered}`);
134
142
  if (useCommittedSource && scanned.metadata?.commit !== commit) throw new Error(`ownership evidence: scanner observed a different source commit under ${registered}`);
@@ -181,6 +189,7 @@ function createEvidence(repoRoot, {
181
189
  floorOk: floor.ok,
182
190
  visibility: registration?.visibility ?? null,
183
191
  registrationDigest: registration ? sha256(registration) : null,
192
+ ...(scope ? { ownershipScope: scope, ownershipScopeClosed: [...new Set(scans.flatMap(({ scanned }) => scanned.metadata?.ownershipScopeClosed ?? scope))].sort() } : {}),
184
193
  registeredRoots: registration ? [...registration.roots] : [],
185
194
  contentDigest: enrollmentPending ? null : contentDigest,
186
195
  checkedAt: new Date().toISOString(),
@@ -190,13 +199,13 @@ function createEvidence(repoRoot, {
190
199
  }
191
200
 
192
201
  /** Create scan evidence bound to committed source, catalog, and signed exceptions. */
193
- export function createOwnershipEvidence(repoRoot, scanRoot = repoRoot) {
194
- return createEvidence(repoRoot, { scanRoot, committed: true, requireCleanCheckout: true, allowPendingAdvisory: true });
202
+ export function createOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null) {
203
+ return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true, allowPendingAdvisory: true });
195
204
  }
196
205
 
197
206
  /** Alias spelling makes the immutable-source contract explicit at adapter boundaries. */
198
- export function createCommittedOwnershipEvidence(repoRoot, scanRoot = repoRoot) {
199
- return createEvidence(repoRoot, { scanRoot, committed: true, requireCleanCheckout: true });
207
+ export function createCommittedOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null) {
208
+ return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true });
200
209
  }
201
210
 
202
211
  // Injection seam is deliberately named for tests and is not used by release commands.
@@ -219,21 +228,24 @@ export function runProtectedLocalOwnershipCheck(repoRoot, scanRoot = repoRoot) {
219
228
  }
220
229
 
221
230
  /** Re-scan exact source and compare every sealed binding before release upload. */
222
- export function verifyOwnershipEvidence(expected, repoRoot, scanRoot = repoRoot) {
223
- return verifyEvidence(expected, repoRoot, scanRoot, () => createCommittedOwnershipEvidence(repoRoot, scanRoot));
231
+ export function verifyOwnershipEvidence(expected, repoRoot, scanRoot = repoRoot, scope = null) {
232
+ return verifyEvidence(expected, repoRoot, scanRoot, () => createCommittedOwnershipEvidence(repoRoot, scanRoot, scope), scope);
224
233
  }
225
234
 
226
235
  export function verifyOwnershipEvidenceForTest(expected, repoRoot, options) {
227
- return verifyEvidence(expected, repoRoot, options.scanRoot ?? repoRoot, () => createOwnershipEvidenceForTest(repoRoot, options));
236
+ return verifyEvidence(expected, repoRoot, options.scanRoot ?? repoRoot, () => createOwnershipEvidenceForTest(repoRoot, options), options.scope ?? null);
228
237
  }
229
238
 
230
- function verifyEvidence(expected, repoRoot, _scanRoot, createCurrent) {
239
+ function verifyEvidence(expected, repoRoot, _scanRoot, createCurrent, scope = null) {
231
240
  if (!expected || expected.schema !== 1) return { ok: false, reason: "sealed ownership evidence is missing or malformed" };
241
+ // The sealed scope must equal what the current config yields; the re-scan below uses that same scope.
242
+ const wanted = scope ? [...new Set(scope)].sort() : null;
243
+ if (JSON.stringify(expected.ownershipScope ?? null) !== JSON.stringify(wanted)) return { ok: false, reason: "ownership evidence ownershipScope changed after build" };
232
244
  let current;
233
245
  try { current = createCurrent(); }
234
246
  catch (error) { return { ok: false, reason: error.message }; }
235
- for (const key of ["repositoryIdentity", "source", "commit", "tree", "catalogDigest", "catalogVersion", "scannerVersion", "approvalsDigest", "keyLogDigest", "keyCheckpoint", "rootFingerprint", "keyAuthority", "visibility", "registrationDigest", "registeredRoots", "contentDigest"]) {
236
- const unchanged = key === "registeredRoots" || key === "keyCheckpoint" || key === "keyAuthority"
247
+ for (const key of ["repositoryIdentity", "source", "commit", "tree", "catalogDigest", "catalogVersion", "scannerVersion", "approvalsDigest", "keyLogDigest", "keyCheckpoint", "rootFingerprint", "keyAuthority", "visibility", "registrationDigest", "registeredRoots", "contentDigest", "ownershipScopeClosed"]) {
248
+ const unchanged = key === "registeredRoots" || key === "ownershipScopeClosed" || key === "keyCheckpoint" || key === "keyAuthority"
237
249
  ? JSON.stringify(expected[key]) === JSON.stringify(current.evidence[key])
238
250
  : expected[key] === current.evidence[key];
239
251
  if (!unchanged) return { ok: false, reason: `ownership evidence ${key} changed after build` };
@@ -8,6 +8,8 @@ import { createKeyEvent, keyFingerprint, verifyKeyLog, verifyKeyRequest } from "
8
8
  import { canonicalRepositoryIdentity, DEFAULT_TRUST_PATH, TRUST_SYNC_RETIRED, loadTrustedExternal, resolveKeyLogTrust, __testOnly as approvalTestOnly } from "./ownership-approval.mjs";
9
9
  import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
10
10
  import { buildMacInvocation, hasTerminal } from "./ownership-key-elevation.mjs";
11
+ import { mergeRepositoriesFromInstalledRelease, prepareTrustUpdate, REPOSITORY_REGISTRY_MALFORMED_CODE } from "./ownership-key-trust.mjs";
12
+ import { summarizeTrustChange } from "./ownership-trust-summary.mjs";
11
13
 
12
14
  const here = path.dirname(fileURLToPath(import.meta.url));
13
15
  const LOG = ".rightkit/ownership-keys.json";
@@ -355,14 +357,24 @@ export async function main(args, deps = {}) {
355
357
  if (args[0] === "trust-sync") { print(`[right-release] ${TRUST_SYNC_RETIRED}`); return 0; }
356
358
  try {
357
359
  if (!platform) throw new Error(`ownership key management unsupported platform: ${process.platform}`);
360
+ const refresh = args[0] === "trust-refresh";
358
361
  // Delegate BEFORE taking the command lock: the desktop-session child needs that same lock.
359
- const helper = deps.helper ?? helperFor(run);
362
+ // trust-refresh signs nothing, so it never touches the native key helper.
363
+ const helper = deps.helper ?? (refresh ? null : helperFor(run));
360
364
  const lockPath = deps.lockPath ?? path.join(os.tmpdir(), "rightkit-key-command.lock");
361
365
  let lockFd;
362
366
  lockFd = acquireCommandLock(lockPath);
363
367
  try {
364
368
  commandTemp = mkdtempSync(path.join(os.tmpdir(), "rightkit-key-command-"));
365
369
  const authority = resolveAuthority(root, run, deps.authority);
370
+ if (refresh) {
371
+ // Key-neutral: no key log event, no metadata commit, so no minimize hook. A dry run changes nothing at all,
372
+ // so it also skips the fetch and fast-forward that the real ceremony performs.
373
+ const dryRun = args.includes("--dry-run");
374
+ const transport = dryRun ? null : syncBeforeCeremony(root, run);
375
+ const headSnapshot = dryRun ? null : git(run, root, ["rev-parse", "HEAD"]);
376
+ return await trustRefresh(args, { ...deps, run, print, root, authority, transport, headSnapshot });
377
+ }
366
378
  const hook = detectMinimizeHook(root, run, deps);
367
379
  const transport = syncBeforeCeremony(root, run);
368
380
  const headSnapshot = git(run, root, ["rev-parse", "HEAD"]);
@@ -370,7 +382,7 @@ export async function main(args, deps = {}) {
370
382
  if (verb === "enroll") return await enroll(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
371
383
  if (verb === "approve-key") return await approve(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
372
384
  if (verb === "revoke-key") return await revoke(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
373
- throw new Error("expected enroll, approve-key, or revoke-key (trust-sync is retired)");
385
+ throw new Error("expected enroll, approve-key, revoke-key, or trust-refresh (trust-sync is retired)");
374
386
  } finally {
375
387
  closeSync(lockFd);
376
388
  rmSync(lockPath, { force: true });
@@ -571,6 +583,61 @@ async function revoke(args, d) {
571
583
  print(`Ownership key revoked: ${keyId}; active keys: ${verifyKeyLog(next, { rootFingerprint: pin }).keys.length}`);
572
584
  return 0;
573
585
  }
586
+ const repositoryNames = (list) => list.map((identity) => String(identity).replace(/^https:\/\/github\.com\//i, "")).join(", ");
587
+
588
+ /**
589
+ * Key-neutral elevated trust update. Builds the same request an approve-key checkpoint update sends (current committed
590
+ * key log, the host's pinned root fingerprint, the protected authority) so performProtectedTrustUpdate runs its
591
+ * additions-only registration merge and floor raise from the INSTALLED release. It adds no key event, writes nothing
592
+ * under .rightkit/ and creates no commit. Elevation, the pinned request digest and the installed-sibling-script rule
593
+ * are the unchanged elevate() path.
594
+ */
595
+ async function trustRefresh(args, d) {
596
+ const { print, root } = d;
597
+ const dryRun = args.includes("--dry-run");
598
+ const rest = args.slice(1);
599
+ const unexpected = rest.filter((arg, i) => !(arg === "--dry-run" || arg === "--app" || rest[i - 1] === "--app"));
600
+ if (unexpected.length) throw new Error(`trust-refresh accepts only --dry-run and --app <dir>; unexpected: ${unexpected.join(" ")}`);
601
+ assertSafeMetadataParents(root, LOG);
602
+ const hostTrust = (d.trustLoader ?? trustState)();
603
+ const existing = hostTrust?.trust;
604
+ if (!existing?.rootFingerprint) throw new Error("this host is not enrolled: protected ownership trust has no pinned root. trust-refresh never creates trust; run `right-release ownership enroll` first");
605
+ const log = readLog(root);
606
+ if (!log.events.length) throw new Error("no root key log exists in this checkout");
607
+ // Verifies the local log against the pinned root, the protected authority, and (unless injected) the committed HEAD blob.
608
+ currentLogTrust(root, log, hostTrust, d.authority, Boolean(d.resolveTrust));
609
+ const pin = existing.rootFingerprint;
610
+ const requestBytes = Buffer.from(json({ schema: 1, log, rootFingerprint: pin, baseline: null, authority: d.authority }));
611
+ const registry = d.installedRegistry ? { registry: d.installedRegistry } : {};
612
+ const merge = mergeRepositoriesFromInstalledRelease(prepareTrustUpdate(existing, JSON.parse(requestBytes.toString("utf8"))), registry);
613
+ const unusable = merge.warnings.find((item) => item.code === REPOSITORY_REGISTRY_MALFORMED_CODE);
614
+ if (unusable) throw new Error(`the installed release's repository registry is missing or malformed (${unusable.message}); refusing to refresh trust from it`);
615
+ const summary = summarizeTrustChange({ existing, requestBytes, ...registry });
616
+ const before = new Set((existing.repositories ?? []).map((entry) => entry.repositoryIdentity));
617
+ const added = merge.trust.repositories.map((entry) => entry.repositoryIdentity).filter((identity) => !before.has(identity));
618
+ print(added.length
619
+ ? `trust-refresh would add ${added.length} repositor${added.length === 1 ? "y" : "ies"} to protected trust: ${repositoryNames(added)}`
620
+ : "trust-refresh would add no repository registrations");
621
+ for (const line of summary.lines) print(` ${line}`);
622
+ if (!summary.changed) { print("Protected ownership trust is already current; nothing to elevate."); return 0; }
623
+ if (dryRun) { print("Dry run: nothing was elevated or written."); return 0; }
624
+ if (git(d.run, root, ["rev-parse", "HEAD"]) !== d.headSnapshot) throw new Error("repository HEAD changed during ownership ceremony");
625
+ const request = makeTrustRequest(log, pin, null, d.authority);
626
+ try {
627
+ if (request.digest !== summary.payloadDigest) throw new Error("trust request bytes differ from the summarized request; refusing to elevate");
628
+ try { await (d.elevate ?? elevate)(d.run, request.file, request.digest); }
629
+ catch (error) {
630
+ if (platform === "macos" && !hasTerminal()) error.message += ". macOS elevation needs a human: run `right-release ownership trust-refresh` at a terminal for the sudo prompt, or approve the administrator dialog on the desktop; an unattended agent session without a tty cannot pass it";
631
+ throw error;
632
+ }
633
+ } finally { await import("node:fs").then(({ unlinkSync }) => unlinkSync(request.file)).catch(() => {}); }
634
+ postVerify(root, log, d);
635
+ const present = ((d.trustLoader ?? trustState)().trust?.repositories ?? []).map((entry) => entry.repositoryIdentity);
636
+ const missing = added.filter((identity) => !present.includes(identity));
637
+ if (missing.length) throw new Error(`elevated update finished but protected trust still lacks: ${repositoryNames(missing)}`);
638
+ print(`Protected trust now registers ${present.length} repositories: ${repositoryNames(present)}`);
639
+ return 0;
640
+ }
574
641
  async function signWithHelper(run, helper, log, operation, subject, signer) {
575
642
  const draft = createKeyEvent(log, { operation, subject, signerKeyId: signer.keyId });
576
643
  const input = temp("draft"), output = temp("draft-signed");