@rightkit/release 0.2.135 → 0.2.137

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/README.md +62 -0
  2. package/addon-command.mjs +53 -34
  3. package/apple-profile-renew.mjs +30 -12
  4. package/artifact-storage.mjs +47 -10
  5. package/background-process.mjs +18 -0
  6. package/build-release.mjs +1 -1
  7. package/cache-command.mjs +4 -3
  8. package/cargo-contract.mjs +1 -1
  9. package/cargo-guard.mjs +4 -3
  10. package/cargo-registry-readback.mjs +51 -0
  11. package/cargo-target.mjs +3 -1
  12. package/cli/right-release.mjs +26 -0
  13. package/codesign-environment.mjs +26 -2
  14. package/codesign-shim.mjs +2 -1
  15. package/committed-source.mjs +3 -3
  16. package/development.mjs +7 -7
  17. package/direct-bootstrap.mjs +19 -9
  18. package/dmg-build.mjs +117 -0
  19. package/dmg-layout.mjs +123 -0
  20. package/github-release.mjs +74 -37
  21. package/heavy-command.mjs +3 -2
  22. package/install-app.mjs +358 -0
  23. package/inventory-check.mjs +980 -0
  24. package/macos-signing-session.mjs +3 -0
  25. package/managed-cargo.mjs +23 -19
  26. package/managed-host-secrets.mjs +30 -0
  27. package/managed-roots.mjs +39 -0
  28. package/model-promote.mjs +31 -37
  29. package/notary-auth.mjs +4 -0
  30. package/osascript-guard.mjs +24 -0
  31. package/ownership-approval-command.mjs +15 -4
  32. package/ownership-approval.mjs +114 -49
  33. package/ownership-availability.mjs +25 -0
  34. package/ownership-catalog-config.json +18 -0
  35. package/ownership-catalog.mjs +94 -0
  36. package/ownership-command.mjs +20 -10
  37. package/ownership-declaration.json +15 -0
  38. package/ownership-default-policy.json +1 -11
  39. package/ownership-evidence.mjs +10 -8
  40. package/ownership-git-inventory.mjs +29 -1
  41. package/ownership-helper-paths.json +2 -2
  42. package/ownership-hooks.mjs +107 -0
  43. package/ownership-internal-owners.json +44 -0
  44. package/ownership-key-authority.mjs +112 -7
  45. package/ownership-key-command.mjs +76 -19
  46. package/ownership-key-trust.mjs +97 -9
  47. package/ownership-lockfiles.mjs +311 -0
  48. package/ownership-report-command.mjs +12 -3
  49. package/ownership-report.mjs +20 -17
  50. package/ownership-repositories.json +19 -0
  51. package/ownership-scan-worker.mjs +96 -11
  52. package/ownership-source.mjs +39 -0
  53. package/ownership-staged.mjs +193 -0
  54. package/ownership-verdict.mjs +74 -0
  55. package/ownership.mjs +215 -107
  56. package/package.json +1 -1
  57. package/protected-credentials.mjs +111 -0
  58. package/prune-r2.mjs +4 -3
  59. package/pub-upload.mjs +3 -85
  60. package/publication-artifacts.mjs +108 -0
  61. package/publication-gate.mjs +28 -7
  62. package/publish-cargo.mjs +47 -16
  63. package/publish-swift.mjs +46 -17
  64. package/publish-update.mjs +61 -39
  65. package/r2-verified-upload.mjs +41 -0
  66. package/registry-parity.mjs +5 -3
  67. package/release-state.mjs +1 -0
  68. package/release-token.mjs +2 -0
  69. package/rightkit-drift.mjs +3 -2
  70. package/rightkit-ownership.json +2028 -139
  71. package/rightkit-signing-agent.mjs +5 -3
  72. package/rightkit-sync.mjs +6 -2
  73. package/rightkit-versions.json +215 -103
  74. package/runtime-artifact-manifest.mjs +20 -6
  75. package/sign-updater.mjs +5 -0
  76. package/signing-agent-client.mjs +2 -1
  77. package/signing-agent-install.mjs +3 -2
  78. package/standalone-clone-verify.mjs +3 -2
  79. package/tauri-bundle-marker.mjs +3 -2
  80. package/upload-large.mjs +36 -82
  81. package/verifier.mjs +5 -1
@@ -7,6 +7,7 @@ import { sha256 } from "./ownership-approval.mjs";
7
7
 
8
8
  const MAX_LIST_BYTES = 32 * 1024 * 1024;
9
9
  const MAX_BLOB_BATCH_BYTES = 256 * 1024 * 1024;
10
+ const BLOB_CHUNK_BYTES = 32 * 1024 * 1024;
10
11
  const DEFAULT_MAX_FILE_BYTES = 2 * 1024 * 1024;
11
12
  const CACHE_LIMIT = 128 * 1024 * 1024;
12
13
  const blobCache = new Map();
@@ -43,10 +44,37 @@ function decodePath(bytes) {
43
44
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
44
45
  }
45
46
 
