@rightkit/release 0.2.134 → 0.2.136

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 (86) 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 +53 -12
  5. package/background-process.mjs +18 -0
  6. package/build-release.mjs +4 -3
  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-argv.mjs +64 -0
  14. package/codesign-environment.mjs +58 -0
  15. package/codesign-shim.mjs +51 -0
  16. package/committed-source.mjs +3 -3
  17. package/development.mjs +11 -9
  18. package/direct-bootstrap.mjs +19 -9
  19. package/dmg-build.mjs +117 -0
  20. package/dmg-layout.mjs +123 -0
  21. package/github-release.mjs +74 -37
  22. package/heavy-command.mjs +3 -2
  23. package/install-app.mjs +358 -0
  24. package/inventory-check.mjs +980 -0
  25. package/macos-notarization.mjs +24 -8
  26. package/macos-signing-session.mjs +50 -2
  27. package/managed-cargo.mjs +23 -19
  28. package/managed-host-secrets.mjs +30 -0
  29. package/managed-roots.mjs +39 -0
  30. package/model-promote.mjs +31 -37
  31. package/notarize-macos.mjs +5 -5
  32. package/notary-auth.mjs +4 -0
  33. package/osascript-guard.mjs +24 -0
  34. package/ownership-approval-command.mjs +15 -4
  35. package/ownership-approval.mjs +114 -49
  36. package/ownership-availability.mjs +25 -0
  37. package/ownership-catalog-config.json +18 -0
  38. package/ownership-catalog.mjs +94 -0
  39. package/ownership-command.mjs +17 -10
  40. package/ownership-declaration.json +15 -0
  41. package/ownership-default-policy.json +1 -11
  42. package/ownership-evidence.mjs +10 -8
  43. package/ownership-git-inventory.mjs +29 -1
  44. package/ownership-helper-paths.json +4 -4
  45. package/ownership-hooks.mjs +107 -0
  46. package/ownership-internal-owners.json +44 -0
  47. package/ownership-key-authority.mjs +112 -7
  48. package/ownership-key-command.mjs +76 -19
  49. package/ownership-key-trust.mjs +97 -9
  50. package/ownership-lockfiles.mjs +311 -0
  51. package/ownership-report-command.mjs +12 -3
  52. package/ownership-report.mjs +20 -17
  53. package/ownership-repositories.json +19 -0
  54. package/ownership-scan-worker.mjs +96 -11
  55. package/ownership-source.mjs +39 -0
  56. package/ownership-staged.mjs +193 -0
  57. package/ownership-verdict.mjs +74 -0
  58. package/ownership.mjs +215 -107
  59. package/package.json +1 -1
  60. package/protected-credentials.mjs +111 -0
  61. package/prune-r2.mjs +4 -3
  62. package/pub-upload.mjs +3 -85
  63. package/publication-artifacts.mjs +108 -0
  64. package/publication-gate.mjs +28 -7
  65. package/publish-cargo.mjs +47 -16
  66. package/publish-swift.mjs +46 -17
  67. package/publish-update.mjs +61 -39
  68. package/r2-verified-upload.mjs +41 -0
  69. package/registry-parity.mjs +5 -3
  70. package/release-state.mjs +1 -0
  71. package/release-token.mjs +2 -0
  72. package/release.mjs +2 -1
  73. package/rightkit-drift.mjs +3 -2
  74. package/rightkit-ownership.json +2028 -139
  75. package/rightkit-signing-agent.mjs +161 -19
  76. package/rightkit-sync.mjs +6 -2
  77. package/rightkit-versions.json +220 -106
  78. package/runtime-artifact-manifest.mjs +20 -6
  79. package/sign-updater.mjs +5 -0
  80. package/signing-agent-client.mjs +78 -22
  81. package/signing-agent-command.mjs +5 -4
  82. package/signing-agent-install.mjs +31 -4
  83. package/standalone-clone-verify.mjs +3 -2
  84. package/tauri-bundle-marker.mjs +3 -2
  85. package/upload-large.mjs +36 -82
  86. package/verifier.mjs +5 -1
