@rightkit/release 0.2.133 → 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 {
@@ -72,13 +72,21 @@ function destinationFor(line) {
72
72
  }
73
73
 
74
74
  /** Find repository subdirectories above storage threshold outside managed root. */
75
- export function findLargeRepositorySubdirectories({ appRoot, managedRoot = resolveManagedRoot(), thresholdBytes = DEFAULT_STORAGE_THRESHOLD_BYTES, ignored = gitIgnored } = {}) {
75
+ export function findLargeRepositorySubdirectories({ appRoot, managedRoot = resolveManagedRoot(), thresholdBytes = DEFAULT_STORAGE_THRESHOLD_BYTES, ignored = gitIgnored, declared = [] } = {}) {
76
76
  if (!Number.isFinite(thresholdBytes) || thresholdBytes < 1) throw new TypeError("thresholdBytes must be positive");
77
77
  const root = path.resolve(appRoot);
78
78
  const ignoredRoot = managedRoot ? path.resolve(managedRoot) : null;
79
79
  const findings = [];
80
80
  const inside = (candidate, parent) => candidate === parent || candidate.startsWith(`${parent}${path.sep}`);
81
81
  const managedIgnored = ignoredRoot && inside(ignoredRoot, root) ? Boolean(ignored(ignoredRoot, root)) : false;
82
+ // App-declared durable stores (model stores, test fixture roots) are intentional and exempt.
83
+ // Each needs a reason: config `storage.declaredStores: [{ path, reason }]`, paths relative to the app root.
84
+ const declaredRoots = (declared ?? []).map((item) => {
85
+ if (!item?.path || !String(item.reason ?? "").trim()) throw new TypeError("storage.declaredStores entries need path and reason");
86
+ const resolved = path.resolve(root, item.path);
87
+ if (!inside(resolved, root) || resolved === root) throw new TypeError(`storage.declaredStores path must be inside the app root: ${item.path}`);
88
+ return resolved;
89
+ });
82
90
  const walk = (directory) => {
83
91
  let total = 0;
84
92
  let entries;
@@ -91,6 +99,7 @@ export function findLargeRepositorySubdirectories({ appRoot, managedRoot = resol
91
99
  if (stat.isSymbolicLink()) continue;
92
100
  if (stat.isDirectory()) {
93
101
  if (managedIgnored && inside(file, ignoredRoot)) continue;
102
+ if (declaredRoots.some((declaredRoot) => inside(file, declaredRoot))) continue;
94
103
  total += walk(file);
95
104
  } else if (stat.isFile()) total += stat.size;
96
105
  }
@@ -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.133",
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");
@@ -210,7 +211,7 @@ if (opts.doctor) {
210
211
  });
211
212
  const storageFindings = [
212
213
  ...installerCopyFindings,
213
- ...findLargeRepositorySubdirectories({ appRoot: root, managedRoot, thresholdBytes: Number(process.env.RIGHTKIT_ARTIFACT_STORAGE_THRESHOLD_BYTES ?? DEFAULT_STORAGE_THRESHOLD_BYTES) }),
214
+ ...findLargeRepositorySubdirectories({ appRoot: root, managedRoot, declared: config.storage?.declaredStores ?? [], thresholdBytes: Number(process.env.RIGHTKIT_ARTIFACT_STORAGE_THRESHOLD_BYTES ?? DEFAULT_STORAGE_THRESHOLD_BYTES) }),
214
215
  ];
215
216
  if (storageFindings.length) {
216
217
  console.log("artifact-storage: report-only storage findings detected:");
@@ -281,8 +282,9 @@ if (opts.platform === "win") {
281
282
  });
282
283
  }
283
284
  } else if (opts.platform === "mac") {
284
- const signingFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
285
285
  if (runMacPhase("prepackage")) await runCommand(target.prePackage, root);
286
+ // Resolve after prePackage: with storage governance the artifact may live only in RIGHT_RELEASE_ARTIFACT_DIR.
287
+ const signingFiles = target.sign.prePackageFiles.map((p) => resolveReleaseArtifactPath(root, p));
286
288
  for (const file of signingFiles) await mustExist(file, `missing macOS Developer ID signing artifact: ${file}`);
287
289
  if (runMacPhase("sign")) await signMacos(signingFiles, root);
288
290
  } else if (nativeAssembly) {
@@ -961,7 +963,7 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
961
963
  const useShell = process.platform === "win32" && !/\.(?:exe|com)$/i.test(cmd);
962
964
  const child = spawn(cmd, runArgs, {
963
965
  cwd,
964
- env: { ...process.env, ...releaseEnv, ...env },
966
+ env: macosCodesignEnvironment({ ...process.env, ...releaseEnv, ...env }),
965
967
  stdio: options.captureStdout ? ["inherit", "pipe", "inherit"] : "inherit",
966
968
  shell: useShell,
967
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.0",
30
+ "@rightkit/qa": "0.3.2",
31
31
  "@rightkit/qa-chrome": "0.1.3",
32
- "@rightkit/release": "0.2.133",
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",
@@ -156,7 +156,9 @@
156
156
  "0.2.129",
157
157
  "0.2.130",
158
158
  "0.2.131",
159
- "0.2.132"
159
+ "0.2.132",
160
+ "0.2.133",
161
+ "0.2.134"
160
162
  ],
161
163
  "@rightkit/qa": [
162
164
  "0.1.0",
@@ -164,7 +166,9 @@
164
166
  "0.2.1",
165
167
  "0.2.2",
166
168
  "0.2.3",
167
- "0.2.4"
169
+ "0.2.4",
170
+ "0.3.0",
171
+ "0.3.1"
168
172
  ],
169
173
  "@rightkit/app-shell": [
170
174
  "0.1.0",
@@ -238,7 +242,7 @@
238
242
  "rightkit-automation": "0.1.0",
239
243
  "rightkit-browser": "0.2.2",
240
244
  "rightkit-cache": "0.1.0",
241
- "rightkit-control": "0.1.8",
245
+ "rightkit-control": "0.1.9",
242
246
  "rightkit-crash": "0.1.0",
243
247
  "rightkit-db": "0.3.0",
244
248
  "rightkit-embed": "0.2.2",
@@ -256,7 +260,7 @@
256
260
  "rightkit-pdf": "0.1.0",
257
261
  "rightkit-playback": "0.1.1",
258
262
  "rightkit-process": "0.3.1",
259
- "rightkit-qa": "0.2.10",
263
+ "rightkit-qa": "0.2.12",
260
264
  "rightkit-qts": "0.2.1",
261
265
  "rightkit-qts-ggml": "0.1.0",
262
266
  "rightkit-qts-ggml-sys": "0.1.0",
@@ -288,7 +292,7 @@
288
292
  "rightkit-audio": "0.1.1",
289
293
  "rightkit-browser": "0.2.2",
290
294
  "rightkit-cache": "0.1.0",
291
- "rightkit-control": "0.1.8",
295
+ "rightkit-control": "0.1.9",
292
296
  "rightkit-crash": "0.1.0",
293
297
  "rightkit-db": "0.3.0",
294
298
  "rightkit-embed": "0.2.2",
@@ -306,7 +310,7 @@
306
310
  "rightkit-pdf": "0.1.0",
307
311
  "rightkit-playback": "0.1.1",
308
312
  "rightkit-process": "0.3.1",
309
- "rightkit-qa": "0.2.10",
313
+ "rightkit-qa": "0.2.12",
310
314
  "rightkit-qts": "0.2.1",
311
315
  "rightkit-qts-ggml": "0.1.0",
312
316
  "rightkit-qts-ggml-sys": "0.1.0",
@@ -335,9 +339,9 @@
335
339
  "@rightkit/onboarding": "0.2.3",
336
340
  "@rightkit/pdf-ui": "0.1.2",
337
341
  "@rightkit/platform-ui": "0.1.2",
338
- "@rightkit/qa": "0.3.0",
342
+ "@rightkit/qa": "0.3.2",
339
343
  "@rightkit/qa-chrome": "0.1.3",
340
- "@rightkit/release": "0.2.132",
344
+ "@rightkit/release": "0.2.134",
341
345
  "@rightkit/settings-ui": "0.2.7",
342
346
  "@rightkit/shell": "0.1.0",
343
347
  "@rightkit/tauri": "0.1.1",
@@ -389,7 +393,9 @@
389
393
  "rightkit-qa": [
390
394
  "0.2.7",
391
395
  "0.2.8",
392
- "0.2.9"
396
+ "0.2.9",
397
+ "0.2.10",
398
+ "0.2.11"
393
399
  ],
394
400
  "rightkit-sandbox": [
395
401
  "0.1.0",
@@ -413,7 +419,8 @@
413
419
  ],
414
420
  "rightkit-control": [
415
421
  "0.1.6",
416
- "0.1.7"
422
+ "0.1.7",
423
+ "0.1.8"
417
424
  ]
418
425
  }
419
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"})`);