@rightkit/release 0.2.134 → 0.2.135

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.
@@ -11,6 +11,7 @@ import {
11
11
  writeFileSync,
12
12
  } from "node:fs";
13
13
  import path from "node:path";
14
+ import os from "node:os";
14
15
 
15
16
  export const ARTIFACT_MARKER_SCHEMA = 1;
16
17
  export const DEFAULT_MANAGED_ROOT = process.platform === "win32"
@@ -19,9 +20,12 @@ export const DEFAULT_MANAGED_ROOT = process.platform === "win32"
19
20
 
20
21
  const SAFE_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/;
21
22
 
22
- export function resolveManagedRoot({ env = process.env } = {}) {
23
- const value = env.RIGHTKIT_MANAGED_ROOT || DEFAULT_MANAGED_ROOT;
24
- return path.resolve(value);
23
+ export function resolveManagedRoot({ env = process.env, exists = existsSync } = {}) {
24
+ if (env.RIGHTKIT_MANAGED_ROOT) return path.resolve(env.RIGHTKIT_MANAGED_ROOT);
25
+ // Workstation default only when its drive exists; hosted CI runners fall back to RUNNER_TEMP, then the system temp dir.
26
+ const volume = process.platform === "win32" ? "D:\\" : "/Volumes/D";
27
+ if (exists(volume)) return path.resolve(DEFAULT_MANAGED_ROOT);
28
+ return path.join(env.RUNNER_TEMP && exists(env.RUNNER_TEMP) ? env.RUNNER_TEMP : os.tmpdir(), "rightkit-managed");
25
29
  }
26
30
 
27
31
  export function resolveArtifactRoot({ app, managedRoot } = {}) {
package/build-release.mjs CHANGED
@@ -39,13 +39,14 @@ import { acquireHeavyWorkSlot, heavyCommandEnvironment, summarizePreBrokerWaits,
39
39
  import { processGroupCpuSeconds } from "./process-liveness.mjs";
40
40
  import { cancelAndReapManagedRequest, firstManagedRequestId, releaseProgressLimits } from "./progress-control.mjs";
41
41
  import { beginArtifactLease, completeArtifactLease, materializeArtifacts, retainArtifacts, resolveArtifactDir, resolveManagedRoot } from "./artifact-storage.mjs";
42
+ import { macosCodesignEnvironment } from "./codesign-environment.mjs";
42
43
 
43
44
  // Fingerprint of the pipeline code that can change the bytes we ship or the way
44
45
  // they are signed — deliberately NOT the package version, which moves for docs
45
46
  // and test-only edits and would force a cold Rust rebuild of every app each time.
46
47
  // Cached target directories must not outlive changes to these signing helpers.
47
48
 
48
- const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
49
+ const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "codesign-environment.mjs", "codesign-shim.mjs", "codesign-argv.mjs", "rightkit-signing-agent.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
49
50
  const PIPELINE_FINGERPRINT = createHash("sha256")
50
51
  .update(PIPELINE_FINGERPRINT_SOURCES.map((file) => `${file}:${createHash("sha256").update(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), file))).digest("hex")}`).join("\n"))
51
52
  .digest("hex")
@@ -251,7 +252,7 @@ try {
251
252
  // resolves to MSYS Perl, which cannot configure OpenSSL. Put the real Windows
252
253
  // Perl first so the build behaves the same from any shell.
253
254
  const pathPrefix = buildPathPrefix({ platform });
254
- const env = {
255
+ const env = macosCodesignEnvironment({
255
256
  ...heavyCommandEnvironment(process.env),
256
257
  ...releaseEnvironment({ root: vaultRoot, cacheRoot: sharedCacheRoot, platform, architecture: cacheIdentity.architecture, app: config.app, cacheKey, kind: "release", appRoot, mode: cacheMode }),
257
258
  ...(pathPrefix ? { PATH: `${pathPrefix}${path.delimiter}${process.env.PATH ?? ""}` } : {}),
@@ -259,7 +260,7 @@ try {
259
260
  RIGHT_RELEASE_APP_ROOT: layout.appRoot,
260
261
  RIGHTKIT_MANAGED_ROOT: resolveManagedRoot({ env: process.env }),
261
262
  RIGHT_RELEASE_ARTIFACT_DIR: resolveArtifactDir({ app: config.app, version: config.version, managedRoot: resolveManagedRoot({ env: process.env }) }),
262
- };
263
+ });
263
264
  if (managedCargoTarget) {
264
265
  for (const name of ["CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC_WRAPPER", "SCCACHE_DIR", "SCCACHE_BASEDIRS", "SCCACHE_CACHE_SIZE", "RUSTFLAGS"]) {
265
266
  delete env[name];
@@ -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,34 @@
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
+ // Immutable wrappers avoid races between GUI & SSH builds or package versions.
24
+ const identity = createHash("sha256").update(script).digest("hex").slice(0, 24);
25
+ const bin = path.join(resolveManagedRoot({ env }), "signing", "codesign-bin", identity);
26
+ ensurePrivateDirectory(bin, { io });
27
+ const file = path.join(bin, "codesign");
28
+ if (io.existsSync(file)) {
29
+ privatePath(file, 0o700, { io });
30
+ if (io.readFileSync(file, "utf8") !== script) throw new Error("Managed codesign shim content changed");
31
+ } else io.writeFileSync(file, script, { mode: 0o700, flag: "wx" });
32
+ // Keep every other shim (especially managed Cargo) in its original order.
33
+ return { ...env, PATH: [bin, ...(env.PATH ?? "").split(path.delimiter).filter((entry) => entry !== bin)].join(path.delimiter) };
34
+ }
@@ -0,0 +1,50 @@
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
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
39
+ try {
40
+ // Only the managed shell wrapper invokes this entry: session detection was
41
+ // already performed by right-release before selecting its wrapper directory.
42
+ const result = await runCodesignShim(process.argv.slice(2), { detect: () => ({ guiRunner: true }) });
43
+ process.stdout.write(result.stdout);
44
+ process.stderr.write(result.stderr);
45
+ process.exitCode = result.status;
46
+ } catch (error) {
47
+ process.stderr.write(`rightkit codesign: [${error.code ?? "MAC_SIGNING_AGENT_JOB_FAILED"}] ${error.message}\n`);
48
+ process.exitCode = 1;
49
+ }
50
+ }
package/development.mjs CHANGED
@@ -9,6 +9,7 @@ import { acquireManagedRunLease, managedRuntimeClient, runCargoSync } from "./ma
9
9
  import { BUNDLE_MARKERS, bundleMarkerBuffer, readTauriBundleMarkers } from "./tauri-bundle-marker.mjs";
10
10
  import { installPortableDevelopment } from "./portable-development.mjs";
11
11
  import { signMacosDevelopmentBundle } from "./development-signing.mjs";
12
+ import { macosCodesignEnvironment } from "./codesign-environment.mjs";
12
13
 
13
14
  export const DEVELOPMENT_PROFILE = "release-iterate";
14
15
  const RELEASE_ONLY_APPLE_ENV = ["APPLE_ID", "APPLE_PASSWORD", "APPLE_API_KEY", "APPLE_API_ISSUER", "APPLE_API_KEY_PATH", "APPLE_CERTIFICATE", "APPLE_CERTIFICATE_PASSWORD"];
@@ -60,7 +61,8 @@ export function validateDevelopmentConfig(config, platform) {
60
61
  return target;
61
62
  }
62
63
 
63
- export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync }) {
64
+ export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync,
65
+ codesignEnvironment = macosCodesignEnvironment }) {
64
66
  if (!new Set(["win", "mac"]).has(platform)) throw new Error("--platform must be win or mac");
65
67
  assertNativeDevelopmentHost(platform);
66
68
  const target = validateDevelopmentConfig(config, platform);
@@ -68,7 +70,7 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
68
70
  assertPrimaryReleaseCheckout(repoRoot, env);
69
71
  if (dryRun) return { built: false, installed: false, plan: { build: target.build, install: buildOnly ? null : target.install ?? null, profile: DEVELOPMENT_PROFILE } };
70
72
  assertReleaseExecutionAllowed({ cwd: repoRoot, environment: env, command: `right-release dev --platform ${platform}` });
71
- const childEnv = developmentEnvironment(env);
73
+ const childEnv = codesignEnvironment(developmentEnvironment(env));
72
74
  // One lease spans every managed request this run makes, so a broker cutover cannot land between them.
73
75
  const lease = acquireManagedRunLease({ command: `right-release dev --platform ${platform}`, env });
74
76
  try {
@@ -1,8 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { spawnSync } from "./background-process.mjs";
5
5
  import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
6
+ import { detectMacSigningSession, runMacGuiNotarization } from "./macos-signing-session.mjs";
7
+ import { notarytoolAuthArgs } from "./notary-auth.mjs";
6
8
 
7
9
  function checkedRun(args, run) {
8
10
  const result = run("xcrun", args, { encoding: "utf8" });
@@ -10,9 +12,13 @@ function checkedRun(args, run) {
10
12
  return result.stdout ?? "";
11
13
  }
12
14
 
13
- export function stapleMacosArtifact(file, { run = spawnSync } = {}) {
15
+ export function stapleMacosArtifact(file, options = {}) {
16
+ const { run = spawnSync, env = process.env } = options;
14
17
  file = path.resolve(file);
15
18
  if (!/\.(dmg|pkg|app)$/i.test(file)) throw new Error(`not a stapleable macOS artifact: ${file}`);
19
+ if (detectMacSigningSession({ env, run }).guiRunner) {
20
+ return { ...runMacGuiNotarization({ ...options, kind: "staple", file }).stapling[0], file };
21
+ }
16
22
  const probe = run("xcrun", ["stapler", "validate", file], { encoding: "utf8" });
17
23
  if (probe.error) throw new Error(`cannot validate macOS staple: ${probe.error.message}`);
18
24
  const alreadyStapled = probe.status === 0;
@@ -27,7 +33,7 @@ function fileEvidence(file) {
27
33
 
28
34
  // DMG/PKG receive their own ticket. For a separately submitted app ZIP,
29
35
  // target.notarize.app (CLI --app) identifies the bundle to staple before packaging.
30
- export function notarizeMacosArtifact({ archive, receiptPath, authArgs, app, run = spawnSync }) {
36
+ export function notarizeMacosArtifact({ archive, receiptPath, authArgs, profile, app, run = spawnSync, env = process.env, ...agentOptions }) {
31
37
  archive = path.resolve(archive);
32
38
  const stapleTargets = /\.(dmg|pkg)$/i.test(archive) ? [archive] : [];
33
39
  if (app) {
@@ -36,11 +42,21 @@ export function notarizeMacosArtifact({ archive, receiptPath, authArgs, app, run
36
42
  stapleTargets.push(bundle);
37
43
  }
38
44
  if (!stapleTargets.length) throw new Error("notarization archives require notarize.app (or --app) for stapling their app bundle");
39
- let result;
40
- try { result = JSON.parse(checkedRun(["notarytool", "submit", archive, "--wait", "--output-format", "json", ...authArgs], run)); }
41
- catch (error) { throw new Error(`macOS notarization failed for ${archive}: ${error.message}`); }
42
- if (result.status !== "Accepted") throw new Error(`notarization was not accepted for ${archive}: ${result.status ?? "missing status"}`);
43
- const stapling = [...new Set(stapleTargets)].map((file) => stapleMacosArtifact(file, { run }));
45
+ let result, stapling;
46
+ if (detectMacSigningSession({ env, run }).guiRunner) {
47
+ // SSH ignores API credential environment/argv entirely. Only profile name
48
+ // reaches Aqua, where Keychain authentication already works.
49
+ const selectedProfile = profile ?? (authArgs?.[0] === "--keychain-profile" && authArgs.length === 2 ? authArgs[1] : undefined);
50
+ const report = runMacGuiNotarization({ ...agentOptions, kind: "notarize", file: archive, app, profile: selectedProfile, env, run });
51
+ result = report.notarization;
52
+ stapling = report.stapling.map((item) => ({ ...item, file: stapleTargets.find((file) => realpathSync.native(file) === item.file) ?? item.file }));
53
+ } else {
54
+ authArgs ??= notarytoolAuthArgs({ profile, env });
55
+ try { result = JSON.parse(checkedRun(["notarytool", "submit", archive, "--wait", "--output-format", "json", ...authArgs], run)); }
56
+ catch (error) { throw new Error(`macOS notarization failed for ${archive}: ${error.message}`); }
57
+ if (result.status !== "Accepted") throw new Error(`notarization was not accepted for ${archive}: ${result.status ?? "missing status"}`);
58
+ stapling = [...new Set(stapleTargets)].map((file) => stapleMacosArtifact(file, { run, env }));
59
+ }
44
60
  // Stapling changes container bytes. Record final bytes, never the submission digest.
45
61
  const receipt = {
46
62
  schema: 1,
@@ -3,9 +3,9 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawnSync } from "./background-process.mjs";
5
5
 
6
- import { requestSigningAgentSync } from "./signing-agent-client.mjs";
6
+ import { requestSigningAgentSync, validateNotaryAgentReport } from "./signing-agent-client.mjs";
7
7
  import { installSigningAgent, requireMacGuiSession } from "./signing-agent-install.mjs";
8
- import { signingAgentPaths, defaultSigningRoots, ensurePrivateDirectory, privatePath, within } from "./rightkit-signing-agent.mjs";
8
+ import { signingAgentPaths, defaultSigningRoots, ensurePrivateDirectory, privatePath, within, NOTARIZATION_TIMEOUT_MS, MAX_NOTARIZATION_TIMEOUT_MS } from "./rightkit-signing-agent.mjs";
9
9
 
10
10
  export class MacSigningSessionError extends Error {
11
11
  constructor(code, message) {
@@ -126,6 +126,51 @@ export function runMacCodesignCommands(commands, options = {}) {
126
126
  return results;
127
127
  }
128
128
 
129
+ export function runMacGuiNotarization({ kind = "notarize", file, profile = "apple-dev-notary", app, validateOnly,
130
+ cwd = process.cwd(), env = process.env, run = spawnSync, io = fs, home = os.userInfo().homedir,
131
+ uid = process.getuid?.(), username = os.userInfo().username, allowedRoots, timeoutMs = NOTARIZATION_TIMEOUT_MS,
132
+ request = requestSigningAgentSync, install = installSigningAgent } = {}) {
133
+ if (!["notarize", "staple"].includes(kind) || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_NOTARIZATION_TIMEOUT_MS) throw new TypeError("Invalid notarization request or timeout");
134
+ if (kind === "notarize" && (typeof profile !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_. -]{0,127}$/.test(profile))) throw new TypeError("Notarization requires a profile name only");
135
+ requireMacGuiSession({ run, uid });
136
+ const paths = signingAgentPaths(home);
137
+ let roots = allowedRoots ?? defaultSigningRoots(home, env.RIGHTKIT_MANAGED_ROOT);
138
+ if (!allowedRoots && io.existsSync(paths.config)) {
139
+ privatePath(paths.config, 0o600, { io, uid });
140
+ roots = JSON.parse(io.readFileSync(paths.config, "utf8")).roots;
141
+ }
142
+ const plan = { schema: 1, type: kind, path: path.resolve(file), cwd: path.resolve(cwd),
143
+ allowedRoots: roots.filter((root) => io.existsSync(root)), timeoutMs,
144
+ ...(kind === "notarize" ? { profile, staple: true, ...(app ? { app: path.resolve(app) } : {}) } : { validateOnly: validateOnly === true }) };
145
+ const options = { home, timeoutMs: timeoutMs + 5000 };
146
+ let report;
147
+ try { report = request(plan, options); }
148
+ catch (error) {
149
+ let missing = error.code === "MAC_SIGNING_AGENT_UNAVAILABLE";
150
+ // Upgrade earlier installed agents once; never turn a rejected path or
151
+ // malformed request into an install/retry loop.
152
+ if (error.code === "MAC_SIGNING_AGENT_PROTOCOL") {
153
+ const health = request({ schema: 1, type: "health" }, { home, timeoutMs: 1500 });
154
+ missing = !health.capabilities?.includes(kind);
155
+ }
156
+ if (!missing) throw error;
157
+ install({ home, username, uid, run, io, managedRoot: env.RIGHTKIT_MANAGED_ROOT });
158
+ report = request(plan, options);
159
+ }
160
+ validateNotaryAgentReport(report);
161
+ const targets = kind === "staple" ? [plan.path] : [...new Set([...( /\.(dmg|pkg)$/i.test(plan.path) ? [plan.path] : []), ...(plan.app ? [plan.app] : [])])];
162
+ const canonical = (value) => (io.realpathSync.native ?? io.realpathSync)(value);
163
+ if (!report.ok || (kind === "notarize" && (report.notarization.status !== "Accepted" || !report.notarization.id
164
+ || !report.steps.some((step) => step.step === "submit" && step.exitCode === 0)))
165
+ || report.stapling.length !== targets.length || targets.some((target) => !report.stapling.some((item) => item.file === canonical(target)
166
+ && report.steps.some((step) => step.step === "validate" && step.file === item.file && step.exitCode === 0)
167
+ && (kind !== "notarize" || report.steps.some((step) => step.step === "staple" && step.file === item.file && step.exitCode === 0))))
168
+ || report.steps.some((step) => step.step !== "probe" && step.exitCode !== 0)) {
169
+ throw new MacSigningSessionError("MAC_SIGNING_AGENT_PROTOCOL", "Signing agent returned incomplete notarization or stapling evidence");
170
+ }
171
+ return report;
172
+ }
173
+
129
174
  export function signMacUpdaterFiles(files, options = {}) {
130
175
  const { cwd = process.cwd(), env = process.env, run = spawnSync, username = os.userInfo().username } = options;
131
176
  const commands = files.map((file) => ["exec", "tauri", "signer", "sign", file]);
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from "node:fs";
3
3
  import path from "node:path";
4
- import { notarytoolAuthArgs } from "./notary-auth.mjs";
5
4
  import { notarizeMacosArtifact } from "./macos-notarization.mjs";
6
5
 
7
6
  const args = process.argv.slice(2);
@@ -24,10 +23,11 @@ if (dryRun) {
24
23
  if (process.platform !== "darwin") fail("macOS notarization must run on macOS");
25
24
  if (!existsSync(archive)) fail(`missing archive: ${archive}`);
26
25
 
27
- let authArgs;
28
- try { authArgs = notarytoolAuthArgs({ profile }); } catch (error) { fail(error.message); }
29
- try { notarizeMacosArtifact({ archive, receiptPath, authArgs, app }); }
30
- catch (error) { fail(error.message); }
26
+ try { notarizeMacosArtifact({ archive, receiptPath, profile, app }); }
27
+ catch (error) {
28
+ if (error.report) console.error(JSON.stringify(error.report));
29
+ fail(`${error.code ? `[${error.code}] ` : ""}${error.message}`);
30
+ }
31
31
 
32
32
  function fail(message) {
33
33
  console.error(`right-notarize-macos: ${message}`);
@@ -5,7 +5,7 @@
5
5
  "sha256": "8d12e39ee6bac3bf8332df8dc61c74637e2eda6becbcc39746fe1ed5a1fe7ef6"
6
6
  },
7
7
  "macos": {
8
- "path": "/Users/adrdsouza/.rightkit-managed/ownership-approval/macos/content-54224936edb74c9044dbaaa38a0773ee77acec21826237572b9b7c67a87b8ccc/ownership-approval",
9
- "sha256": "54224936edb74c9044dbaaa38a0773ee77acec21826237572b9b7c67a87b8ccc"
8
+ "path": "/Users/adrdsouza/.rightkit-managed/ownership-approval/macos/content-2c30864bdb666a1ca1e4388ea7a3bc83cb985196d666b96cc30e1da7cd5b0e6b/RightKitOwnershipApproval.app/Contents/MacOS/ownership-approval",
9
+ "sha256": "2c30864bdb666a1ca1e4388ea7a3bc83cb985196d666b96cc30e1da7cd5b0e6b"
10
10
  }
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.134",
3
+ "version": "0.2.135",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
package/release.mjs CHANGED
@@ -28,6 +28,7 @@ import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
28
28
  import { runConfiguredChecks } from "./release-checks.mjs";
29
29
  import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
30
30
  import { DEFAULT_STORAGE_THRESHOLD_BYTES, findInstallerCopyScripts, findLargeRepositorySubdirectories } from "./installer-storage-doctor.mjs";
31
+ import { macosCodesignEnvironment } from "./codesign-environment.mjs";
31
32
 
32
33
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
33
34
  const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
@@ -962,7 +963,7 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
962
963
  const useShell = process.platform === "win32" && !/\.(?:exe|com)$/i.test(cmd);
963
964
  const child = spawn(cmd, runArgs, {
964
965
  cwd,
965
- env: { ...process.env, ...releaseEnv, ...env },
966
+ env: macosCodesignEnvironment({ ...process.env, ...releaseEnv, ...env }),
966
967
  stdio: options.captureStdout ? ["inherit", "pipe", "inherit"] : "inherit",
967
968
  shell: useShell,
968
969
  windowsHide: true,
@@ -5,10 +5,13 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { randomBytes, timingSafeEqual, randomUUID } from "node:crypto";
7
7
  import { fileURLToPath } from "node:url";
8
- import { spawnSync } from "./background-process.mjs";
8
+ import { spawn, spawnSync } from "./background-process.mjs";
9
+ import { parseCodesignArgv } from "./codesign-argv.mjs";
9
10
 
10
11
  export const AGENT_LABEL = "dev.rightkit.signing-agent";
11
12
  export const MAX_FRAME = 1024 * 1024;
13
+ export const NOTARIZATION_TIMEOUT_MS = 1800000;
14
+ export const MAX_NOTARIZATION_TIMEOUT_MS = 7200000;
12
15
  export class SigningAgentError extends Error {
13
16
  constructor(code, message) { super(message); this.name = "SigningAgentError"; this.code = code; }
14
17
  }
@@ -19,8 +22,9 @@ export function signingAgentPaths(home = os.userInfo().homedir) {
19
22
  config: path.join(directory, "config.json"), runtime: path.join(directory, "runtime"),
20
23
  log: path.join(directory, "requests.jsonl"), plist: path.join(home, "Library/LaunchAgents", `${AGENT_LABEL}.plist`) };
21
24
  }
22
- export function defaultSigningRoots(home = os.userInfo().homedir, managedRoot = "/Volumes/D/.rightkit-managed") {
23
- return ["/Volumes/D/claude", managedRoot, "/Volumes/D/rightkit-build-control", path.join(home, "Library/Caches/RightSuite/release")];
25
+ export function defaultSigningRoots(home = os.userInfo().homedir, managedRoot = "/Volumes/D/.rightkit-managed", cargoTargetDirs = []) {
26
+ return [...new Set(["/Volumes/D/claude", managedRoot, "/Volumes/D/rightkit-build-control",
27
+ path.join(home, "Library/Caches/RightSuite/release"), "/tmp", "/private/tmp", os.tmpdir(), ...cargoTargetDirs])];
24
28
  }
25
29
  export const within = (root, file) => file === root || file.startsWith(`${root}${path.sep}`);
26
30
  export function privatePath(file, mode, { io = fs, uid = process.getuid?.(), kind = "file" } = {}) {
@@ -55,15 +59,19 @@ function absolute(value) {
55
59
  return path.resolve(value);
56
60
  }
57
61
  export function validateSigningRequest(request, { roots, stagingRoot, io = fs } = {}) {
62
+ // Node's JS realpath normalizes .. before following links. Native realpath
63
+ // follows the same path traversal as codesign & catches link/.. escapes.
64
+ const realpath = io.realpathSync.native ?? io.realpathSync;
58
65
  const common = ["schema", "token", "type", "allowedRoots", "cwd", "timeoutMs"];
59
66
  if (request?.type === "health") {
60
67
  shape(request, ["schema", "token", "type"]);
61
68
  if (request.schema !== 1) fail("MAC_SIGNING_AGENT_PROTOCOL", "Unsupported protocol schema");
62
69
  return request;
63
70
  }
64
- if (!["codesign", "updater"].includes(request?.type)) fail("MAC_SIGNING_AGENT_PROTOCOL", "Unsupported signing request type");
65
- shape(request, [...common, request.type === "codesign" ? "items" : "files"]);
66
- if (request.schema !== 1 || !Number.isInteger(request.timeoutMs) || request.timeoutMs < 1 || request.timeoutMs > 180000) {
71
+ const notary = ["notarize", "staple"].includes(request?.type);
72
+ if (!["codesign", "codesign-argv", "updater", "notarize", "staple"].includes(request?.type)) fail("MAC_SIGNING_AGENT_PROTOCOL", "Unsupported signing request type");
73
+ shape(request, [...common, ...(notary ? ["path", "profile", "staple", "app", "validateOnly"] : request.type === "codesign-argv" ? ["argv", "targets"] : [request.type === "codesign" ? "items" : "files"])]);
74
+ if (request.schema !== 1 || !Number.isInteger(request.timeoutMs) || request.timeoutMs < 1 || request.timeoutMs > (notary ? MAX_NOTARIZATION_TIMEOUT_MS : 180000)) {
67
75
  fail("MAC_SIGNING_AGENT_PROTOCOL", "Invalid schema or signing deadline");
68
76
  }
69
77
  if (!Array.isArray(request.allowedRoots) || !request.allowedRoots.length || request.allowedRoots.length > 32) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Allowed roots required");
@@ -73,15 +81,16 @@ export function validateSigningRequest(request, { roots, stagingRoot, io = fs }
73
81
  const allowed = request.allowedRoots.map((root) => {
74
82
  const lexical = absolute(root);
75
83
  if (![...installed, stagingRoot].filter(Boolean).some((base) => within(base, lexical))) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Caller root is outside installed signing policy");
76
- const canonical = io.realpathSync(lexical);
84
+ const canonical = realpath(root);
77
85
  if (![...installed, stagingRoot].filter(Boolean).some((base) => {
78
- try { return within(io.realpathSync(base), canonical); } catch { return false; }
86
+ try { return within(realpath(base), canonical); } catch { return false; }
79
87
  })) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Allowed root escapes installed policy");
80
88
  return { lexical, canonical };
81
89
  });
82
90
  function checked(file, { directory = false, entitlements = false } = {}) {
83
91
  const lexical = absolute(file);
84
- const canonical = io.realpathSync(lexical);
92
+ let canonical;
93
+ try { canonical = realpath(file); } catch { fail("MAC_SIGNING_AGENT_PATH_DENIED", "Signing path is unavailable"); }
85
94
  if (!allowed.some((root) => within(root.lexical, lexical) && within(root.canonical, canonical))
86
95
  || (!entitlements && stagingRoot && within(stagingRoot, canonical))) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Signing path escapes allowed roots");
87
96
  const info = io.statSync(canonical);
@@ -89,6 +98,33 @@ export function validateSigningRequest(request, { roots, stagingRoot, io = fs }
89
98
  return canonical;
90
99
  }
91
100
  const cwd = checked(request.cwd, { directory: true });
101
+ if (notary) {
102
+ const file = checked(request.path);
103
+ if (request.type === "staple") {
104
+ if (request.profile !== undefined || request.app !== undefined || request.staple !== undefined
105
+ || (request.validateOnly !== undefined && typeof request.validateOnly !== "boolean")) fail("MAC_SIGNING_AGENT_PROTOCOL", "Invalid stapling options");
106
+ if (!/\.(dmg|pkg|app)$/i.test(file) || (/\.app$/i.test(file) ? !io.statSync(file).isDirectory() : !io.statSync(file).isFile())) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Invalid staple target");
107
+ return { ...request, cwd, path: file };
108
+ }
109
+ const profile = request.profile ?? "apple-dev-notary";
110
+ if (typeof profile !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_. -]{0,127}$/.test(profile)
111
+ || request.staple !== true || request.validateOnly !== undefined) fail("MAC_SIGNING_AGENT_PROTOCOL", "Notarization requires a profile name & staple: true");
112
+ if (!/\.(dmg|zip|pkg)$/i.test(file) || !io.statSync(file).isFile()) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Notarization requires DMG, ZIP, or PKG");
113
+ const app = request.app === undefined ? undefined : checked(request.app, { directory: true });
114
+ if (app && !/\.app$/i.test(app)) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Stapling app must be an app bundle");
115
+ if (/\.zip$/i.test(file) && !app) fail("MAC_SIGNING_AGENT_PROTOCOL", "ZIP submission requires app bundle for stapling");
116
+ return { ...request, cwd, path: file, profile, ...(app ? { app } : {}) };
117
+ }
118
+ if (request.type === "codesign-argv") {
119
+ const parsed = parseCodesignArgv(request.argv, request.cwd);
120
+ if (!Array.isArray(request.targets) || request.targets.length !== parsed.targets.length
121
+ || request.targets.some((target, index) => target !== parsed.targets[index])) fail("MAC_SIGNING_AGENT_PROTOCOL", "Codesign target paths do not match argv");
122
+ for (const target of parsed.targets) checked(target);
123
+ for (const file of parsed.entitlements) checked(file, { entitlements: true });
124
+ // Preserve argv & cwd spelling: relative arguments must resolve exactly as
125
+ // they would for the original caller, including symlink/.. semantics.
126
+ return { ...request };
127
+ }
92
128
  if (request.type === "updater") {
93
129
  if (!Array.isArray(request.files) || !request.files.length || request.files.length > 4096) fail("MAC_SIGNING_AGENT_PROTOCOL", "Updater files required");
94
130
  const files = request.files.map((file) => {
@@ -119,9 +155,17 @@ export function authenticateSigningToken(candidate, token) {
119
155
  }
120
156
  export function executeSigningRequest(request, { run = spawnSync, home = os.userInfo().homedir,
121
157
  username = os.userInfo().username, pnpm, environmentPath, now = Date.now } = {}) {
122
- if (request.type === "health") return { schema: 1, ok: true, service: AGENT_LABEL, pid: process.pid };
158
+ if (request.type === "health") return { schema: 1, ok: true, service: AGENT_LABEL, pid: process.pid, capabilities: ["codesign", "codesign-argv", "updater", "notarize", "staple"] };
123
159
  const deadline = now() + request.timeoutMs;
124
160
  const env = { HOME: home, PATH: environmentPath, TMPDIR: path.join(signingAgentPaths(home).directory, "tmp") };
161
+ if (request.type === "codesign-argv") {
162
+ const r = run("/usr/bin/codesign", request.argv, { cwd: request.cwd, env, encoding: "utf8", stdio: "pipe", shell: false,
163
+ windowsHide: true, timeout: request.timeoutMs, killSignal: "SIGKILL", maxBuffer: MAX_FRAME });
164
+ if (r.error || !Number.isInteger(r.status)) fail(r.error?.code === "ETIMEDOUT" ? "MAC_SIGNING_AGENT_TIMEOUT" : "MAC_SIGNING_AGENT_JOB_FAILED", "Codesign execution failed");
165
+ const report = { schema: 1, ok: true, results: [{ status: r.status, stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") }] };
166
+ if (Buffer.byteLength(`${JSON.stringify(report)}\n`) > MAX_FRAME) fail("MAC_SIGNING_AGENT_PROTOCOL", "Codesign output exceeds protocol limit");
167
+ return report;
168
+ }
125
169
  const results = [];
126
170
  const outputLimit = Math.min(4096, Math.floor(MAX_FRAME / (4 * (request.items?.length ?? request.files.length))));
127
171
  const invoke = (command, args, sensitive = false) => {
@@ -158,6 +202,92 @@ export function executeSigningRequest(request, { run = spawnSync, home = os.user
158
202
  throw error;
159
203
  } finally { delete env.TAURI_SIGNING_PRIVATE_KEY; delete env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD; }
160
204
  }
205
+ // No notarytool/stapler output is forwarded verbatim: diagnostics can contain
206
+ // account details. Only typed status, submission UUID, counts & issue codes leave Aqua.
207
+ export async function executeNotarizationRequest(request, { launch = spawn, home = os.userInfo().homedir,
208
+ environmentPath = "/usr/bin:/bin", now = Date.now, id = randomUUID(), progress = () => {}, signal } = {}) {
209
+ const deadline = now() + request.timeoutMs;
210
+ const env = { HOME: home, PATH: environmentPath, TMPDIR: path.join(signingAgentPaths(home).directory, "tmp") };
211
+ const report = { schema: 1, ok: false, id, notarization: { id: null, status: "Unknown", logSummary: null }, steps: [], stapling: [] };
212
+ const invoke = async (step, args, file) => {
213
+ progress({ schema: 1, event: "status", id, step });
214
+ const remaining = deadline - now();
215
+ if (remaining <= 0 || signal?.aborted) throw new SigningAgentError("MAC_SIGNING_AGENT_TIMEOUT", "Notarization deadline expired");
216
+ const result = await new Promise((resolve) => {
217
+ let child, output = "", bytes = 0, timedOut = false, overflow = false, failed = false;
218
+ let timer;
219
+ const kill = () => { try { child?.kill("SIGKILL"); } catch {} };
220
+ const cancel = () => { timedOut = true; kill(); };
221
+ const finish = (exitCode) => {
222
+ clearTimeout(timer); signal?.removeEventListener("abort", cancel);
223
+ resolve({ exitCode: Number.isInteger(exitCode) ? exitCode : null, output, timedOut, overflow, failed });
224
+ };
225
+ try {
226
+ child = launch("/usr/bin/xcrun", args, { cwd: request.cwd, env, stdio: ["ignore", "pipe", "pipe"], shell: false });
227
+ timer = setTimeout(cancel, remaining);
228
+ signal?.addEventListener("abort", cancel, { once: true });
229
+ child.stdout.on("data", (chunk) => {
230
+ bytes += chunk.length;
231
+ if (bytes > MAX_FRAME) { overflow = true; kill(); } else output += chunk.toString();
232
+ });
233
+ child.stderr.on("data", () => {}); // Drain, never disclose.
234
+ child.once("error", () => { failed = true; finish(null); });
235
+ child.once("close", finish);
236
+ } catch { failed = true; finish(null); }
237
+ });
238
+ report.steps.push({ step, ...(file ? { file } : {}), exitCode: result.exitCode });
239
+ if (result.timedOut) throw new SigningAgentError("MAC_SIGNING_AGENT_TIMEOUT", "Notarization deadline expired");
240
+ if (result.failed || result.overflow) throw new SigningAgentError("MAC_SIGNING_AGENT_JOB_FAILED", `${step} execution failed`);
241
+ return result;
242
+ };
243
+ const parse = (output) => { try { return JSON.parse(output); } catch { return {}; } };
244
+ try {
245
+ if (request.type === "notarize") {
246
+ const submitted = await invoke("submit", ["notarytool", "submit", request.path, "--keychain-profile", request.profile ?? "apple-dev-notary", "--wait", "--output-format", "json"]);
247
+ const result = parse(submitted.output);
248
+ const submissionId = result.id ?? result.submissionId;
249
+ report.notarization.id = typeof submissionId === "string" && /^[a-f0-9-]{36}$/i.test(submissionId) ? submissionId : null;
250
+ report.notarization.status = ["Accepted", "Invalid", "In Progress", "Rejected"].includes(result.status) ? result.status : "Unknown";
251
+ if (report.notarization.status === "Invalid" && report.notarization.id) {
252
+ const logged = await invoke("log", ["notarytool", "log", report.notarization.id, "--keychain-profile", request.profile ?? "apple-dev-notary", "--output-format", "json"]);
253
+ if (logged.exitCode === 0) {
254
+ const log = parse(logged.output);
255
+ const issues = Array.isArray(log.issues) ? log.issues : [];
256
+ report.notarization.logSummary = { statusCode: Number.isInteger(log.statusCode) ? log.statusCode : null,
257
+ errors: issues.filter((issue) => issue.severity === "error").length,
258
+ warnings: issues.filter((issue) => issue.severity === "warning").length,
259
+ issues: issues.slice(0, 100).map((issue) => ({ severity: ["error", "warning"].includes(issue.severity) ? issue.severity : "unknown",
260
+ code: Number.isInteger(issue.code) ? issue.code : null })) };
261
+ }
262
+ }
263
+ if (submitted.exitCode !== 0 || report.notarization.status !== "Accepted" || !report.notarization.id) {
264
+ throw new SigningAgentError("MAC_SIGNING_NOTARIZATION_FAILED", `Notarization ${report.notarization.status}`);
265
+ }
266
+ }
267
+ const targets = request.type === "staple" ? [request.path]
268
+ : [...new Set([...( /\.(dmg|pkg)$/i.test(request.path) ? [request.path] : []), ...(request.app ? [request.app] : [])])];
269
+ for (const file of targets) {
270
+ let alreadyStapled = false;
271
+ if (request.type === "staple" && !request.validateOnly) {
272
+ const probe = await invoke("probe", ["stapler", "validate", file], file);
273
+ alreadyStapled = probe.exitCode === 0;
274
+ }
275
+ if (!request.validateOnly && !alreadyStapled) {
276
+ const stapled = await invoke("staple", ["stapler", "staple", file], file);
277
+ if (stapled.exitCode !== 0) throw new SigningAgentError("MAC_SIGNING_STAPLE_FAILED", "Stapling failed");
278
+ }
279
+ const validated = await invoke("validate", ["stapler", "validate", file], file);
280
+ if (validated.exitCode !== 0) throw new SigningAgentError("MAC_SIGNING_STAPLE_FAILED", "Staple validation failed");
281
+ report.stapling.push({ file, alreadyStapled, validated: true });
282
+ }
283
+ report.ok = true;
284
+ } catch (error) {
285
+ report.code = error.code?.startsWith("MAC_SIGNING_") ? error.code : "MAC_SIGNING_AGENT_JOB_FAILED";
286
+ report.message = report.code === "MAC_SIGNING_AGENT_TIMEOUT" ? "Notarization deadline expired" : "Notarization or stapling failed inside signing agent";
287
+ }
288
+ return report;
289
+ }
290
+
161
291
  export function managedSigningLog(file, event, { io = fs } = {}) {
162
292
  if (io.existsSync(file)) {
163
293
  privatePath(file, 0o600, { io });
@@ -170,7 +300,7 @@ export function managedSigningLog(file, event, { io = fs } = {}) {
170
300
  // Only service-authored metadata is logged, never request/token/key/argv/output.
171
301
  io.appendFileSync(file, `${JSON.stringify({ at: new Date().toISOString(), ...event })}\n`, { mode: 0o600 });
172
302
  }
173
- export async function startSigningAgent({ paths = signingAgentPaths(), config, io = fs, run = spawnSync,
303
+ export async function startSigningAgent({ paths = signingAgentPaths(), config, io = fs, run = spawnSync, launch = spawn, heartbeatMs = 10000,
174
304
  log = (event) => managedSigningLog(paths.log, event), createServer = net.createServer } = {}) {
175
305
  privatePath(paths.directory, 0o700, { io, kind: "directory" });
176
306
  privatePath(paths.config, 0o600, { io });
@@ -211,9 +341,14 @@ export async function startSigningAgent({ paths = signingAgentPaths(), config, i
211
341
  return;
212
342
  }
213
343
  const receivedAt = Date.now();
344
+ const notary = ["notarize", "staple"].includes(request.type);
345
+ const controller = new AbortController();
346
+ const progress = (frame) => { if (!socket.destroyed) socket.write(`${JSON.stringify(frame)}\n`); };
347
+ const heartbeat = notary ? setInterval(() => progress({ schema: 1, event: "heartbeat", id }), heartbeatMs) : null;
348
+ if (notary) socket.on("close", () => { clearInterval(heartbeat); controller.abort(); });
214
349
  pending++;
215
- log({ event: "request.accepted", id, type: request.type, count: request.items?.length ?? request.files?.length ?? 0 });
216
- queue = queue.then(() => {
350
+ log({ event: "request.accepted", id, type: request.type, count: notary ? 1 : request.targets?.length ?? request.items?.length ?? request.files?.length ?? 0 });
351
+ queue = queue.then(async () => {
217
352
  if (socket.destroyed) { log({ event: "request.cancelled", id, type: request.type }); return; }
218
353
  log({ event: "request.started", id, type: request.type });
219
354
  try {
@@ -223,15 +358,20 @@ export async function startSigningAgent({ paths = signingAgentPaths(), config, i
223
358
  // Revalidate immediately before effects, not merely on admission.
224
359
  request = validateSigningRequest(request, { roots: config.roots, stagingRoot: path.join(paths.directory, "requests"), io });
225
360
  }
226
- const report = executeSigningRequest(request, { run, home: config.home, username: config.username, pnpm: config.pnpm, environmentPath: config.environmentPath });
361
+ const report = notary
362
+ ? await executeNotarizationRequest(request, { launch, home: config.home, environmentPath: config.environmentPath, id, progress, signal: controller.signal })
363
+ : executeSigningRequest(request, { run, home: config.home, username: config.username, pnpm: config.pnpm, environmentPath: config.environmentPath });
227
364
  respond(report);
228
- log({ event: "request.completed", id, type: request.type });
365
+ log({ event: report.ok ? "request.completed" : "request.failed", id, type: request.type,
366
+ ...(report.code ? { code: report.code } : {}),
367
+ ...(notary ? { status: report.notarization.status, steps: report.steps.map(({ step, exitCode }) => ({ step, exitCode })) } : {}),
368
+ ...(request.type === "codesign-argv" ? { status: report.results[0].status } : {}) });
229
369
  } catch (error) {
230
370
  const code = error.code?.startsWith("MAC_SIGNING_") ? error.code : "MAC_SIGNING_AGENT_JOB_FAILED";
231
- respond({ schema: 1, ok: false, code, message: request.type === "updater" ? "Updater signing failed" : error.message });
371
+ respond({ schema: 1, ok: false, ...(notary ? { id } : {}), code, message: request.type === "updater" ? "Updater signing failed" : notary ? "Notarization job failed" : error.message });
232
372
  log({ event: "request.failed", id, type: request.type, code });
233
373
  }
234
- }).finally(() => { pending--; }).catch(() => { socket.destroy(); });
374
+ }).finally(() => { clearInterval(heartbeat); pending--; }).catch(() => { socket.destroy(); });
235
375
  });
236
376
  });
237
377
  await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); });
@@ -27,9 +27,9 @@
27
27
  "@rightkit/onboarding": "0.2.3",
28
28
  "@rightkit/pdf-ui": "0.1.2",
29
29
  "@rightkit/platform-ui": "0.1.2",
30
- "@rightkit/qa": "0.3.1",
30
+ "@rightkit/qa": "0.3.2",
31
31
  "@rightkit/qa-chrome": "0.1.3",
32
- "@rightkit/release": "0.2.134",
32
+ "@rightkit/release": "0.2.135",
33
33
  "@rightkit/settings-ui": "0.2.7",
34
34
  "@rightkit/shell": "0.1.0",
35
35
  "@rightkit/tauri": "0.1.1",
@@ -157,7 +157,8 @@
157
157
  "0.2.130",
158
158
  "0.2.131",
159
159
  "0.2.132",
160
- "0.2.133"
160
+ "0.2.133",
161
+ "0.2.134"
161
162
  ],
162
163
  "@rightkit/qa": [
163
164
  "0.1.0",
@@ -166,7 +167,8 @@
166
167
  "0.2.2",
167
168
  "0.2.3",
168
169
  "0.2.4",
169
- "0.3.0"
170
+ "0.3.0",
171
+ "0.3.1"
170
172
  ],
171
173
  "@rightkit/app-shell": [
172
174
  "0.1.0",
@@ -240,7 +242,7 @@
240
242
  "rightkit-automation": "0.1.0",
241
243
  "rightkit-browser": "0.2.2",
242
244
  "rightkit-cache": "0.1.0",
243
- "rightkit-control": "0.1.8",
245
+ "rightkit-control": "0.1.9",
244
246
  "rightkit-crash": "0.1.0",
245
247
  "rightkit-db": "0.3.0",
246
248
  "rightkit-embed": "0.2.2",
@@ -258,7 +260,7 @@
258
260
  "rightkit-pdf": "0.1.0",
259
261
  "rightkit-playback": "0.1.1",
260
262
  "rightkit-process": "0.3.1",
261
- "rightkit-qa": "0.2.11",
263
+ "rightkit-qa": "0.2.12",
262
264
  "rightkit-qts": "0.2.1",
263
265
  "rightkit-qts-ggml": "0.1.0",
264
266
  "rightkit-qts-ggml-sys": "0.1.0",
@@ -290,7 +292,7 @@
290
292
  "rightkit-audio": "0.1.1",
291
293
  "rightkit-browser": "0.2.2",
292
294
  "rightkit-cache": "0.1.0",
293
- "rightkit-control": "0.1.8",
295
+ "rightkit-control": "0.1.9",
294
296
  "rightkit-crash": "0.1.0",
295
297
  "rightkit-db": "0.3.0",
296
298
  "rightkit-embed": "0.2.2",
@@ -308,7 +310,7 @@
308
310
  "rightkit-pdf": "0.1.0",
309
311
  "rightkit-playback": "0.1.1",
310
312
  "rightkit-process": "0.3.1",
311
- "rightkit-qa": "0.2.11",
313
+ "rightkit-qa": "0.2.12",
312
314
  "rightkit-qts": "0.2.1",
313
315
  "rightkit-qts-ggml": "0.1.0",
314
316
  "rightkit-qts-ggml-sys": "0.1.0",
@@ -337,9 +339,9 @@
337
339
  "@rightkit/onboarding": "0.2.3",
338
340
  "@rightkit/pdf-ui": "0.1.2",
339
341
  "@rightkit/platform-ui": "0.1.2",
340
- "@rightkit/qa": "0.3.1",
342
+ "@rightkit/qa": "0.3.2",
341
343
  "@rightkit/qa-chrome": "0.1.3",
342
- "@rightkit/release": "0.2.133",
344
+ "@rightkit/release": "0.2.134",
343
345
  "@rightkit/settings-ui": "0.2.7",
344
346
  "@rightkit/shell": "0.1.0",
345
347
  "@rightkit/tauri": "0.1.1",
@@ -392,7 +394,8 @@
392
394
  "0.2.7",
393
395
  "0.2.8",
394
396
  "0.2.9",
395
- "0.2.10"
397
+ "0.2.10",
398
+ "0.2.11"
396
399
  ],
397
400
  "rightkit-sandbox": [
398
401
  "0.1.0",
@@ -416,7 +419,8 @@
416
419
  ],
417
420
  "rightkit-control": [
418
421
  "0.1.6",
419
- "0.1.7"
422
+ "0.1.7",
423
+ "0.1.8"
420
424
  ]
421
425
  }
422
426
  }
@@ -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,22 @@ 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
130
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
77
131
  try {
78
132
  const { request, home, timeoutMs } = JSON.parse(fs.readFileSync(0, "utf8"));
79
- const report = await requestSigningAgent(request, { paths: signingAgentPaths(home), timeoutMs });
133
+ const report = await requestSigningAgent(request, { paths: signingAgentPaths(home), timeoutMs,
134
+ onProgress: (frame) => process.stderr.write(`right-release signing-agent: ${frame.id} ${frame.step ?? frame.event}\n`) });
80
135
  process.stdout.write(JSON.stringify(report));
81
136
  } catch (error) {
82
- process.stdout.write(JSON.stringify({ schema: 1, ok: false, code: error.code ?? "MAC_SIGNING_AGENT_PROTOCOL", message: error.message }));
137
+ process.stdout.write(JSON.stringify(error.report ?? { schema: 1, ok: false, code: error.code ?? "MAC_SIGNING_AGENT_PROTOCOL", message: error.message }));
83
138
  process.exitCode = 1;
84
139
  }
85
140
  }
@@ -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();
@@ -6,6 +6,8 @@ import { AGENT_LABEL, signingAgentPaths, defaultSigningRoots, ensurePrivateDirec
6
6
  import { requestSigningAgentSync } from "./signing-agent-client.mjs";
7
7
 
8
8
  const capture = { encoding: "utf8", stdio: "pipe", shell: false, timeout: 10000 };
9
+ function sleepMs(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
10
+
9
11
  export function requireMacGuiSession({ run = spawnSync, uid = process.getuid?.() } = {}) {
10
12
  const session = run("/bin/launchctl", ["print", `gui/${uid}`], capture);
11
13
  if (session.error || session.status !== 0) throw new SigningAgentError("MAC_SIGNING_NO_GUI_SESSION", `No logged-in Aqua session for uid ${uid}`);
@@ -27,7 +29,7 @@ export function signingAgentPlist({ paths, node = process.execPath, home }) {
27
29
  }
28
30
  export function installSigningAgent({ home = os.userInfo().homedir, username = os.userInfo().username,
29
31
  uid = process.getuid?.(), platform = process.platform, run = spawnSync, io = fs, node = process.execPath,
30
- managedRoot = "/Volumes/D/.rightkit-managed", health = requestSigningAgentSync,
32
+ managedRoot = "/Volumes/D/.rightkit-managed", cargoTargetDirs = [], health = requestSigningAgentSync,
31
33
  now = Date.now, sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms) } = {}) {
32
34
  if (platform !== "darwin") throw new SigningAgentError("MAC_SIGNING_AGENT_PLATFORM", "Signing agent requires macOS");
33
35
  requireMacGuiSession({ run, uid });
@@ -41,14 +43,26 @@ export function installSigningAgent({ home = os.userInfo().homedir, username = o
41
43
  const pnpm = String(found.stdout ?? "").trim();
42
44
  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
45
  // Resident runtime is self-contained; no package/checkout imports after install.
44
- for (const name of ["rightkit-signing-agent.mjs", "background-process.mjs"]) {
46
+ for (const root of cargoTargetDirs) {
47
+ 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");
48
+ }
49
+ for (const name of ["rightkit-signing-agent.mjs", "background-process.mjs", "codesign-argv.mjs"]) {
45
50
  const target = path.join(paths.runtime, name);
46
51
  if (io.existsSync(target)) privatePath(target, 0o600, { io, uid });
47
52
  io.writeFileSync(target, io.readFileSync(new URL(`./${name}`, import.meta.url)), { mode: 0o600 });
48
53
  }
49
54
  if (io.existsSync(paths.config)) privatePath(paths.config, 0o600, { io, uid });
55
+ // SSH & Aqua can inherit different TMPDIR spellings. Include both aliases of
56
+ // this uid's Darwin temp root, since bundlers can canonicalize temporary paths.
57
+ const temp = run("/usr/bin/getconf", ["DARWIN_USER_TEMP_DIR"], capture);
58
+ const directory = String(temp.stdout ?? "").trim();
59
+ const temporaryRoots = [];
60
+ if (!temp.error && temp.status === 0 && path.isAbsolute(directory) && !directory.includes("\n")) {
61
+ temporaryRoots.push(path.resolve(directory));
62
+ try { temporaryRoots.push((io.realpathSync.native ?? io.realpathSync)(directory)); } catch {}
63
+ }
50
64
  io.writeFileSync(paths.config, JSON.stringify({ schema: 1, home, username, pnpm,
51
- roots: defaultSigningRoots(home, path.resolve(managedRoot)),
65
+ roots: defaultSigningRoots(home, path.resolve(managedRoot), [...cargoTargetDirs.map((root) => path.resolve(root)), ...temporaryRoots]),
52
66
  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
67
  ensurePrivateDirectory(path.dirname(paths.plist), { io, uid });
54
68
  if (io.existsSync(paths.plist)) privatePath(paths.plist, 0o600, { io, uid });
@@ -57,8 +71,20 @@ export function installSigningAgent({ home = os.userInfo().homedir, username = o
57
71
  if (!loaded.error && loaded.status === 0) {
58
72
  const stopped = run("/bin/launchctl", ["bootout", `gui/${uid}/${AGENT_LABEL}`], capture);
59
73
  if (stopped.error || stopped.status !== 0) throw new SigningAgentError("MAC_SIGNING_AGENT_INSTALL_FAILED", "Could not stop existing signing agent");
74
+ // bootout is asynchronous: wait until launchd has released the label before bootstrapping again.
75
+ const gone = now() + 10000;
76
+ while (now() < gone) {
77
+ const still = run("/bin/launchctl", ["print", `gui/${uid}/${AGENT_LABEL}`], capture);
78
+ if (still.error || still.status !== 0) break;
79
+ sleepMs(100);
80
+ }
81
+ }
82
+ let boot = run("/bin/launchctl", ["bootstrap", `gui/${uid}`, paths.plist], capture);
83
+ // Exit 5 (EIO) is launchd still tearing the old job down; retry briefly.
84
+ for (let attempt = 0; attempt < 20 && !boot.error && boot.status === 5; attempt++) {
85
+ sleepMs(250);
86
+ boot = run("/bin/launchctl", ["bootstrap", `gui/${uid}`, paths.plist], capture);
60
87
  }
61
- const boot = run("/bin/launchctl", ["bootstrap", `gui/${uid}`, paths.plist], capture);
62
88
  if (boot.error || boot.status !== 0) {
63
89
  requireMacGuiSession({ run, uid });
64
90
  throw new SigningAgentError("MAC_SIGNING_AGENT_INSTALL_FAILED", `Signing agent bootstrap failed (exit ${boot.status ?? "unavailable"})`);