@@ -42,6 +42,7 @@ try {
42
42
  if (githubBuildCommands.has(first)
43
43
  || first?.startsWith("-")
44
44
  || (first === "addon" && args[1] === "build")
45
+ || (first === "dmg" && args[1] === "build")
45
46
  || (first === "publish" && ["cargo", "swift"].includes(args[1]))) {
46
47
  assertReleaseExecutionAllowed({ command: `right-release ${args.join(" ")}`.trim() });
47
48
  }
@@ -63,6 +64,8 @@ if (first === "--version" || first === "-v") {
63
64
  } else {
64
65
  run("release.mjs", ["--doctor", ...args.slice(1)]);
65
66
  }
67
+ } else if (first === "hooks") {
68
+ run("ownership-hooks.mjs", args.slice(1));
66
69
  } else if (first === "suite-doctor") {
67
70
  runTest(
68
71
  ["--test", path.join(packageRoot, "right-suite-contract.test.mjs")],
@@ -70,6 +73,10 @@ if (first === "--version" || first === "-v") {
70
73
  );
71
74
  } else if (first === "cache") {
72
75
  run("cache-command.mjs", args.slice(1));
76
+ } else if (first === "install-app") {
77
+ run("install-app.mjs", args.slice(1));
78
+ } else if (first === "dmg") {
79
+ run("dmg-build.mjs", args.slice(1));
73
80
  } else if (first === "apple") {
74
81
  run("apple-profile-renew.mjs", args.slice(1));
75
82
  } else if (first === "publish") {
@@ -83,6 +90,12 @@ if (first === "--version" || first === "-v") {
83
90
  } else {
84
91
  run("upload-release.mjs", rest);
85
92
  }
93
+ } else if (first === "inventory") {
94
+ if (args[1] !== "check") {
95
+ console.error("right-release inventory: expected check");
96
+ process.exit(2);
97
+ }
98
+ run("inventory-check.mjs", args.slice(2));
86
99
  } else if (first === "model") {
87
100
  const rest = args.slice(1);
88
101
  if (rest[0] !== "promote") {
@@ -155,12 +168,20 @@ function printHelp() {
155
168
  console.log(`right-release <command> [options]
156
169
 
157
170
  Commands:
171
+ dmg build --app <.app> --out <dmg> --volname <name> [--background <png>]
172
+ [--icon-size N] [--app-position x,y] [--applications-position x,y] [--window w,h]
173
+ Build branded UDZO image without Finder
174
+ install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id <id>] [--dest /Applications]
175
+ [--relaunch|--no-relaunch] [--json]
176
+ Verify a signed DMG, quit, replace & relaunch hidden; no UI, no osascript (macOS)
158
177
  build [--platform mac|win] [--skip-checks] [--resume-from <phase>]
159
178
  Resume macOS at prepackage, sign, package, post-sign, or harden
160
179
  dev --platform win|mac [--config <path>] [--dry-run] [--build-only]
161
180
  Incremental unsigned build & optional local install
162
181
  upload --release <id> --platform mac|win --tier patch|update
163
182
  Upload one sealed release; never builds
183
+ publish-update --release <id> --platform mac|win --tier patch|update
184
+ Upload sealed bytes to R2 & register RightApps
164
185
  release Compatibility alias for build
165
186
  publish Compatibility alias for upload
166
187
  publish npm --package @rightkit/<name> [--dry-run] Gate preflight for npm publish
@@ -169,6 +190,8 @@ Commands:
169
190
  (npm-rightkit-publish) and verify registry bytes
170
191
  publish cargo --crate <name> [--dry-run] Test, inspect, scan, and publish one crates.io package
171
192
  publish swift [--repo <git-url>] [--dry-run] Test, tag, resolve a fresh exact-pin consumer, and publish RightKitSwift to its git repo
193
+ inventory check [--json] [--warn-only] [--md <file>] [--no-cache]
194
+ Registry, account, dependency, app & hygiene drift (public endpoints)
172
195
  model promote --authority <app-key> --config <file> [--dry-run]
173
196
  Sign and promote one runtime/model artifact
174
197
  addon doctor|build|upload|adopt <options> Manage a signed headless add-on
@@ -179,10 +202,13 @@ Commands:
179
202
  doctor --all Verify all Right Suite app release contracts
180
203
  suite-doctor Verify all local Right Suite repositories
181
204
  ownership [--app <dir>] [--json] Fail on app copies of RightKit-owned capabilities
205
+ ownership --staged [--app <dir>] [--json] Pre-commit gate: scan the staged index for block-tier findings
206
+ hooks install [--app <dir>] Install the advisory pre-commit ownership gate (pinned @rightkit/release)
182
207
  ownership enroll [--root] [--name <device>] [--root-fingerprint <sha256>] [--app <dir>]
183
208
  Enroll one native key or create a signed-device request
184
209
  ownership approve-key <fingerprint|request.json> Approve one pending device key from an active host
185
210
  ownership revoke-key <fingerprint> Revoke one active device key
211
+ ownership trust-sync Advance this host's protected trust to the committed key log
186
212
  ownership-approval --draft <input.json> [--out <draft.json>]
187
213
  Draft an exception for native owner approval
188
214
  ownership-approval --verify <signed.json> --context <scan.json>
@@ -0,0 +1,64 @@
1
+ import path from "node:path";
2
+
3
+ // Closed grammar shared by shim & agent. Never accept executable names, an
4
+ // environment, keychains, detached signatures, or file-producing display flags.
5
+ export function parseCodesignArgv(argv, cwd) {
6
+ const deny = () => { const error = new Error("Unsupported codesign argv"); error.code = "MAC_SIGNING_AGENT_ARGV_DENIED"; throw error; };
7
+ if (!Array.isArray(argv) || !argv.length || argv.length > 8192
8
+ || argv.some((arg) => typeof arg !== "string" || !arg || arg.includes("\0") || arg.length > 32768)
9
+ || typeof cwd !== "string" || !path.isAbsolute(cwd)) deny();
10
+ const targets = [], inputs = [], options = [];
11
+ let mode, verbose = false, operands = false;
12
+ const operation = (next) => { if (mode) deny(); mode = next; };
13
+ const value = (inline, i) => {
14
+ const result = inline ?? argv[i + 1];
15
+ if (!result || result.startsWith("--")) deny();
16
+ return result;
17
+ };
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const arg = argv[i];
20
+ if (operands || !arg.startsWith("-")) { targets.push(path.isAbsolute(arg) ? arg : `${cwd}/${arg}`); continue; }
21
+ if (arg === "--") { operands = true; continue; }
22
+ if (/^-[dv]+$/.test(arg)) {
23
+ if (arg.includes("d")) { if ((arg.match(/d/g) ?? []).length !== 1) deny(); operation("display"); }
24
+ if (arg.includes("v")) verbose = true;
25
+ continue;
26
+ }
27
+ const equal = arg.indexOf("=");
28
+ const flag = equal < 0 ? arg : arg.slice(0, equal);
29
+ const inline = equal < 0 ? undefined : arg.slice(equal + 1);
30
+ if (flag === "--display" && inline === undefined) { operation("display"); continue; }
31
+ if (flag === "--verify" && inline === undefined) { operation("verify"); continue; }
32
+ if (flag === "--verbose") { if (inline !== undefined && !/^\d{1,2}$/.test(inline)) deny(); verbose = true; continue; }
33
+ if (["-s", "--sign"].includes(flag) || /^-s.+/.test(arg)) {
34
+ const attached = flag === "-s" || flag === "--sign" ? inline : arg.slice(2);
35
+ const identity = value(attached, i);
36
+ if (identity.length > 512 || (identity.startsWith("-") && identity !== "-")) deny();
37
+ operation("sign"); if (attached === undefined) i++;
38
+ continue;
39
+ }
40
+ if (["--force", "--deep", "--strict"].includes(flag) && inline === undefined) { options.push(flag); continue; }
41
+ if (flag === "--timestamp" && (inline === undefined || inline === "none")) { options.push(flag); continue; }
42
+ if (["--options", "--identifier", "--preserve-metadata", "--entitlements", "--requirements"].includes(flag) || arg === "-r-") {
43
+ const parameter = arg === "-r-" ? "-" : value(inline, i);
44
+ if (arg !== "-r-" && inline === undefined) i++;
45
+ if (flag === "--options" && !/^(runtime|hard|kill|host|expires|restrict|library)(,(runtime|hard|kill|host|expires|restrict|library))*$/.test(parameter)) deny();
46
+ if (flag === "--preserve-metadata" && !/^(identifier|entitlements|requirements|flags|runtime)(,(identifier|entitlements|requirements|flags|runtime))*$/.test(parameter)) deny();
47
+ if (flag === "--identifier" && (parameter.startsWith("-") || parameter.length > 512)) deny();
48
+ options.push(arg === "-r-" ? "--requirements" : flag);
49
+ if (flag === "--entitlements") inputs.push({ value: parameter, path: path.isAbsolute(parameter) ? parameter : `${cwd}/${parameter}` });
50
+ if ((flag === "--requirements" || arg === "-r-") && parameter !== "-") deny();
51
+ continue;
52
+ }
53
+ deny();
54
+ }
55
+ mode ??= verbose ? "verify" : undefined;
56
+ if (!mode || !targets.length || targets.length > 4096) deny();
57
+ const permitted = mode === "sign"
58
+ ? ["--force", "--options", "--timestamp", "--entitlements", "--deep", "--preserve-metadata", "--identifier"]
59
+ : mode === "verify" ? ["--strict", "--deep"] : ["--entitlements", "--requirements"];
60
+ if (options.some((flag) => !permitted.includes(flag))) deny();
61
+ if (mode === "display" && inputs.some((input) => ![":-", "-"].includes(input.value))) deny();
62
+ if (mode === "sign" && inputs.some((input) => [":-", "-"].includes(input.value))) deny();
63
+ return { mode, targets, entitlements: mode === "sign" ? inputs.map((input) => input.path) : [] };
64
+ }
@@ -0,0 +1,58 @@
1
+ import * as fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { fileURLToPath } from "node:url";
5
+ import { detectMacSigningSession } from "./macos-signing-session.mjs";
6
+ import { ensurePrivateDirectory, privatePath } from "./rightkit-signing-agent.mjs";
7
+ import { resolveManagedRoot } from "./artifact-storage.mjs";
8
+
9
+ const quote = (text) => `'${String(text).replaceAll("'", "'\\''")}'`;
10
+ export function codesignShimScript({ route, node = process.execPath,
11
+ entrypoint = fileURLToPath(new URL("./codesign-shim.mjs", import.meta.url)), realCodesign = "/usr/bin/codesign" }) {
12
+ return `#!/bin/sh\nif [ -n "\${SSH_CONNECTION-}\${SSH_TTY-}\${SSH_CLIENT-}" ] || [ ${quote(route)} = agent ]; then
13
+ exec ${quote(node)} ${quote(entrypoint)} "$@"
14
+ fi
15
+ exec ${quote(realCodesign)} "$@"\n`;
16
+ }
17
+
18
+ export function macosCodesignEnvironment(env = process.env, { platform = process.platform, io = fs,
19
+ detect = detectMacSigningSession, node = process.execPath } = {}) {
20
+ if (platform !== "darwin") return env;
21
+ const route = detect({ env }).guiRunner ? "agent" : "direct";
22
+ const script = codesignShimScript({ route, node });
23
+ const automationScript = `#!/bin/sh\nexec ${quote(node)} ${quote(fileURLToPath(new URL("./osascript-guard.mjs", import.meta.url)))} "$@"\n`;
24
+ // Tauri's script invokes /usr/bin/osascript, bypassing PATH. Its env-bash
25
+ // shebang & app-side `bash bundle_dmg.sh` calls both traverse this wrapper.
26
+ const bashScript = `#!/bin/sh\nlayout_script=\nskip_layout=0\nfor arg in "$@"; do
27
+ case "$arg" in
28
+ */bundle_dmg.sh|bundle_dmg.sh) layout_script="$arg" ;;
29
+ --skip-jenkins) skip_layout=1 ;;
30
+ esac
31
+ done
32
+ if [ -n "$layout_script" ] && [ "$skip_layout" = 0 ]; then
33
+ exec ${quote(node)} ${quote(fileURLToPath(new URL("./osascript-guard.mjs", import.meta.url)))} --caller "$layout_script"
34
+ fi
35
+ exec /bin/bash "$@"\n`;
36
+ // Immutable wrappers avoid races between GUI & SSH builds or package versions.
37
+ const identity = createHash("sha256").update(script).update(automationScript).update(bashScript).digest("hex").slice(0, 24);
38
+ const bin = path.join(resolveManagedRoot({ env }), "signing", "codesign-bin", identity);
39
+ ensurePrivateDirectory(bin, { io });
40
+ const file = path.join(bin, "codesign");
41
+ if (io.existsSync(file)) {
42
+ privatePath(file, 0o700, { io });
43
+ if (io.readFileSync(file, "utf8") !== script) throw new Error("Managed codesign shim content changed");
44
+ } else io.writeFileSync(file, script, { mode: 0o700, flag: "wx" });
45
+ const automationFile = path.join(bin, "osascript");
46
+ if (io.existsSync(automationFile)) {
47
+ privatePath(automationFile, 0o700, { io });
48
+ if (io.readFileSync(automationFile, "utf8") !== automationScript) throw new Error("Managed osascript guard content changed");
49
+ } else io.writeFileSync(automationFile, automationScript, { mode: 0o700, flag: "wx" });
50
+ const bashFile = path.join(bin, "bash");
51
+ if (io.existsSync(bashFile)) {
52
+ privatePath(bashFile, 0o700, { io });
53
+ if (io.readFileSync(bashFile, "utf8") !== bashScript) throw new Error("Managed bash guard content changed");
54
+ } else io.writeFileSync(bashFile, bashScript, { mode: 0o700, flag: "wx" });
55
+ // Keep every other shim (especially managed Cargo) in its original order.
56
+ return { ...env, CI: "true", TAURI_BUNDLER_DMG_IGNORE_CI: "false",
57
+ PATH: [bin, ...(env.PATH ?? "").split(path.delimiter).filter((entry) => entry !== bin)].join(path.delimiter) };
58
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { spawnSync } from "./background-process.mjs";
7
+ import { parseCodesignArgv } from "./codesign-argv.mjs";
8
+ import { detectMacSigningSession } from "./macos-signing-session.mjs";
9
+ import { requestSigningAgent } from "./signing-agent-client.mjs";
10
+ import { defaultSigningRoots, privatePath, signingAgentPaths, SigningAgentError } from "./rightkit-signing-agent.mjs";
11
+
12
+ export async function runCodesignShim(argv, { cwd = process.cwd(), env = process.env, home = os.userInfo().homedir,
13
+ io = fs, run = spawnSync, detect = detectMacSigningSession, request = requestSigningAgent } = {}) {
14
+ if (!detect({ env, run }).guiRunner) {
15
+ // Installed shell shim uses exec here; this branch also supports API callers.
16
+ const result = run("/usr/bin/codesign", argv, { cwd, env, stdio: "inherit", shell: false, windowsHide: true });
17
+ if (result.error) throw result.error;
18
+ return { status: result.status ?? 1, stdout: "", stderr: "" };
19
+ }
20
+ const paths = signingAgentPaths(home);
21
+ let roots = defaultSigningRoots(home, env.RIGHTKIT_MANAGED_ROOT);
22
+ if (io.existsSync(paths.config)) {
23
+ privatePath(paths.config, 0o600, { io });
24
+ roots = JSON.parse(io.readFileSync(paths.config, "utf8")).roots;
25
+ }
26
+ const { targets } = parseCodesignArgv(argv, cwd);
27
+ const report = await request({ schema: 1, type: "codesign-argv", argv, cwd, targets,
28
+ allowedRoots: roots.filter((root) => io.existsSync(root)), timeoutMs: 180000 }, { paths });
29
+ const result = report.results?.[0];
30
+ if (report.schema !== 1 || report.ok !== true || report.results?.length !== 1
31
+ || !Number.isInteger(result?.status) || result.status < 0 || result.status > 255
32
+ || typeof result.stdout !== "string" || typeof result.stderr !== "string") {
33
+ throw new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Invalid codesign result");
34
+ }
35
+ return result;
36
+ }
37
+
38
+ const realOrSelf = (file) => { try { return fs.realpathSync(file); } catch { return path.resolve(file); } };
39
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
40
+ try {
41
+ // Only the managed shell wrapper invokes this entry: session detection was
42
+ // already performed by right-release before selecting its wrapper directory.
43
+ const result = await runCodesignShim(process.argv.slice(2), { detect: () => ({ guiRunner: true }) });
44
+ process.stdout.write(result.stdout);
45
+ process.stderr.write(result.stderr);
46
+ process.exitCode = result.status;
47
+ } catch (error) {
48
+ process.stderr.write(`rightkit codesign: [${error.code ?? "MAC_SIGNING_AGENT_JOB_FAILED"}] ${error.message}\n`);
49
+ process.exitCode = 1;
50
+ }
51
+ }
@@ -35,7 +35,7 @@ function gitEnv(env = process.env, platform = process.platform) {
35
35
  }
36
36
 
37
37
  function runGit(root, args, { encoding = "buffer", maxBuffer = 4 * 1024 * 1024, timeout = COMMAND_TIMEOUT_MS, runner = spawnSync, env = gitEnv(), input } = {}) {
38
- const result = runner(nativeGitPath(), ["-C", root, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, ...args], {
38
+ const result = runner(nativeGitPath(), ["-C", root, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, "-c", `safe.directory=${root}`, ...args], {
39
39
  encoding, env, windowsHide: true, maxBuffer, timeout, ...(input ? { input } : {}),
40
40
  });
41
41
  if (result?.error || result?.status !== 0) throw new Error(`committed ownership: git ${args[0] ?? "command"} failed${result?.error?.code ? ` (${result.error.code})` : ""}`);
@@ -118,7 +118,7 @@ function acquire(root, { expectedCommit, expectedTree, requiredPaths = [], runne
118
118
  const text = (args) => runGit(root, args, { encoding: "utf8", runner, env }).trim();
119
119
  const repositoryRoot = path.resolve(text(["rev-parse", "--show-toplevel"]));
120
120
  const pathPrefix = text(["rev-parse", "--show-prefix"]).replaceAll("\\", "/");
121
- const originResult = runner(nativeGitPath(), ["-C", repositoryRoot, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, "config", "--get", "remote.origin.url"], {
121
+ const originResult = runner(nativeGitPath(), ["-C", repositoryRoot, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, "-c", `safe.directory=${repositoryRoot}`, "config", "--get", "remote.origin.url"], {
122
122
  encoding: "utf8", env, windowsHide: true, maxBuffer: 4 * 1024 * 1024, timeout: COMMAND_TIMEOUT_MS,
123
123
  });
124
124
  const origin = originResult?.status === 0 ? String(originResult.stdout).trim() : null;
@@ -132,7 +132,7 @@ function acquire(root, { expectedCommit, expectedTree, requiredPaths = [], runne
132
132
  const rows = parseTree(runGit(root, ["ls-tree", "-r", "-z", "--full-tree", tree], { encoding: "buffer", maxBuffer: MAX_TREE_BYTES, timeout: COMMAND_TIMEOUT_MS, runner, env }), objectFormat);
133
133
  const wanted = new Set(requiredPaths.filter(validatePath));
134
134
  const shouldRead = (row) => row.type === "blob" && row.mode !== "120000" && (
135
- wanted.has(row.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json"].includes(path.posix.basename(row.rel)) ||
135
+ wanted.has(row.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json", "Cargo.lock", "pnpm-lock.yaml", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock"].includes(path.posix.basename(row.rel)) ||
136
136
  /\.(?:rs|ts|tsx|js|mjs|cjs|jsx|swift|m|mm|c|cc|cpp|h|py|go|kt|java|cs)$/i.test(row.rel)
137
137
  );
138
138
  const candidates = rows.filter(shouldRead);
package/development.mjs CHANGED
@@ -1,14 +1,16 @@
1
+ import { discoverBrokerSocket, resolveManagedRoots } from './managed-roots.mjs';
1
2
  import { spawnSync } from "./background-process.mjs";
2
3
  import { createHash } from "node:crypto";
3
4
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync } from "node:fs";
4
5
  import path from "node:path";
5
- import { pathToFileURL } from "node:url";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
7
  import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
7
8
  import { assertReleaseExecutionAllowed } from "./execution-policy.mjs";
8
9
  import { acquireManagedRunLease, managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
9
10
  import { BUNDLE_MARKERS, bundleMarkerBuffer, readTauriBundleMarkers } from "./tauri-bundle-marker.mjs";
10
11
  import { installPortableDevelopment } from "./portable-development.mjs";
11
12
  import { signMacosDevelopmentBundle } from "./development-signing.mjs";
13
+ import { macosCodesignEnvironment } from "./codesign-environment.mjs";
12
14
 
13
15
  export const DEVELOPMENT_PROFILE = "release-iterate";
14
16
  const RELEASE_ONLY_APPLE_ENV = ["APPLE_ID", "APPLE_PASSWORD", "APPLE_API_KEY", "APPLE_API_ISSUER", "APPLE_API_KEY_PATH", "APPLE_CERTIFICATE", "APPLE_CERTIFICATE_PASSWORD"];
@@ -26,16 +28,14 @@ export function developmentEnvironment(env = process.env) {
26
28
  delete result.CARGO_INCREMENTAL;
27
29
  const client = managedRuntimeClient(result);
28
30
  if (client) {
29
- const managedRoot = result.RIGHTKIT_MANAGED_ROOT ?? result.RIGHTKIT_EXTERNAL_ROOT
30
- ?? (process.platform === "win32" ? "D:\\.rightkit-managed" : process.platform === "darwin" ? "/Volumes/D/.rightkit-managed" : undefined);
31
- const agentBin = managedRoot && path.join(managedRoot, "agent-bin");
31
+ const { agentBin } = resolveManagedRoots({ env: result });
32
32
  if (agentBin && existsSync(agentBin)) {
33
33
  const pathKey = Object.keys(result).find((key) => key.toLowerCase() === "path") ?? "PATH";
34
34
  result[pathKey] = [agentBin, result[pathKey]].filter(Boolean).join(path.delimiter);
35
35
  }
36
36
  // Same default as the agent-bin `rightkit` shim: children that run the managed client directly
37
37
  // (resolveCommand("rightkit"), build scripts) must not lose the broker endpoint.
38
- const socket = managedRoot && path.join(managedRoot, "broker.sock");
38
+ const socket = discoverBrokerSocket(result);
39
39
  if (!result.RIGHTKIT_BUILD_BROKER_SOCKET && socket && existsSync(socket)) result.RIGHTKIT_BUILD_BROKER_SOCKET = socket;
40
40
  }
41
41
  return result;
@@ -60,7 +60,8 @@ export function validateDevelopmentConfig(config, platform) {
60
60
  return target;
61
61
  }
62
62
 
63
- export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync }) {
63
+ export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync,
64
+ codesignEnvironment = macosCodesignEnvironment }) {
64
65
  if (!new Set(["win", "mac"]).has(platform)) throw new Error("--platform must be win or mac");
65
66
  assertNativeDevelopmentHost(platform);
66
67
  const target = validateDevelopmentConfig(config, platform);
@@ -68,7 +69,7 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
68
69
  assertPrimaryReleaseCheckout(repoRoot, env);
69
70
  if (dryRun) return { built: false, installed: false, plan: { build: target.build, install: buildOnly ? null : target.install ?? null, profile: DEVELOPMENT_PROFILE } };
70
71
  assertReleaseExecutionAllowed({ cwd: repoRoot, environment: env, command: `right-release dev --platform ${platform}` });
71
- const childEnv = developmentEnvironment(env);
72
+ const childEnv = codesignEnvironment(developmentEnvironment(env));
72
73
  // One lease spans every managed request this run makes, so a broker cutover cannot land between them.
73
74
  const lease = acquireManagedRunLease({ command: `right-release dev --platform ${platform}`, env });
74
75
  try {
@@ -210,7 +211,7 @@ export function resolveCommand(command, args, { root, env = process.env } = {})
210
211
  if (command === "rightkit") {
211
212
  const client = managedRuntimeClient(env);
212
213
  if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
213
- const socket = path.join(path.dirname(path.dirname(path.dirname(path.dirname(client)))), "broker.sock");
214
+ const socket = discoverBrokerSocket(env);
214
215
  const childEnv = !env.RIGHTKIT_BUILD_BROKER_SOCKET && existsSync(socket) ? { ...env, RIGHTKIT_BUILD_BROKER_SOCKET: socket } : undefined;
215
216
  return { command: process.execPath, args: [client, ...args], ...(childEnv ? { env: childEnv } : {}) };
216
217
  }
@@ -280,6 +281,7 @@ export async function main(args = process.argv.slice(2)) {
280
281
  return result;
281
282
  }
282
283
 
283
- if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
284
+ const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
285
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
284
286
  main().catch((error) => { console.error(`right-release dev: ${error.code ? `[${error.code}] ` : ""}${error.message}`); process.exitCode = 1; });
285
287
  }
@@ -1,5 +1,6 @@
1
+ import { readVerifiedArtifact } from "./publication-artifacts.mjs";
1
2
  import { createHash } from "node:crypto";
2
- import { mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
3
+ import { mkdirSync, readFileSync, rmSync, realpathSync, statSync, writeFileSync } from "node:fs";
3
4
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
5
  import { spawnSync } from "./background-process.mjs";
5
6
  import { RIGHTRELEASE_MANIFEST_SIGNER, signReleaseManifestWithAzure } from "./sign-release-manifest.mjs";
@@ -539,7 +540,8 @@ export function planBootstrapPublication({ product, bootstrapVersion, scriptPath
539
540
  if (stableKey !== `${product}/install.ps1`) throw new Error("bootstrap stable key must be product/install.ps1");
540
541
  const script = statSync(scriptPath);
541
542
  if (!script.isFile() || script.size < 1 || script.size > MAX_BOOTSTRAP_BYTES || scriptPath.split(/[\\/]/).at(-1) !== "install.ps1") throw new Error("bootstrap must be one small install.ps1 file");
542
- const validation = validatePowerShellBootstrap(readFileSync(scriptPath, "utf8"), { product });
543
+ const scriptBytes = readFileSync(scriptPath);
544
+ const validation = validatePowerShellBootstrap(scriptBytes.toString("utf8"), { product });
543
545
  if (!validation.valid) throw new Error("bootstrap script violates generated RightRelease contract: " + validation.errors.join("; "));
544
546
  const versionedKey = `${product}/versions/${bootstrapVersion}/install.ps1`;
545
547
  return {
@@ -549,7 +551,7 @@ export function planBootstrapPublication({ product, bootstrapVersion, scriptPath
549
551
  stableKey,
550
552
  versionedKey,
551
553
  scriptPath: resolve(scriptPath),
552
- sha256: sha256File(scriptPath),
554
+ sha256: createHash("sha256").update(scriptBytes).digest("hex"),
553
555
  };
554
556
  }
555
557
 
@@ -567,16 +569,24 @@ export function createWranglerR2Client({ commandRunner = spawnSync, wranglerRunn
567
569
 
568
570
  export function publishBootstrapPlan(plan, r2, { verificationPath }) {
569
571
  validateBootstrapPublicationPlan(plan);
570
- r2.put(plan.bucket, plan.versionedKey, plan.scriptPath);
571
- r2.get(plan.bucket, plan.versionedKey, verificationPath);
572
- if (sha256File(verificationPath) !== plan.sha256) throw new Error("immutable bootstrap remote verification failed");
573
- r2.put(plan.bucket, plan.stableKey, plan.scriptPath);
574
- r2.get(plan.bucket, plan.stableKey, verificationPath);
575
- if (sha256File(verificationPath) !== plan.sha256) throw new Error("stable bootstrap remote verification failed");
572
+ readVerifiedArtifact(plan.scriptPath, { sha256: plan.sha256 });
573
+ for (const [label, key] of [["immutable", plan.versionedKey], ["stable", plan.stableKey]]) {
574
+ try { r2.put(plan.bucket, key, plan.scriptPath); }
575
+ catch { throw new Error(`bootstrap upload failed; inspect remote key ${plan.bucket}/${key}`); }
576
+ // A successful readback must materialize fresh bytes, never reuse prior output.
577
+ rmSync(verificationPath, { force: true });
578
+ try {
579
+ r2.get(plan.bucket, key, verificationPath);
580
+ readVerifiedArtifact(verificationPath, { sha256: plan.sha256 });
581
+ } catch {
582
+ throw new Error(`${label} bootstrap remote verification failed; remove remote key ${plan.bucket}/${key}`);
583
+ }
584
+ }
576
585
  return { ...plan, verified: true };
577
586
  }
578
587
 
579
588
  export function validateBootstrapPublicationPlan(plan) {
589
+ if (!/^[a-f0-9]{64}$/.test(plan?.sha256 ?? "")) throw new Error("bootstrap publication hash evidence is required");
580
590
  if (!plan || !/^[a-z][a-z0-9-]+$/.test(plan.product ?? "")) throw new Error("bootstrap publication product is invalid");
581
591
  if (!VERSION.test(plan.bootstrapVersion ?? "")) throw new Error("bootstrap publication version is invalid");
582
592
  if (plan.bucket !== "rightapps-downloads") throw new Error("bootstrap publication bucket is not branded public R2");
package/dmg-build.mjs ADDED
@@ -0,0 +1,117 @@
1
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { spawnSync } from "./background-process.mjs";
5
+ import { backgroundAlias, dmgDSStore } from "./dmg-layout.mjs";
6
+ import { assertReleaseExecutionAllowed } from "./execution-policy.mjs";
7
+
8
+ const fail = (code, message) => Object.assign(new Error(message), { code });
9
+ const pair = (value, label, positive = false) => {
10
+ const numbers = Array.isArray(value) ? value : String(value).split(",").map(Number);
11
+ if (numbers.length !== 2 || numbers.some((n) => !Number.isInteger(n) || n < (positive ? 1 : 0) || n > 32767)) {
12
+ throw fail("DMG_INVALID_OPTIONS", `${label} requires ${positive ? "positive" : "nonnegative"} x,y integers <=32767`);
13
+ }
14
+ return numbers;
15
+ };
16
+
17
+ export function buildDmg({ app, out, volname, background, iconSize = 128, appPosition = [158, 231],
18
+ applicationsPosition = [492, 231], window = [660, 400], volumeIcon } = {},
19
+ { platform = process.platform, run = spawnSync, makeAlias = backgroundAlias,
20
+ log = (event) => console.error(JSON.stringify(event)) } = {}) {
21
+ if (platform !== "darwin") throw fail("DMG_NATIVE_HOST_REQUIRED", "DMG creation requires macOS");
22
+ if (!app || !out || !volname) throw fail("DMG_INVALID_OPTIONS", "--app, --out & --volname are required");
23
+ app = path.resolve(app); out = path.resolve(out);
24
+ if (!app.endsWith(".app") || !statSync(app).isDirectory() || !existsSync(path.join(app, "Contents", "Info.plist"))) {
25
+ throw fail("DMG_INVALID_OPTIONS", `expected .app bundle with Contents/Info.plist: ${app}`);
26
+ }
27
+ if (!out.endsWith(".dmg") || out.startsWith(`${app}${path.sep}`)) throw fail("DMG_INVALID_OPTIONS", "--out must be a .dmg outside app bundle");
28
+ if (/[\0\r\n/:]/.test(volname) || Buffer.byteLength(volname) > 255) throw fail("DMG_INVALID_OPTIONS", "invalid volume name");
29
+ iconSize = Number(iconSize);
30
+ if (!Number.isInteger(iconSize) || iconSize < 16 || iconSize > 512) throw fail("DMG_INVALID_OPTIONS", "--icon-size must be 16..512");
31
+ appPosition = pair(appPosition, "--app-position"); applicationsPosition = pair(applicationsPosition, "--applications-position");
32
+ window = pair(window, "--window", true);
33
+ if (volumeIcon) {
34
+ volumeIcon = path.resolve(volumeIcon);
35
+ if (!existsSync(volumeIcon) || !statSync(volumeIcon).isFile() || !/\.icns$/i.test(volumeIcon)) throw fail("DMG_INVALID_OPTIONS", "--volume-icon must be an .icns file");
36
+ }
37
+ if (background) {
38
+ background = path.resolve(background);
39
+ // Existing Right Suite backgrounds include TIFF; accept PNG & TIFF without conversion.
40
+ if (!statSync(background).isFile() || !/\.(png|tiff?)$/i.test(background)) throw fail("DMG_INVALID_OPTIONS", "background must be PNG or TIFF");
41
+ if (/\.png$/i.test(background) && !readFileSync(background).subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex"))) {
42
+ throw fail("DMG_INVALID_OPTIONS", "invalid PNG signature");
43
+ }
44
+ }
45
+ for (let cursor = out; cursor !== path.dirname(cursor); cursor = path.dirname(cursor)) {
46
+ if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink()) throw fail("DMG_INVALID_OPTIONS", `output must not traverse symlink: ${cursor}`);
47
+ }
48
+ assertReleaseExecutionAllowed({ cwd: app, command: "right-release dmg build" });
49
+ mkdirSync(path.dirname(out), { recursive: true });
50
+ const work = mkdtempSync(path.join(path.dirname(out), ".rightkit-dmg-")), stage = path.join(work, "stage"), mount = path.join(work, "mount");
51
+ const rw = path.join(work, "writable.dmg"), compressed = path.join(work, "compressed.dmg");
52
+ let attached = false, attaching = false;
53
+ const command = (tool, args) => {
54
+ const result = run(tool, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], shell: false, timeout: 300000 });
55
+ if (result.error || result.status !== 0) throw fail("DMG_COMMAND_FAILED", `${tool} ${args[0]} failed: ${result.error?.message ?? result.stderr ?? result.status}`);
56
+ return result;
57
+ };
58
+ log({ event: "dmg-build-started", app, out, volname });
59
+ try {
60
+ mkdirSync(stage); mkdirSync(mount);
61
+ command("/usr/bin/ditto", [app, path.join(stage, path.basename(app))]);
62
+ symlinkSync("/Applications", path.join(stage, "Applications"));
63
+ const relative = background ? path.join(".background", path.basename(background)) : null;
64
+ if (relative) { mkdirSync(path.join(stage, ".background")); copyFileSync(background, path.join(stage, relative)); }
65
+ // Include layout headroom in hdiutil's source-folder size estimate. Alias
66
+ // CNIDs become available only after mounting, when this reservation is replaced.
67
+ writeFileSync(path.join(stage, ".DS_Store"), Buffer.alloc(64 * 1024));
68
+ command("/usr/bin/hdiutil", ["create", "-fs", "HFS+", "-format", "UDRW", "-volname", volname, "-srcfolder", stage, rw]);
69
+ attaching = true;
70
+ command("/usr/bin/hdiutil", ["attach", "-readwrite", "-nobrowse", "-noautoopen", "-mountpoint", mount, rw]);
71
+ attached = true; attaching = false;
72
+ if (volumeIcon) {
73
+ // Custom volume icon: .VolumeIcon.icns at the root plus the volume's kHasCustomIcon (0x0400) Finder flag.
74
+ copyFileSync(volumeIcon, path.join(mount, ".VolumeIcon.icns"));
75
+ const finderInfo = Buffer.alloc(32); finderInfo.writeUInt16BE(0x0400, 8);
76
+ command("/usr/bin/xattr", ["-wx", "com.apple.FinderInfo", finderInfo.toString("hex"), mount]);
77
+ }
78
+ const alias = relative ? makeAlias({ mount, volname, relative }) : undefined;
79
+ writeFileSync(path.join(mount, ".DS_Store"), dmgDSStore({ appName: path.basename(app), iconSize,
80
+ appPosition, applicationsPosition, window, alias }));
81
+ command("/usr/bin/hdiutil", ["detach", mount]); attached = false;
82
+ command("/usr/bin/hdiutil", ["convert", rw, "-format", "UDZO", "-imagekey", "zlib-level=9", "-o", compressed]);
83
+ command("/usr/bin/hdiutil", ["verify", compressed]);
84
+ renameSync(compressed, out);
85
+ log({ event: "dmg-build-completed", out, format: "UDZO" });
86
+ return { out, format: "UDZO" };
87
+ } catch (error) {
88
+ log({ event: "dmg-build-failed", out, code: error.code ?? "DMG_BUILD_FAILED", message: error.message });
89
+ throw error;
90
+ } finally {
91
+ if (attached || attaching) {
92
+ try { command("/usr/bin/hdiutil", ["detach", "-force", mount]); attached = false; attaching = false; }
93
+ catch (error) { log({ event: "dmg-cleanup-failed", mount, work, message: error.message }); }
94
+ }
95
+ // Never recurse through a mount that failed to detach.
96
+ if (!attached && !attaching) rmSync(work, { recursive: true, force: true });
97
+ }
98
+ }
99
+
100
+ export function parseDmgArguments(args) {
101
+ if (args[0] !== "build") throw fail("DMG_INVALID_OPTIONS", "expected dmg build");
102
+ const flags = new Map([["--app", "app"], ["--out", "out"], ["--volname", "volname"], ["--background", "background"],
103
+ ["--icon-size", "iconSize"], ["--app-position", "appPosition"], ["--applications-position", "applicationsPosition"], ["--window", "window"], ["--volume-icon", "volumeIcon"]]);
104
+ const options = {};
105
+ for (let i = 1; i < args.length; i += 2) {
106
+ const key = flags.get(args[i]);
107
+ if (!key || options[key] !== undefined || !args[i + 1] || args[i + 1].startsWith("--")) throw fail("DMG_INVALID_OPTIONS", `invalid option: ${args[i]}`);
108
+ options[key] = args[i + 1];
109
+ }
110
+ return options;
111
+ }
112
+
113
+ const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
114
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
115
+ try { buildDmg(parseDmgArguments(process.argv.slice(2))); }
116
+ catch (error) { console.error(`right-release dmg: [${error.code ?? "DMG_BUILD_FAILED"}] ${error.message}`); process.exitCode = 1; }
117
+ }