47
+ function gitBlobSizes(root, repositoryRoot, oids) {
48
+ const lines = runGit(root, repositoryRoot, ["cat-file", "--batch-check"], { input: Buffer.from(`${oids.join("\n")}\n`) }).toString("ascii").split("\n");
49
+ if (lines.pop() !== "" || lines.length !== oids.length) throw new Error("ownership inventory: malformed Git blob size response");
50
+ return lines.map((line, index) => {
51
+ const [oid, type, sizeText] = line.split(" "), size = Number(sizeText);
52
+ if (oid !== oids[index] || type !== "blob" || !Number.isSafeInteger(size) || size < 0 || size > MAX_BLOB_BATCH_BYTES) throw new Error("ownership inventory: invalid Git blob");
53
+ return size;
54
+ });
55
+ }
56
+
57
+ // Chunk by blob size so a whole-repository read never exceeds one spawn's output buffer.
46
58
  function readGitBlobs(root, repositoryRoot, oids) {
47
59
  if (!oids.length) return new Map();
60
+ const sizes = gitBlobSizes(root, repositoryRoot, oids), result = new Map();
61
+ let chunk = [], chunkBytes = 0;
62
+ const flush = () => {
63
+ for (const [oid, bytes] of readGitBlobChunk(root, repositoryRoot, chunk, chunkBytes)) result.set(oid, bytes);
64
+ chunk = []; chunkBytes = 0;
65
+ };
66
+ oids.forEach((oid, index) => {
67
+ const cost = sizes[index] + 128;
68
+ if (chunk.length && chunkBytes + cost > BLOB_CHUNK_BYTES) flush();
69
+ chunk.push(oid); chunkBytes += cost;
70
+ });
71
+ if (chunk.length) flush();
72
+ return result;
73
+ }
74
+
75
+ function readGitBlobChunk(root, repositoryRoot, oids, expectedBytes) {
48
76
  const input = Buffer.from(`${oids.join("\n")}\n`);
49
- const output = runGit(root, repositoryRoot, ["cat-file", "--batch"], { input, maxBuffer: MAX_BLOB_BATCH_BYTES });
77
+ const output = runGit(root, repositoryRoot, ["cat-file", "--batch"], { input, maxBuffer: Math.min(MAX_BLOB_BATCH_BYTES, expectedBytes + 1024 * 1024) });
50
78
  const result = new Map(); let offset = 0;
51
79
  for (const expected of oids) {
52
80
  const lineEnd = output.indexOf(0x0a, offset);
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schema": 1,
3
3
  "windows": {
4
- "path": "C:\\Users\\adrds\\.rightkit-managed\\ownership-approval\\windows\\content-8d12e39ee6bac3bf8332df8dc61c74637e2eda6becbcc39746fe1ed5a1fe7ef6\\RightKit.OwnershipApproval.Windows.exe",
5
- "sha256": "8d12e39ee6bac3bf8332df8dc61c74637e2eda6becbcc39746fe1ed5a1fe7ef6"
4
+ "path": "C:\\Users\\adrds\\.rightkit-managed\\ownership-approval\\windows\\content-6df7a0a80c70cc9b9a66a9c30d5956500d505f8d15f9c98563a4e8e63a60f12f\\RightKit.OwnershipApproval.Windows.exe",
5
+ "sha256": "6df7a0a80c70cc9b9a66a9c30d5956500d505f8d15f9c98563a4e8e63a60f12f"
6
6
  },
7
7
  "macos": {
8
8
  "path": "/Users/adrdsouza/.rightkit-managed/ownership-approval/macos/content-2c30864bdb666a1ca1e4388ea7a3bc83cb985196d666b96cc30e1da7cd5b0e6b/RightKitOwnershipApproval.app/Contents/MacOS/ownership-approval",
@@ -0,0 +1,107 @@
1
+ // right-release hooks install: writes the advisory pre-commit ownership gate into an app repository.
2
+ //
3
+ // right-release hooks install [--app <dir>]
4
+ //
5
+ // The hook runs the app's pinned @rightkit/release (node_modules) via `right-release ownership --staged`.
6
+ // An existing foreign pre-commit is preserved as pre-commit.rightkit-foreign and chained first.
7
+ // The gate is advisory: `git commit --no-verify` bypasses it; broker admission and the release gate
8
+ // remain authoritative.
9
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { spawnSync } from "./background-process.mjs";
13
+ import { repositoryRootFor } from "./ownership-staged.mjs";
14
+
15
+ export const HOOK_MARKER = "rightkit-ownership-precommit v1";
16
+ const FOREIGN = "pre-commit.rightkit-foreign";
17
+ const PINNED_CLI = path.join("node_modules", "@rightkit", "release", "cli", "right-release.mjs");
18
+
19
+ export function renderPreCommitHook() {
20
+ return [
21
+ "#!/bin/sh",
22
+ `# ${HOOK_MARKER}: advisory ownership gate installed by \`right-release hooks install\`.`,
23
+ "# Block-tier findings without a valid exception fail here; --no-verify bypasses this gate.",
24
+ "# Authoritative enforcement remains in broker admission and the release gate.",
25
+ "ROOT=\"$(git rev-parse --show-toplevel)\" || exit 1",
26
+ "HOOKS_DIR=\"$(dirname \"$0\")\"",
27
+ `if [ -x "$HOOKS_DIR/${FOREIGN}" ]; then`,
28
+ ` "$HOOKS_DIR/${FOREIGN}" "$@" || exit $?`,
29
+ "fi",
30
+ `GATE="$ROOT/${PINNED_CLI.split(path.sep).join("/")}"`,
31
+ "if [ ! -f \"$GATE\" ]; then",
32
+ " echo \"[rightkit] @rightkit/release is not installed in this repository; add it as a devDependency, then re-run: right-release hooks install\" >&2",
33
+ " exit 1",
34
+ "fi",
35
+ "exec node \"$GATE\" ownership --staged",
36
+ "",
37
+ ].join("\n");
38
+ }
39
+
40
+ function git(cwd, args) {
41
+ const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", windowsHide: true, env: Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^GIT_/i.test(k))) });
42
+ if (result.status !== 0) return null;
43
+ return String(result.stdout).trim();
44
+ }
45
+
46
+ /** The directory git runs hooks from: core.hooksPath when configured, else the common hooks directory. */
47
+ export function hooksDirectoryFor(root) {
48
+ const configured = git(root, ["config", "--get", "core.hooksPath"]);
49
+ if (configured) return path.resolve(root, configured);
50
+ const common = git(root, ["rev-parse", "--git-path", "hooks"]);
51
+ if (!common) throw new Error("could not resolve the repository hooks directory");
52
+ return path.resolve(root, common);
53
+ }
54
+
55
+ /**
56
+ * Idempotent install. Returns { status: "installed" | "updated" | "unchanged" | "chained", file }.
57
+ */
58
+ export function installPreCommitHook(repoDir) {
59
+ const root = repositoryRootFor(repoDir);
60
+ const pinned = path.join(root, PINNED_CLI);
61
+ if (!existsSync(pinned)) throw new Error(`@rightkit/release is not installed at ${pinned}; add it as a devDependency and run pnpm install first`);
62
+ const hooksDir = hooksDirectoryFor(root);
63
+ const file = path.join(hooksDir, "pre-commit");
64
+ const foreign = path.join(hooksDir, FOREIGN);
65
+ const body = renderPreCommitHook();
66
+ mkdirSync(hooksDir, { recursive: true });
67
+ let status = "installed";
68
+ if (lstatSync(file, { throwIfNoEntry: false })) {
69
+ const current = readFileSync(file, "utf8");
70
+ if (current === body) return { status: "unchanged", file };
71
+ if (current.includes(HOOK_MARKER)) status = "updated";
72
+ else {
73
+ if (existsSync(path.join(hooksDir, FOREIGN))) throw new Error(`${FOREIGN} already exists; resolve the chained hook manually before installing`);
74
+ renameSync(file, foreign);
75
+ status = "chained";
76
+ }
77
+ }
78
+ const temporary = `${file}.${process.pid}.tmp`;
79
+ writeFileSync(temporary, body, { mode: 0o755 });
80
+ chmodSync(temporary, 0o755);
81
+ renameSync(temporary, file);
82
+ return { status, file };
83
+ }
84
+
85
+ export function main(args, { print = (line) => console.log(line) } = {}) {
86
+ const flag = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
87
+ if (args[0] !== "install") {
88
+ print("right-release hooks install [--app <dir>]");
89
+ return args.includes("--help") ? 0 : 2;
90
+ }
91
+ try {
92
+ const result = installPreCommitHook(path.resolve(flag("--app") ?? process.cwd()));
93
+ print(`[right-release] pre-commit ownership gate ${result.status}: ${result.file}`);
94
+ if (result.status === "chained") print(`[right-release] previous pre-commit preserved as ${FOREIGN} and runs first`);
95
+ return 0;
96
+ } catch (error) {
97
+ print(`[right-release] hooks install FAILED: ${error.message}`);
98
+ return 1;
99
+ }
100
+ }
101
+
102
+ const isEntry = (() => {
103
+ if (!process.argv[1]) return false;
104
+ try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); }
105
+ catch { return false; }
106
+ })();
107
+ if (isEntry) process.exitCode = main(process.argv.slice(2));
@@ -0,0 +1,44 @@
1
+ {
2
+ "owners": [
3
+ {
4
+ "package": "@rightkit/build-control-broker",
5
+ "path": "packages/build-control-broker/package.json",
6
+ "reason": "Private build-control infrastructure; not an application capability"
7
+ },
8
+ {
9
+ "package": "@rightkit/build-control-clients",
10
+ "path": "packages/build-control-clients/package.json",
11
+ "reason": "Private build-control infrastructure; not an application capability"
12
+ },
13
+ {
14
+ "package": "@rightkit/build-control-core",
15
+ "path": "packages/build-control-core/package.json",
16
+ "reason": "Private build-control infrastructure; not an application capability"
17
+ },
18
+ {
19
+ "package": "@rightkit/build-control-phase0",
20
+ "path": "packages/build-control-phase0/package.json",
21
+ "reason": "Private build-control infrastructure; not an application capability"
22
+ },
23
+ {
24
+ "package": "@rightkit/build-control-policy",
25
+ "path": "packages/build-control-policy/package.json",
26
+ "reason": "Private build-control infrastructure; not an application capability"
27
+ },
28
+ {
29
+ "package": "@rightkit/build-control-staging",
30
+ "path": "packages/build-control-staging/package.json",
31
+ "reason": "Private build-control infrastructure; not an application capability"
32
+ },
33
+ {
34
+ "package": "@rightkit/build-control-phase0-macos",
35
+ "path": "packages/build-control-phase0/macos/package.json",
36
+ "reason": "Private native build-control feasibility fixture"
37
+ },
38
+ {
39
+ "package": "@rightkit/build-control-phase0-windows",
40
+ "path": "packages/build-control-phase0/windows/package.json",
41
+ "reason": "Private native build-control feasibility fixture"
42
+ }
43
+ ]
44
+ }
@@ -1,9 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
- import { realpathSync, statSync } from "node:fs";
2
+ import { closeSync, constants, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { spawnSync } from "./background-process.mjs";
5
- import { canonicalRepositoryIdentity, resolveKeyLogTrust } from "./ownership-approval.mjs";
6
+ import { canonicalRepositoryIdentity, resolveKeyLogTrust, trustedPath, __testOnly as approvalTestOnly } from "./ownership-approval.mjs";
6
7
 
8
+ import { performProtectedTrustUpdate } from "./ownership-key-trust.mjs";
9
+
10
+ const HEX = /^[a-f0-9]{64}$/;
7
11
  const LOG_PATH = ".rightkit/ownership-keys.json";
8
12
  const MAX_LOG_BYTES = 2 * 1024 * 1024;
9
13
  const executable = process.platform === "win32" ? "C:\\Program Files\\Git\\cmd\\git.exe" : "/usr/bin/git";
@@ -38,9 +42,83 @@ function validateAuthority(authority) {
38
42
  return { repositoryIdentity, checkoutPath };
39
43
  }
40
44
 
45
+ function checkpointPath(file, rootFingerprint) {
46
+ if (!HEX.test(rootFingerprint ?? "")) throw new Error("protected checkpoint root fingerprint is malformed");
47
+ return path.join(path.dirname(file), "checkpoints", `${rootFingerprint}.json`);
48
+ }
49
+ function auditPath(file, requireNonElevated = true) {
50
+ const result = approvalTestOnly.protectedLocation(file, process.platform, { requireNonElevated });
51
+ if (!result.ok) throw new Error(`protected checkpoint audit failed: ${result.problems.join("; ")}`);
52
+ }
53
+ function boundedJson(file, audit) {
54
+ const st = lstatSync(file);
55
+ if (!st.isFile() || st.isSymbolicLink() || st.size > MAX_LOG_BYTES) throw new Error("protected checkpoint input must be a bounded regular file");
56
+ audit(file);
57
+ return JSON.parse(readFileSync(file, "utf8"));
58
+ }
59
+ function validCheckpoint(value) {
60
+ return value && Object.keys(value).sort().join("\0") === "head\0sequence" && Number.isSafeInteger(value.sequence) && value.sequence >= 0 && HEX.test(value.head ?? "");
61
+ }
62
+ function resolveProtectedCheckpoint(trust, bytes, file, audit) {
63
+ const disk = boundedJson(file, audit);
64
+ if (!disk || disk.rootFingerprint !== trust.rootFingerprint || !validCheckpoint(disk.keyCheckpoint)) throw new Error("protected checkpoint trust pin is malformed or differs from reader pin");
65
+ const floors = [trust.keyCheckpoint, disk.keyCheckpoint];
66
+ const statePath = checkpointPath(file, trust.rootFingerprint);
67
+ let state;
68
+ try { state = boundedJson(statePath, audit); }
69
+ catch (error) { if (error.code !== "ENOENT") throw error; }
70
+ if (state) {
71
+ if (Object.keys(state).sort().join("\0") !== "keyCheckpoint\0rootFingerprint\0schema" || state.schema !== 1 || state.rootFingerprint !== trust.rootFingerprint || !validCheckpoint(state.keyCheckpoint)) throw new Error("protected checkpoint state is malformed");
72
+ floors.push(state.keyCheckpoint);
73
+ }
74
+ let resolved;
75
+ for (const keyCheckpoint of floors) {
76
+ resolved = resolveKeyLogTrust({ ...trust, keyCheckpoint }, bytes);
77
+ if (!resolved.ok) throw new Error(`authority key log verification failed: ${resolved.problems.join("; ")}`);
78
+ }
79
+ return { resolved, statePath, state };
80
+ }
81
+ function syncDirectory(directory) {
82
+ let fd;
83
+ try { fd = openSync(directory, "r"); fsyncSync(fd); }
84
+ catch (error) { if (process.platform !== "win32" || !["EISDIR", "EPERM"].includes(error.code)) throw error; }
85
+ finally { if (fd !== undefined) closeSync(fd); }
86
+ }
87
+ // Called only inside the existing elevated trust-update ceremony.
88
+ function persistCheckpoint(trust, bytes, file, audit) {
89
+ const directory = path.dirname(checkpointPath(file, trust.rootFingerprint));
90
+ audit(path.dirname(file));
91
+ try { mkdirSync(directory, { mode: 0o755 }); } catch (error) { if (error.code !== "EEXIST") throw error; }
92
+ if (!lstatSync(directory).isDirectory() || lstatSync(directory).isSymbolicLink()) throw new Error("protected checkpoint directory is missing or linked");
93
+ audit(directory);
94
+ const lock = path.join(directory, `${trust.rootFingerprint}.lock`);
95
+ let lockFd;
96
+ try { lockFd = openSync(lock, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600); }
97
+ catch (error) { throw new Error(error.code === "EEXIST" ? "protected checkpoint state lock already exists; service/admin recovery is required" : `cannot lock protected checkpoint state: ${error.message}`); }
98
+ const temp = path.join(directory, `.${trust.rootFingerprint}.${process.pid}.${Date.now()}.tmp`);
99
+ let fd;
100
+ try {
101
+ writeFileSync(lockFd, `${process.pid}\n`); fsyncSync(lockFd);
102
+ const current = resolveProtectedCheckpoint(trust, bytes, file, audit);
103
+ const state = { schema: 1, rootFingerprint: trust.rootFingerprint, keyCheckpoint: current.resolved.checkpoint };
104
+ fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o644);
105
+ writeFileSync(fd, `${JSON.stringify(state)}\n`); fsyncSync(fd); closeSync(fd); fd = undefined;
106
+ audit(temp);
107
+ renameSync(temp, current.statePath); syncDirectory(directory);
108
+ const readback = resolveProtectedCheckpoint(trust, bytes, file, audit);
109
+ if (readback.state?.keyCheckpoint.sequence !== state.keyCheckpoint.sequence || readback.state?.keyCheckpoint.head !== state.keyCheckpoint.head) throw new Error("protected checkpoint readback differs from verified log");
110
+ return state.keyCheckpoint;
111
+ } finally {
112
+ if (fd !== undefined) closeSync(fd);
113
+ try { unlinkSync(temp); } catch (error) { if (error.code !== "ENOENT") throw error; }
114
+ closeSync(lockFd); unlinkSync(lock); syncDirectory(directory);
115
+ }
116
+ }
41
117
  /** Read one exact key-log blob from the protected authority checkout's Git HEAD. */
42
- export function readCommittedKeyAuthority(trust) {
118
+ export function readCommittedKeyAuthority(trust) { return readAuthority(trust); }
119
+ function readAuthority(trust, { file, audit = auditPath } = {}) {
43
120
  try {
121
+ file ??= trustedPath();
44
122
  const expected = validateAuthority(trust?.keyAuthority);
45
123
  const top = String(git(expected.checkoutPath, ["rev-parse", "--show-toplevel"])).trim();
46
124
  const normalizedTop = process.platform === "win32" ? path.resolve(top).toLowerCase() : path.resolve(top);
@@ -74,12 +152,39 @@ export function readCommittedKeyAuthority(trust) {
74
152
  }
75
153
  // Re-resolve on each call so current protected policy is always merged into
76
154
  // the cached chain result; approval-policy changes never reuse stale trust.
77
- const resolved = resolveKeyLogTrust(trust, result.bytes);
78
- if (!resolved.ok) throw new Error(`authority key log verification failed: ${resolved.problems.join("; ")}`);
79
- return { ok: true, problems: [], bytes: Buffer.from(result.bytes), digest: result.digest, rootFingerprint: result.rootFingerprint, keyCheckpoint: result.keyCheckpoint, resolvedTrust: resolved.trust, authority: { repositoryIdentity: expected.repositoryIdentity, commit, tree } };
155
+ const protectedState = resolveProtectedCheckpoint(trust, result.bytes, file, audit);
156
+ const resolved = protectedState.resolved;
157
+ if (!protectedState.state || protectedState.state.keyCheckpoint.sequence < resolved.checkpoint.sequence) throw new Error("checkpoint advance required: run the elevated ownership trust update on this host");
158
+ return { ok: true, problems: [], bytes: Buffer.from(result.bytes), digest: result.digest, rootFingerprint: result.rootFingerprint, keyCheckpoint: resolved.checkpoint, resolvedTrust: resolved.trust, authority: { repositoryIdentity: expected.repositoryIdentity, commit, tree } };
80
159
  } catch (error) {
81
160
  return { ok: false, problems: [error?.message ?? "protected key authority could not be read"] };
82
161
  }
83
162
  }
84
163
 
85
- export const __testOnly = Object.freeze({ MAX_LOG_BYTES });
164
+ export const __testOnly = Object.freeze({ MAX_LOG_BYTES, checkpointPath, readAuthority, persistCheckpoint });
165
+
166
+ // Wrapper preserves the existing updater's elevation, request digest, lock & policy checks.
167
+ const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
168
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
169
+ try {
170
+ if (process.argv.length !== 8 || process.argv[2] !== "--trust-script" || process.argv[4] !== "--request" || process.argv[6] !== "--sha256") throw new Error("usage: ownership-key-authority.mjs --trust-script <installed-script> --request <absolute-json> --sha256 <digest>");
171
+ const script = path.join(path.dirname(fileURLToPath(import.meta.url)), "ownership-key-trust.mjs");
172
+ if (path.resolve(process.argv[3]) !== script || realpathSync(script) !== script) throw new Error("protected trust updater must be the installed sibling script");
173
+ const requestPath = process.argv[5], digest = process.argv[7];
174
+ if (!path.isAbsolute(requestPath) || !HEX.test(digest ?? "")) throw new Error("checkpoint update request path or SHA-256 is malformed");
175
+ const st = lstatSync(requestPath);
176
+ if (!st.isFile() || st.isSymbolicLink() || st.size > MAX_LOG_BYTES) throw new Error("checkpoint update request must be a bounded regular file");
177
+ const bytes = readFileSync(requestPath);
178
+ if (createHash("sha256").update(bytes).digest("hex") !== digest) throw new Error("checkpoint update request SHA-256 mismatch");
179
+ const request = JSON.parse(bytes.toString("utf8"));
180
+ if (!request || request.schema !== 1 || Object.keys(request).some((key) => !["schema", "log", "rootFingerprint", "baseline", "authority"].includes(key))) throw new Error("checkpoint update request schema is malformed");
181
+ const file = trustedPath();
182
+ // Check existing sidecar before the updater changes protected trust.
183
+ try { const existing = boundedJson(file, (target) => auditPath(target, false)); resolveProtectedCheckpoint(existing, Buffer.from(JSON.stringify(request.log)), file, (target) => auditPath(target, false)); }
184
+ catch (error) { if (error.code !== "ENOENT") throw error; }
185
+ const updated = performProtectedTrustUpdate(request);
186
+ const checkpoint = persistCheckpoint(updated.trust, Buffer.from(JSON.stringify(request.log)), file, (target) => auditPath(target, false));
187
+ for (const item of updated.warnings) process.stderr.write(`${item.code}: ${item.message}\n`);
188
+ process.stdout.write(`${JSON.stringify({ path: file, checkpoint, warnings: updated.warnings })}\n`);
189
+ } catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 2; }
190
+ }
@@ -1,11 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
- import { accessSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { accessSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
5
  import { fileURLToPath } from "node:url";
6
- import { spawnSync } from "./background-process.mjs";
6
+ import { spawnSync, spawnSyncInteractive, windowsPowerShellEnvironment } from "./background-process.mjs";
7
7
  import { createKeyEvent, keyFingerprint, verifyKeyLog, verifyKeyRequest } from "./ownership-key-log.mjs";
8
- import { canonicalRepositoryIdentity, loadTrustedExternal, resolveKeyLogTrust } from "./ownership-approval.mjs";
8
+ import { canonicalRepositoryIdentity, DEFAULT_TRUST_PATH, loadTrustedExternal, resolveKeyLogTrust, __testOnly as approvalTestOnly } from "./ownership-approval.mjs";
9
9
  import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
10
10
 
11
11
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -235,15 +235,16 @@ function helperFor(run) {
235
235
  if (sha(bytes) !== item.sha256) throw new Error("ownership native helper hash does not match pinned config");
236
236
  return item.path;
237
237
  }
238
- function invokeHelper(run, helper, args) {
239
- const r = run(helper, args);
238
+ function invokeHelper(run, helper, args, interactiveRun = run) {
239
+ const interactive = ["--key-operation", "--enroll", "--enroll-root"].includes(args[0]);
240
+ const r = interactive ? interactiveRun(helper, args, { shell: false, windowsHide: false }) : run(helper, args);
240
241
  if (r.status !== 0 || String(r.stderr ?? "").includes("RightKit ownership keychain preflight:")) {
241
242
  throw new Error(String(r.stderr || "native ownership operation cancelled or failed").trim());
242
243
  }
243
244
  return String(r.stdout ?? "");
244
245
  }
245
246
  function helperFile(run, helper, args, outPath, what) {
246
- invokeHelper(run, helper, args);
247
+ invokeHelper(run, helper, args, run.interactive ?? run);
247
248
  return parse(readFileSync(outPath, "utf8"), what);
248
249
  }
249
250
  function readRequestFile(file) {
@@ -257,29 +258,66 @@ function validateNative(out, expectedHost = platform) {
257
258
  if (out.keyId !== `${expectedHost === "windows" ? "win" : "mac"}-${fingerprint}`) throw new Error("native helper key id does not match its public key");
258
259
  return { keyId: fingerprint, publicKeyPem: out.publicKeyPem, platform: expectedHost };
259
260
  }
260
- function elevate(run, requestPath, digest) {
261
- const child = path.join(here, "ownership-key-trust.mjs");
262
- const args = [child, "--request", requestPath, "--sha256", digest];
263
- if (process.platform === "win32") {
264
- const invocation = buildWindowsElevationInvocation(process.execPath, child, requestPath, digest);
265
- const r = run(invocation.parentExecutable, invocation.parentArgs);
261
+ function elevate(run, requestPath, digest, { modulePath = fileURLToPath(import.meta.url), nodeExe = process.execPath, hostPlatform = process.platform, audit = (target) => approvalTestOnly.protectedLocation(target, hostPlatform) } = {}) {
262
+ let child, wrapper, registry, currentNode;
263
+ const refuse = (message) => { throw Object.assign(new Error(`RK_OWNERSHIP_ELEVATION_UNPROTECTED: ${message}`), { code: "RK_OWNERSHIP_ELEVATION_UNPROTECTED", type: "RK_OWNERSHIP_ELEVATION_UNPROTECTED" }); };
264
+ try {
265
+ if (!path.isAbsolute(requestPath) || !HEX.test(digest ?? "")) refuse("request path or SHA-256 is malformed");
266
+ const requestStat = lstatSync(requestPath);
267
+ if (!requestStat.isFile() || requestStat.isSymbolicLink() || (hostPlatform === "win32" && (requestStat.attributes & 0x400)) || requestStat.size > 2 * 1024 * 1024) refuse("request must be a bounded regular non-linked file");
268
+ if (sha(readFileSync(requestPath)) !== digest) refuse("request bytes do not match pinned SHA-256");
269
+ const trustPath = DEFAULT_TRUST_PATH[hostPlatform];
270
+ if (!trustPath) refuse(`ownership trust path is unavailable for ${hostPlatform}`);
271
+ const trustDirectory = path.dirname(trustPath);
272
+ for (const target of [trustDirectory, trustPath]) {
273
+ try {
274
+ const stat = lstatSync(target);
275
+ if (stat.isSymbolicLink() || (hostPlatform === "win32" && (stat.attributes & 0x400))) refuse(`protected trust path is linked: ${target}`);
276
+ if (target === trustDirectory && !stat.isDirectory()) refuse("protected trust location is not a directory");
277
+ if (target === trustPath && !stat.isFile()) refuse("protected trust file is not a regular file");
278
+ } catch (error) { if (error.code !== "ENOENT") throw error; }
279
+ }
280
+ const protectedTarget = existsSync(trustPath) ? trustPath : existsSync(trustDirectory) ? trustDirectory : null;
281
+ if (protectedTarget) {
282
+ const protection = audit(protectedTarget);
283
+ if (protection?.ok !== true) refuse((protection?.problems ?? ["protected trust location is unprotected"]).join("; "));
284
+ }
285
+ const source = realpathSync(modulePath);
286
+ const sourceDir = path.dirname(source);
287
+ child = path.join(sourceDir, "ownership-key-trust.mjs");
288
+ wrapper = path.join(sourceDir, "ownership-key-authority.mjs");
289
+ registry = path.join(sourceDir, "ownership-repositories.json");
290
+ currentNode = realpathSync(nodeExe);
291
+ for (const target of [source, child, wrapper, registry, currentNode]) {
292
+ const stat = lstatSync(target);
293
+ if (!stat.isFile() || stat.isSymbolicLink() || (hostPlatform === "win32" && (stat.attributes & 0x400)) || realpathSync(target) !== target) refuse(`elevated input must be a regular non-linked file: ${target}`);
294
+ }
295
+ } catch (error) {
296
+ if (error?.code === "RK_OWNERSHIP_ELEVATION_UNPROTECTED") throw error;
297
+ refuse(`installed ownership elevation cannot be verified: ${error.message}`);
298
+ }
299
+ const args = [wrapper, "--trust-script", child, "--request", requestPath, "--sha256", digest];
300
+ if (hostPlatform === "win32") {
301
+ const invocation = buildWindowsElevationInvocation(currentNode, child, requestPath, digest, wrapper);
302
+ const r = run(invocation.parentExecutable, invocation.parentArgs, { env: windowsPowerShellEnvironment() });
266
303
  if (r.status !== 0) throw new Error("elevated trust update failed or was cancelled");
267
304
  } else {
268
305
  let r;
269
- try { r = run("/usr/bin/sudo", ["-k", process.execPath, ...args], { stdio: "inherit" }); }
306
+ try { r = run("/usr/bin/sudo", ["-k", currentNode, ...args], { stdio: "inherit", env: { ...process.env, NODE_OPTIONS: "", NODE_PATH: "" } }); }
270
307
  finally { try { run("/usr/bin/sudo", ["-K"], { stdio: "inherit" }); } catch {} }
271
308
  if (r.status !== 0) throw new Error("elevated trust update failed or was cancelled");
272
309
  }
273
310
  }
274
- function buildWindowsElevationInvocation(nodeExe, trustScript, requestPath, digest) {
311
+ function buildWindowsElevationInvocation(nodeExe, trustScript, requestPath, digest, checkpointScript) {
275
312
  const powershell = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
276
313
  const quote = (value) => `'${value.replaceAll("'", "''")}'`;
277
- const childScript = `& ${[nodeExe, trustScript, "--request", requestPath, "--sha256", digest].map(quote).join(" ")}; exit $LASTEXITCODE`;
314
+ const childArgs = checkpointScript ? [nodeExe, checkpointScript, "--trust-script", trustScript, "--request", requestPath, "--sha256", digest] : [nodeExe, trustScript, "--request", requestPath, "--sha256", digest];
315
+ const childScript = `& ${childArgs.map(quote).join(" ")}; exit $LASTEXITCODE`;
278
316
  const childEncoded = Buffer.from(childScript, "utf16le").toString("base64");
279
- const parentScript = `$ErrorActionPreference='Stop'; try { $p=Start-Process -FilePath ${quote(powershell)} -ArgumentList ${quote(`-NoProfile -EncodedCommand ${childEncoded}`)} -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $p.ExitCode } catch { exit 1 }`;
317
+ const parentScript = `$ErrorActionPreference='Stop'; try { $env:NODE_OPTIONS=$null; $env:NODE_PATH=$null; $p=Start-Process -FilePath ${quote(powershell)} -ArgumentList ${quote(`-NoProfile -EncodedCommand ${childEncoded}`)} -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $p.ExitCode } catch { exit 1 }`;
280
318
  return { parentExecutable: powershell, parentArgs: ["-NoProfile", "-EncodedCommand", Buffer.from(parentScript, "utf16le").toString("base64")], childScript };
281
319
  }
282
- export const __testOnly = Object.freeze({ buildWindowsElevationInvocation, assertSafeMetadataParents, detectMinimizeHook });
320
+ export const __testOnly = Object.freeze({ elevate, buildWindowsElevationInvocation, assertSafeMetadataParents, detectMinimizeHook });
283
321
  function trustState() { return loadTrustedExternal(); }
284
322
  function currentLogTrust(root, log, trust = trustState(), authority, injected = false) {
285
323
  if (!trust.trust?.rootFingerprint) throw new Error("protected root trust is not enrolled on this host");
@@ -304,7 +342,10 @@ function makeTrustRequest(log, rootFingerprint, baseline, authority) {
304
342
  }
305
343
 
306
344
  export async function main(args, deps = {}) {
307
- const run = deps.runner ?? spawnSync;
345
+ const runner = deps.runner ?? spawnSync;
346
+ const interactiveRun = deps.runner ?? spawnSyncInteractive;
347
+ const run = (...runArgs) => runner(...runArgs);
348
+ run.interactive = interactiveRun;
308
349
  const print = deps.print ?? console.log;
309
350
  const app = path.resolve(flag(args, "--app") ?? process.cwd());
310
351
  const root = deps.root ?? repoRoot(app, run);
@@ -324,7 +365,8 @@ export async function main(args, deps = {}) {
324
365
  if (verb === "enroll") return await enroll(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
325
366
  if (verb === "approve-key") return await approve(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
326
367
  if (verb === "revoke-key") return await revoke(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
327
- throw new Error("expected enroll, approve-key, or revoke-key");
368
+ if (verb === "trust-sync") return await trustSync(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
369
+ throw new Error("expected enroll, approve-key, revoke-key, or trust-sync");
328
370
  } finally {
329
371
  closeSync(lockFd);
330
372
  rmSync(lockPath, { force: true });
@@ -439,6 +481,21 @@ async function resumeRootEnrollment(log, d) {
439
481
  print(`Ownership root enrollment recovery completed: ${pin}; active keys: ${state.keys.length}`);
440
482
  return 0;
441
483
  }
484
+ /** Advance this host's protected checkpoint to the committed key log (after another host approved or revoked a key). */
485
+ async function trustSync(_args, d) {
486
+ const log = readLog(d.root);
487
+ const pin = log.events[0]?.payload?.rootFingerprint;
488
+ const state = verifyKeyLog(log, { rootFingerprint: pin });
489
+ if (!state.ok) throw new Error(`committed key log is invalid: ${state.problems.join("; ")}`);
490
+ const trust = (d.trustLoader ?? trustState)();
491
+ if (!trust?.trust?.rootFingerprint) throw new Error("protected root trust is not enrolled on this host; run ownership enroll");
492
+ if (trust.trust.rootFingerprint !== pin) throw new Error("committed key log root does not match this host's protected trust");
493
+ const baseline = d.baseline ?? parse(readFileSync(path.join(here, "ownership-default-policy.json"), "utf8"), "ownership default policy");
494
+ await installCheckpoint(log, pin, baseline, d);
495
+ postVerify(d.root, log, d);
496
+ d.print(`Ownership trust synced to key-log sequence ${log.events.length - 1}; active keys: ${state.keys.length}`);
497
+ return 0;
498
+ }
442
499
  async function installCheckpoint(log, rootFingerprint, baseline, d) {
443
500
  const request = makeTrustRequest(log, rootFingerprint, baseline, d.authority);
444
501
  try { await (d.elevate ?? elevate)(d.run, request.file, request.digest); }