@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
@@ -6,7 +6,34 @@ import { fileURLToPath } from "node:url";
6
6
  import { spawnSync } from "./background-process.mjs";
7
7
  import { signingAgentPaths, privatePath, SigningAgentError, MAX_FRAME } from "./rightkit-signing-agent.mjs";
8
8
 
9
- export async function requestSigningAgent(request, { paths = signingAgentPaths(), timeoutMs = 185000,
9
+ export function validateNotaryAgentReport(report) {
10
+ const fields = (value, allowed) => value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).every((key) => allowed.includes(key));
11
+ const uuid = (value) => typeof value === "string" && /^[a-f0-9-]{36}$/i.test(value);
12
+ const n = report?.notarization;
13
+ const summary = n?.logSummary;
14
+ if (!fields(report, ["schema", "ok", "id", "notarization", "steps", "stapling", "code", "message"])
15
+ || report.schema !== 1 || typeof report.ok !== "boolean" || !uuid(report.id)
16
+ || (report.code !== undefined && !["MAC_SIGNING_AGENT_TIMEOUT", "MAC_SIGNING_AGENT_JOB_FAILED", "MAC_SIGNING_NOTARIZATION_FAILED", "MAC_SIGNING_STAPLE_FAILED"].includes(report.code))
17
+ || (report.message !== undefined && !["Notarization deadline expired", "Notarization or stapling failed inside signing agent"].includes(report.message))
18
+ || !fields(n, ["id", "status", "logSummary"]) || !(n.id === null || uuid(n.id))
19
+ || !["Accepted", "Invalid", "Unknown", "In Progress", "Rejected"].includes(n.status)
20
+ || !(summary === null || (fields(summary, ["statusCode", "errors", "warnings", "issues"])
21
+ && (summary.statusCode === null || Number.isInteger(summary.statusCode))
22
+ && Number.isInteger(summary.errors) && summary.errors >= 0 && Number.isInteger(summary.warnings) && summary.warnings >= 0
23
+ && Array.isArray(summary.issues) && summary.issues.length <= 100 && summary.issues.every((issue) => fields(issue, ["severity", "code"])
24
+ && ["error", "warning", "unknown"].includes(issue.severity) && (issue.code === null || Number.isInteger(issue.code)))))
25
+ || !Array.isArray(report.steps) || report.steps.length > 10 || report.steps.some((step) => !fields(step, ["step", "file", "exitCode"])
26
+ || !["submit", "log", "probe", "staple", "validate"].includes(step.step)
27
+ || !(step.exitCode === null || (Number.isInteger(step.exitCode) && step.exitCode >= 0 && step.exitCode <= 255)))
28
+ || !Array.isArray(report.stapling) || report.stapling.length > 2 || report.stapling.some((item) => !fields(item, ["file", "alreadyStapled", "validated"])
29
+ || typeof item.file !== "string" || !path.isAbsolute(item.file) || typeof item.alreadyStapled !== "boolean" || item.validated !== true)) {
30
+ throw new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Invalid notarization agent report");
31
+ }
32
+ return report;
33
+ }
34
+
35
+ export async function requestSigningAgent(request, { paths = signingAgentPaths(), timeoutMs = (request.timeoutMs ?? 180000) + 5000,
36
+ onProgress = () => {},
10
37
  io = fs, connect = net.createConnection } = {}) {
11
38
  try {
12
39
  privatePath(paths.directory, 0o700, { io, kind: "directory" });
@@ -23,7 +50,7 @@ export async function requestSigningAgent(request, { paths = signingAgentPaths()
23
50
  if (frame.length > MAX_FRAME) throw new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Signing request is too large");
24
51
  return new Promise((resolve, reject) => {
25
52
  const socket = connect({ path: paths.socket });
26
- let input = Buffer.alloc(0), settled = false;
53
+ let input = Buffer.alloc(0), settled = false, jobId;
27
54
  const finish = (error, report) => {
28
55
  if (settled) return;
29
56
  settled = true; clearTimeout(timer); socket.destroy();
@@ -38,30 +65,53 @@ export async function requestSigningAgent(request, { paths = signingAgentPaths()
38
65
  socket.on("data", (chunk) => {
39
66
  input = Buffer.concat([input, chunk]);
40
67
  if (input.length > MAX_FRAME) { finish(new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Signing agent response is too large")); return; }
41
- const end = input.indexOf(10);
42
- if (end < 0) return;
43
68
  try {
44
- const report = JSON.parse(input.subarray(0, end).toString("utf8"));
45
- if (end !== input.length - 1 || report.schema !== 1 || typeof report.ok !== "boolean") throw new Error();
46
- if (!report.ok) {
47
- const code = typeof report.code === "string" && report.code.startsWith("MAC_SIGNING_") ? report.code : "MAC_SIGNING_AGENT_JOB_FAILED";
48
- finish(new SigningAgentError(code, request.type === "updater" ? "Updater signing failed inside signing agent" : String(report.message ?? "Signing agent rejected request")));
49
- return;
69
+ while (input.indexOf(10) >= 0 && !settled) {
70
+ const end = input.indexOf(10);
71
+ const report = JSON.parse(input.subarray(0, end).toString("utf8"));
72
+ input = input.subarray(end + 1);
73
+ const notary = ["notarize", "staple"].includes(request.type);
74
+ if (report.event) {
75
+ if (!notary || report.schema !== 1 || !["status", "heartbeat"].includes(report.event)
76
+ || typeof report.id !== "string" || !/^[a-f0-9-]{36}$/i.test(report.id)
77
+ || Object.keys(report).some((key) => !["schema", "event", "id", "step"].includes(key))
78
+ || (report.step !== undefined && !["submit", "log", "probe", "staple", "validate"].includes(report.step))) throw new Error();
79
+ if (jobId && jobId !== report.id) throw new Error();
80
+ jobId = report.id;
81
+ onProgress(report);
82
+ continue;
83
+ }
84
+ if (input.length || report.schema !== 1 || typeof report.ok !== "boolean") throw new Error();
85
+ if (notary && report.notarization) validateNotaryAgentReport(report);
86
+ if (notary && jobId && jobId !== report.id) throw new Error();
87
+ if (!report.ok) {
88
+ const code = typeof report.code === "string" && report.code.startsWith("MAC_SIGNING_") ? report.code : "MAC_SIGNING_AGENT_JOB_FAILED";
89
+ const error = new SigningAgentError(code, request.type === "updater" ? "Updater signing failed inside signing agent" : notary ? "Notarization or stapling failed inside signing agent" : String(report.message ?? "Signing agent rejected request"));
90
+ if (notary && report.notarization) error.report = report;
91
+ finish(error);
92
+ return;
93
+ }
94
+ if (notary) {
95
+ validateNotaryAgentReport(report);
96
+ } else if (request.type === "health") {
97
+ if (report.service !== "dev.rightkit.signing-agent" || !Number.isInteger(report.pid)) throw new Error();
98
+ } else if (request.type === "codesign-argv") {
99
+ if (!Array.isArray(report.results) || report.results.length !== 1
100
+ || report.results.some((r) => !Number.isInteger(r.status) || r.status < 0 || r.status > 255
101
+ || typeof r.stdout !== "string" || typeof r.stderr !== "string")) throw new Error();
102
+ } else if (!Array.isArray(report.results) || report.results.length !== (request.items?.length ?? request.files?.length)
103
+ || report.results.some((r) => r.status !== 0 || typeof r.stdout !== "string" || typeof r.stderr !== "string")
104
+ || (request.type === "updater" && report.results.some((r) => r.stdout || r.stderr))) throw new Error();
105
+ finish(null, report);
50
106
  }
51
- if (request.type === "health") {
52
- if (report.service !== "dev.rightkit.signing-agent" || !Number.isInteger(report.pid)) throw new Error();
53
- } else if (!Array.isArray(report.results) || report.results.length !== (request.items?.length ?? request.files?.length)
54
- || report.results.some((r) => r.status !== 0 || typeof r.stdout !== "string" || typeof r.stderr !== "string")
55
- || (request.type === "updater" && report.results.some((r) => r.stdout || r.stderr))) throw new Error();
56
- finish(null, report);
57
107
  } catch { finish(new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Invalid signing agent response")); }
58
108
  });
59
109
  });
60
110
  }
61
- export function requestSigningAgentSync(request, { runClient = spawnSync, home, timeoutMs = 185000 } = {}) {
111
+ export function requestSigningAgentSync(request, { runClient = spawnSync, home, timeoutMs = (request.timeoutMs ?? 180000) + 5000 } = {}) {
62
112
  // Existing signing call sites are synchronous. Only typed JSON goes to this
63
113
  // child's stdin; credentials & authentication token never enter argv/env.
64
- const result = runClient(process.execPath, [fileURLToPath(import.meta.url)], { encoding: "utf8", stdio: "pipe", shell: false,
114
+ const result = runClient(process.execPath, [fileURLToPath(import.meta.url)], { encoding: "utf8", stdio: ["pipe", "pipe", "inherit"], shell: false,
65
115
  input: JSON.stringify({ request, home, timeoutMs }), timeout: timeoutMs + 1000, maxBuffer: MAX_FRAME,
66
116
  env: { PATH: process.env.PATH, HOME: process.env.HOME } });
67
117
  let report;
@@ -69,17 +119,23 @@ export function requestSigningAgentSync(request, { runClient = spawnSync, home,
69
119
  throw new SigningAgentError(result.error?.code === "ETIMEDOUT" ? "MAC_SIGNING_AGENT_TIMEOUT" : "MAC_SIGNING_AGENT_CONNECTION", "Signing agent client failed");
70
120
  }
71
121
  if (report.schema !== 1 || typeof report.ok !== "boolean") throw new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Invalid signing agent client report");
72
- if (!report.ok) throw new SigningAgentError(report.code ?? "MAC_SIGNING_AGENT_JOB_FAILED", report.message ?? "Signing agent failed");
122
+ if (!report.ok) {
123
+ const error = new SigningAgentError(report.code ?? "MAC_SIGNING_AGENT_JOB_FAILED", report.message ?? "Signing agent failed");
124
+ if (["notarize", "staple"].includes(request.type) && report.notarization) error.report = validateNotaryAgentReport(report);
125
+ throw error;
126
+ }
73
127
  if (result.error || result.status !== 0) throw new SigningAgentError("MAC_SIGNING_AGENT_CONNECTION", "Signing agent client failed");
74
128
  return report;
75
129
  }
76
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
130
+ const realOrSelf = (file) => { try { return fs.realpathSync(file); } catch { return path.resolve(file); } };
131
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
77
132
  try {
78
133
  const { request, home, timeoutMs } = JSON.parse(fs.readFileSync(0, "utf8"));
79
- const report = await requestSigningAgent(request, { paths: signingAgentPaths(home), timeoutMs });
134
+ const report = await requestSigningAgent(request, { paths: signingAgentPaths(home), timeoutMs,
135
+ onProgress: (frame) => process.stderr.write(`right-release signing-agent: ${frame.id} ${frame.step ?? frame.event}\n`) });
80
136
  process.stdout.write(JSON.stringify(report));
81
137
  } catch (error) {
82
- process.stdout.write(JSON.stringify({ schema: 1, ok: false, code: error.code ?? "MAC_SIGNING_AGENT_PROTOCOL", message: error.message }));
138
+ process.stdout.write(JSON.stringify(error.report ?? { schema: 1, ok: false, code: error.code ?? "MAC_SIGNING_AGENT_PROTOCOL", message: error.message }));
83
139
  process.exitCode = 1;
84
140
  }
85
141
  }
@@ -5,10 +5,11 @@ const [command, ...args] = process.argv.slice(2);
5
5
  try {
6
6
  let result;
7
7
  if (command === "install") {
8
- const options = {};
9
- if (args.length) {
10
- if (args.length !== 2 || args[0] !== "--managed-root" || !args[1]) throw new TypeError("Expected install [--managed-root <absolute-directory>]");
11
- options.managedRoot = args[1];
8
+ const options = { cargoTargetDirs: [] };
9
+ for (let i = 0; i < args.length; i += 2) {
10
+ if (!["--managed-root", "--cargo-target-dir"].includes(args[i]) || !args[i + 1]) throw new TypeError("Expected install [--managed-root <directory>] [--cargo-target-dir <directory>]...");
11
+ if (args[i] === "--managed-root") options.managedRoot = args[i + 1];
12
+ else options.cargoTargetDirs.push(args[i + 1]);
12
13
  }
13
14
  result = installSigningAgent(options);
14
15
  } else if (command === "uninstall" && !args.length) result = uninstallSigningAgent();
@@ -1,3 +1,4 @@
1
+ import { resolveManagedRoots } from './managed-roots.mjs';
1
2
  import * as fs from "node:fs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
@@ -6,6 +7,8 @@ import { AGENT_LABEL, signingAgentPaths, defaultSigningRoots, ensurePrivateDirec
6
7
  import { requestSigningAgentSync } from "./signing-agent-client.mjs";
7
8
 
8
9
  const capture = { encoding: "utf8", stdio: "pipe", shell: false, timeout: 10000 };
10
+ function sleepMs(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
11
+
9
12
  export function requireMacGuiSession({ run = spawnSync, uid = process.getuid?.() } = {}) {
10
13
  const session = run("/bin/launchctl", ["print", `gui/${uid}`], capture);
11
14
  if (session.error || session.status !== 0) throw new SigningAgentError("MAC_SIGNING_NO_GUI_SESSION", `No logged-in Aqua session for uid ${uid}`);
@@ -27,7 +30,7 @@ export function signingAgentPlist({ paths, node = process.execPath, home }) {
27
30
  }
28
31
  export function installSigningAgent({ home = os.userInfo().homedir, username = os.userInfo().username,
29
32
  uid = process.getuid?.(), platform = process.platform, run = spawnSync, io = fs, node = process.execPath,
30
- managedRoot = "/Volumes/D/.rightkit-managed", health = requestSigningAgentSync,
33
+ managedRoot = resolveManagedRoots({ home }).dataRoot, cargoTargetDirs = [], health = requestSigningAgentSync,
31
34
  now = Date.now, sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms) } = {}) {
32
35
  if (platform !== "darwin") throw new SigningAgentError("MAC_SIGNING_AGENT_PLATFORM", "Signing agent requires macOS");
33
36
  requireMacGuiSession({ run, uid });
@@ -41,14 +44,26 @@ export function installSigningAgent({ home = os.userInfo().homedir, username = o
41
44
  const pnpm = String(found.stdout ?? "").trim();
42
45
  if (found.error || found.status !== 0 || !path.isAbsolute(pnpm) || pnpm.includes("\n")) throw new SigningAgentError("MAC_SIGNING_AGENT_INSTALL_FAILED", "Provisioned pnpm executable is unavailable");
43
46
  // Resident runtime is self-contained; no package/checkout imports after install.
44
- for (const name of ["rightkit-signing-agent.mjs", "background-process.mjs"]) {
47
+ for (const root of cargoTargetDirs) {
48
+ if (typeof root !== "string" || !path.isAbsolute(root) || ["/", home, "/Volumes", "/Volumes/D"].includes(path.resolve(root))) throw new SigningAgentError("MAC_SIGNING_AGENT_PATH_DENIED", "Cargo target root must be a dedicated absolute directory");
49
+ }
50
+ for (const name of ["rightkit-signing-agent.mjs", "background-process.mjs", "codesign-argv.mjs", "managed-roots.mjs"]) {
45
51
  const target = path.join(paths.runtime, name);
46
52
  if (io.existsSync(target)) privatePath(target, 0o600, { io, uid });
47
53
  io.writeFileSync(target, io.readFileSync(new URL(`./${name}`, import.meta.url)), { mode: 0o600 });
48
54
  }
49
55
  if (io.existsSync(paths.config)) privatePath(paths.config, 0o600, { io, uid });
56
+ // SSH & Aqua can inherit different TMPDIR spellings. Include both aliases of
57
+ // this uid's Darwin temp root, since bundlers can canonicalize temporary paths.
58
+ const temp = run("/usr/bin/getconf", ["DARWIN_USER_TEMP_DIR"], capture);
59
+ const directory = String(temp.stdout ?? "").trim();
60
+ const temporaryRoots = [];
61
+ if (!temp.error && temp.status === 0 && path.isAbsolute(directory) && !directory.includes("\n")) {
62
+ temporaryRoots.push(path.resolve(directory));
63
+ try { temporaryRoots.push((io.realpathSync.native ?? io.realpathSync)(directory)); } catch {}
64
+ }
50
65
  io.writeFileSync(paths.config, JSON.stringify({ schema: 1, home, username, pnpm,
51
- roots: defaultSigningRoots(home, path.resolve(managedRoot)),
66
+ roots: defaultSigningRoots(home, path.resolve(managedRoot), [...cargoTargetDirs.map((root) => path.resolve(root)), ...temporaryRoots]),
52
67
  environmentPath: [...new Set([path.dirname(node), path.dirname(pnpm), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])].join(":") }), { mode: 0o600 });
53
68
  ensurePrivateDirectory(path.dirname(paths.plist), { io, uid });
54
69
  if (io.existsSync(paths.plist)) privatePath(paths.plist, 0o600, { io, uid });
@@ -57,8 +72,20 @@ export function installSigningAgent({ home = os.userInfo().homedir, username = o
57
72
  if (!loaded.error && loaded.status === 0) {
58
73
  const stopped = run("/bin/launchctl", ["bootout", `gui/${uid}/${AGENT_LABEL}`], capture);
59
74
  if (stopped.error || stopped.status !== 0) throw new SigningAgentError("MAC_SIGNING_AGENT_INSTALL_FAILED", "Could not stop existing signing agent");
75
+ // bootout is asynchronous: wait until launchd has released the label before bootstrapping again.
76
+ const gone = now() + 10000;
77
+ while (now() < gone) {
78
+ const still = run("/bin/launchctl", ["print", `gui/${uid}/${AGENT_LABEL}`], capture);
79
+ if (still.error || still.status !== 0) break;
80
+ sleepMs(100);
81
+ }
82
+ }
83
+ let boot = run("/bin/launchctl", ["bootstrap", `gui/${uid}`, paths.plist], capture);
84
+ // Exit 5 (EIO) is launchd still tearing the old job down; retry briefly.
85
+ for (let attempt = 0; attempt < 20 && !boot.error && boot.status === 5; attempt++) {
86
+ sleepMs(250);
87
+ boot = run("/bin/launchctl", ["bootstrap", `gui/${uid}`, paths.plist], capture);
60
88
  }
61
- const boot = run("/bin/launchctl", ["bootstrap", `gui/${uid}`, paths.plist], capture);
62
89
  if (boot.error || boot.status !== 0) {
63
90
  requireMacGuiSession({ run, uid });
64
91
  throw new SigningAgentError("MAC_SIGNING_AGENT_INSTALL_FAILED", `Signing agent bootstrap failed (exit ${boot.status ?? "unavailable"})`);
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, realpathSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawnSync } from "./background-process.mjs";
@@ -140,7 +140,8 @@ function parseArgs(argv) {
140
140
  return { apps, evidencePath };
141
141
  }
142
142
 
143
- if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
143
+ const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
144
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
144
145
  try {
145
146
  const evidence = await verifyStandaloneClones(parseArgs(process.argv.slice(2)));
146
147
  for (const app of evidence.apps) process.stdout.write(`[standalone] ${app.key} ${app.revision.slice(0, 12)} install+doctor passed\n`);
@@ -10,7 +10,7 @@
10
10
  // a byte changing outside the marker all mean the assumption no longer holds.
11
11
 
12
12
  import { createHash } from "node:crypto";
13
- import { readFileSync, writeFileSync } from "node:fs";
13
+ import { readFileSync, writeFileSync, realpathSync } from "node:fs";
14
14
  import path from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
 
@@ -147,7 +147,8 @@ function sha256(bytes) {
147
147
  return createHash("sha256").update(bytes).digest("hex");
148
148
  }
149
149
 
150
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
150
+ const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
151
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
151
152
  const argv = process.argv.slice(2);
152
153
  const bundleIndex = argv.indexOf("--bundle");
153
154
  const bundle = bundleIndex >= 0 ? argv[bundleIndex + 1] : "nsis";
package/upload-large.mjs CHANGED
@@ -1,86 +1,40 @@
1
1
  #!/usr/bin/env node
2
- import { accessSync, statSync } from "node:fs";
2
+ import { readFileSync } 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
-
6
- const ACCOUNT_ID = "03ae77ccd7a07bcbb2dcfde47fa7ba3a";
7
- const BUCKETS = new Map([
8
- ["public", "rightapps-downloads"],
9
- ["private", "rightapps-updates"],
10
- ]);
11
- const RUNNER = { cmd: "pnpm", prefix: ["dlx"] };
12
-
13
- const [, , localFile, r2Key, bucketAlias = "public"] = process.argv;
14
- if (!localFile || !r2Key) {
15
- console.error("Usage: node upload-large.mjs <localFile> <r2Key> [public|private]");
16
- process.exit(1);
17
- }
18
-
19
- const bucket = BUCKETS.get(bucketAlias);
20
- if (!bucket) {
21
- console.error(`upload-large: invalid bucket alias ${bucketAlias}; expected public|private`);
22
- process.exit(1);
23
- }
24
-
25
- try {
26
- accessSync(localFile);
27
- } catch {
28
- console.error(`upload-large: missing local file: ${localFile}`);
29
- process.exit(1);
30
- }
31
-
32
- const fileSize = statSync(localFile).size;
33
- const object = `${bucket}/${r2Key}`;
34
- const wranglerArgs = ["wrangler@4", "r2", "object", "put", object, "--file", path.resolve(localFile), "--remote"];
35
- const env = cleanCloudflareEnv(process.env);
36
-
37
- console.log(`Uploading ${path.basename(localFile)} (${(fileSize / 1024 / 1024).toFixed(1)} MB) -> ${object}`);
38
- console.log(`wrangler r2 object put ${object} --file ${path.resolve(localFile)} --remote`);
39
-
40
- // Resolve the package runner before the dry-run exit: a release box without one cannot upload,
41
- // and that must fail here rather than after a signed build has already been produced.
42
- const runner = resolveRunner(env);
43
- if (!runner) {
44
- console.error(
45
- "upload-large: no package runner found. Tried: " +
46
- "pnpm dlx. Install the pinned workspace package manager before release.",
47
- );
48
- process.exit(1);
49
- }
50
- console.log(`runner: ${[runner.cmd, ...runner.prefix].join(" ")}`);
51
-
52
- if (process.env.RIGHT_RELEASE_UPLOAD_DRY_RUN === "1") {
53
- process.exit(0);
54
- }
55
-
56
- const result = spawnSync(runner.cmd, [...runner.prefix, ...wranglerArgs], {
57
- env,
58
- stdio: "inherit",
59
- shell: process.platform === "win32",
60
- windowsHide: true,
61
- });
62
-
63
- if (result.error) {
64
- console.error(`upload-large: failed to start wrangler: ${result.error.message}`);
65
- process.exit(1);
66
- }
67
- process.exit(result.status ?? 1);
68
-
69
- function resolveRunner(env) {
70
- const probe = spawnSync(RUNNER.cmd, ["--version"], {
71
- env,
72
- stdio: "ignore",
73
- shell: process.platform === "win32",
74
- windowsHide: true,
75
- });
76
- return !probe.error && probe.status === 0 ? RUNNER : null;
77
- }
78
-
79
- function cleanCloudflareEnv(source) {
80
- const env = { ...source, CLOUDFLARE_ACCOUNT_ID: source.CLOUDFLARE_ACCOUNT_ID || ACCOUNT_ID };
81
- delete env.CLOUDFLARE_API_KEY;
82
- delete env.CLOUDFLARE_EMAIL;
83
- delete env.CF_API_KEY;
84
- delete env.CF_EMAIL;
85
- return env;
6
+ import { verifySealedRelease } from "./release-state.mjs";
7
+ import { uploadVerifiedR2File } from "./r2-verified-upload.mjs";
8
+ import { resolveCloudflareApiToken } from "./protected-credentials.mjs";
9
+
10
+ export function runLargeUpload(argv, { run = spawnSync, env = process.env, credentialStore } = {}) {
11
+ const [localFile, r2Key, bucketAlias = "public", flag, evidencePath, ...extra] = argv;
12
+ if (!localFile || !r2Key) throw new Error("Usage: upload-large <file> <key> public|private --evidence <json> | --seal <sealed-dir>");
13
+ if (!["public", "private"].includes(bucketAlias)) throw new Error(`invalid bucket alias ${bucketAlias}; expected public|private`);
14
+ if (!["--evidence", "--seal"].includes(flag) || !evidencePath || extra.length) throw new Error("publication seal or hash evidence is required (--seal or --evidence)");
15
+ let evidence;
16
+ if (flag === "--seal") {
17
+ const sealed = verifySealedRelease(path.resolve(evidencePath));
18
+ if (path.dirname(path.resolve(localFile)) !== path.resolve(sealed.sealedDir)) throw new Error("artifact is outside sealed directory");
19
+ evidence = sealed.manifest.files.find((entry) => entry.name === path.basename(localFile));
20
+ } else {
21
+ const value = JSON.parse(readFileSync(evidencePath, "utf8"));
22
+ evidence = { sha256: value.artifactSha256, sizeBytes: value.sizeBytes };
23
+ }
24
+ // Validate before runner discovery or any remote operation, including dry runs.
25
+ uploadVerifiedR2File(localFile, evidence, r2Key, bucketAlias, { dryRun: true });
26
+ // The Cloudflare token is resolved only for a real upload; it is injected into the Wrangler child environment alone.
27
+ const dryRun = env.RIGHT_RELEASE_UPLOAD_DRY_RUN === "1";
28
+ const token = resolveCloudflareApiToken({ env, credentialStore, required: !dryRun });
29
+ const uploadEnv = dryRun ? env : { ...env, CLOUDFLARE_API_TOKEN: token };
30
+ const probe = run("pnpm", ["--version"], { env, stdio: "ignore", shell: process.platform === "win32", windowsHide: true });
31
+ if (probe.error || probe.status !== 0) throw new Error("no package runner found: pnpm dlx");
32
+ const object = `${bucketAlias === "public" ? "rightapps-downloads" : "rightapps-updates"}/${r2Key}`;
33
+ console.log(`wrangler r2 object put ${object} --remote (mandatory hash readback)`);
34
+ console.log("runner: pnpm dlx");
35
+ return uploadVerifiedR2File(localFile, evidence, r2Key, bucketAlias, { run, env: uploadEnv, dryRun });
36
+ }
37
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
38
+ try { runLargeUpload(process.argv.slice(2)); }
39
+ catch (error) { console.error(`upload-large: ${error.message}`); process.exitCode = 1; }
86
40
  }
package/verifier.mjs CHANGED
@@ -13,6 +13,9 @@
13
13
  * human nor a model can hand-edit its way to a grant.
14
14
  */
15
15
  import { createHash } from "node:crypto";
16
+ import { realpathSync } from "node:fs";
17
+ import path from "node:path";
18
+ import { fileURLToPath } from "node:url";
16
19
 
17
20
  export const GRANTS = Object.freeze(["BUILD_AUTHORIZED", "SIGNING_AUTHORIZED", "RELEASE_AUTHORIZED"]);
18
21
 
@@ -136,7 +139,8 @@ function decision(grant, granted, issues, input) {
136
139
  return body;
137
140
  }
138
141
 
139
- const invokedDirectly = process.argv[1] && process.argv[1].endsWith("verifier.mjs");
142
+ const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
143
+ const invokedDirectly = process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url));
140
144
  if (invokedDirectly) {
141
145
  const grant = process.argv[2];
142
146
  let input = {};