@rightkit/release 0.2.132 → 0.2.133

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.
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { checkApp, EXCEPTIONS_FILE } from "./ownership.mjs";
4
4
  import { DEFAULT_APPS } from "./standalone-clone-verify.mjs";
5
5
  import { sha256 } from "./ownership-approval.mjs";
6
+ import { findInstallerCopyScripts, findLargeRepositorySubdirectories } from "./installer-storage-doctor.mjs";
6
7
 
7
8
  export const OWNERSHIP_APPS = Object.freeze([
8
9
  ...DEFAULT_APPS.map(({ key, appDir }) => ({ key, root: key === "coderight" ? key : path.posix.join(key, appDir === "." ? "" : appDir), scope: key === "coderight" ? "repository" : appDir })),
@@ -58,7 +59,7 @@ function exceptionRequests(root, app, report) {
58
59
  });
59
60
  }
60
61
 
61
- function asRecord(app, root, scope, report, manifest, versions, scannedAt) {
62
+ function asRecord(app, root, scope, report, manifest, versions, scannedAt, storageFindings = []) {
62
63
  const meta = report.metadata ?? {};
63
64
  const findings = (report.findings ?? []).map((f) => ({ fingerprint: f.fingerprint, capability: f.capability, file: f.file, line: f.line ?? 0, occurrenceHash: f.occurrenceHash ?? sha256(canonicalJson(f)), ...(f.qaOnly ? { qaOnly: true } : {}) }));
64
65
  const files = [...new Set(findings.map((f) => f.file))].sort();
@@ -93,8 +94,9 @@ function asRecord(app, root, scope, report, manifest, versions, scannedAt) {
93
94
  covered: report.covered ?? [],
94
95
  problems: report.problems ?? [],
95
96
  warnings: report.warnings ?? [],
97
+ storageFindings,
96
98
  };
97
- record.resultDigest = sha256(canonicalJson({ app: record.app, commit: record.commit, catalogDigest: record.catalogDigest, contentDigests: record.contentDigests, coverage: record.coverage, rows: record.rows, problems: record.problems, warnings: record.warnings, enrollmentPending: record.enrollmentPending }));
99
+ record.resultDigest = sha256(canonicalJson({ app: record.app, commit: record.commit, catalogDigest: record.catalogDigest, contentDigests: record.contentDigests, coverage: record.coverage, rows: record.rows, problems: record.problems, warnings: record.warnings, storageFindings: record.storageFindings, enrollmentPending: record.enrollmentPending }));
98
100
  record.exceptionRequests = exceptionRequests(root, app, report);
99
101
  return record;
100
102
  }
@@ -104,12 +106,20 @@ export function buildOwnershipReport({ workspaceRoot, manifest, versions, apps =
104
106
  const scannedAt = now.toISOString();
105
107
  const records = apps.map(({ key, root: relativeRoot, scope }) => {
106
108
  const root = path.resolve(workspaceRoot, relativeRoot);
107
- if (!existsSync(root)) return { app: key, scope: scope ?? ".", repositoryRoot: root, status: "unscanned", reason: "configured app root is missing", catalogVersion: catalogVersion(manifest), catalogDigest: sha256(canonicalJson(manifest)), scannedAt, host: null, commit: null, scanAgeSeconds: 0, rows: [], findings: [], violations: [], covered: [], problems: [], coverage: [{ path: ".", status: "unscanned", reason: "configured app root is missing" }], contentDigests: [], resultDigest: sha256(`${key}:missing`) };
108
- try { return asRecord(key, root, scope, scanner(root, manifest), manifest, versions, scannedAt); }
109
- catch (error) { return { app: key, scope: scope ?? ".", repositoryRoot: root, status: "unscanned", reason: error.message, catalogVersion: catalogVersion(manifest), catalogDigest: sha256(canonicalJson(manifest)), scannedAt, host: null, commit: null, scanAgeSeconds: 0, rows: [], findings: [], violations: [], covered: [], problems: [error.message], coverage: [{ path: ".", status: "unscanned", reason: error.message }], contentDigests: [], resultDigest: sha256(`${key}:${error.message}`) }; }
109
+ if (!existsSync(root)) return { app: key, scope: scope ?? ".", repositoryRoot: root, status: "unscanned", reason: "configured app root is missing", catalogVersion: catalogVersion(manifest), catalogDigest: sha256(canonicalJson(manifest)), scannedAt, host: null, commit: null, scanAgeSeconds: 0, rows: [], findings: [], violations: [], covered: [], problems: [], warnings: [], storageFindings: [], coverage: [{ path: ".", status: "unscanned", reason: "configured app root is missing" }], contentDigests: [], resultDigest: sha256(`${key}:missing`) };
110
+ try {
111
+ const scan = scanner(root, manifest);
112
+ const storageFindings = [
113
+ ...findInstallerCopyScripts({ appRoot: root, workspaceRoot }),
114
+ ...findLargeRepositorySubdirectories({ appRoot: root }),
115
+ ];
116
+ return asRecord(key, root, scope, scan, manifest, versions, scannedAt, storageFindings);
117
+ }
118
+ catch (error) { return { app: key, scope: scope ?? ".", repositoryRoot: root, status: "unscanned", reason: error.message, catalogVersion: catalogVersion(manifest), catalogDigest: sha256(canonicalJson(manifest)), scannedAt, host: null, commit: null, scanAgeSeconds: 0, rows: [], findings: [], violations: [], covered: [], problems: [error.message], warnings: [], storageFindings: [], coverage: [{ path: ".", status: "unscanned", reason: error.message }], contentDigests: [], resultDigest: sha256(`${key}:${error.message}`) }; }
110
119
  });
111
120
  const blockMap = new Map(), requestMap = new Map(), warnings = [];
112
121
  for (const record of records) {
122
+ if (record.storageFindings?.length) warnings.push({ app: record.app, message: "release storage ownership findings detected; release artifacts should stay in managed storage", findings: record.storageFindings, problems: [] });
113
123
  for (const request of record.exceptionRequests ?? []) if (!requestMap.has(request.contentHash)) requestMap.set(request.contentHash, request);
114
124
  if (record.enrollmentPending) {
115
125
  warnings.push({ app: record.app, message: "approval enrollment pending; ownership findings remain backlog diagnostics", findings: record.violations, problems: record.problems, scannerWarnings: record.warnings });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.132",
3
+ "version": "0.2.133",
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",
@@ -7,6 +7,7 @@ import { spawn } from "./background-process.mjs";
7
7
  import { pruneReleaseObjects } from "./prune-r2.mjs";
8
8
  import { loadReleaseToken } from "./release-token.mjs";
9
9
  import { registerRightAppsRelease } from "./rightapps-register.mjs";
10
+ import { recordPublishedVersion, resolveArtifactInput } from "./artifact-storage.mjs";
10
11
 
11
12
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
12
13
  const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
@@ -41,6 +42,10 @@ if (config?.distribution?.provider !== "r2-rightapps") {
41
42
  const target = config?.targets?.[platform];
42
43
  const artifacts = target?.updater?.artifacts ?? [];
43
44
  const installers = target?.installer?.artifacts ?? [];
45
+ const versionForStorage = config.version || target?.updater?.version;
46
+ const artifactFile = (declared) => versionForStorage
47
+ ? resolveArtifactInput({ root, app: config.app, version: versionForStorage, declared })
48
+ : path.resolve(root, declared);
44
49
 
45
50
  if (tier === "patch" && !installers.length) fail(`${config?.app ?? "app"} ${platform} patch has no installer.artifacts`);
46
51
  if (tier === "update" && !artifacts.length) fail(`${config?.app ?? "app"} ${platform} update has no updater.artifacts`);
@@ -57,7 +62,7 @@ if (!dryRun && (tier === "update" || artifacts.length > 0)) {
57
62
  if (tier === "patch") {
58
63
  for (const installer of installers) {
59
64
  assertCurrentKey(installer.key, "installer");
60
- const file = path.resolve(root, installer.file);
65
+ const file = artifactFile(installer.file);
61
66
  if (!dryRun) await access(file).catch(() => fail(`missing signed installer: ${installer.file}`));
62
67
  }
63
68
  const platforms = {};
@@ -69,8 +74,8 @@ if (tier === "patch") {
69
74
  assertCurrentKey(artifact.key, "updater");
70
75
  const installerKey = patchInstallerKey(artifact);
71
76
  assertCurrentKey(installerKey, "installer");
72
- const file = path.resolve(root, artifact.file);
73
- const signatureFile = path.resolve(root, artifact.signature);
77
+ const file = artifactFile(artifact.file);
78
+ const signatureFile = artifactFile(artifact.signature);
74
79
  if (!dryRun) {
75
80
  await access(file).catch(() => fail(`missing patch updater payload: ${artifact.file}`));
76
81
  await access(signatureFile).catch(() => fail(`missing patch updater signature: ${artifact.signature}`));
@@ -96,7 +101,7 @@ if (tier === "patch") {
96
101
  });
97
102
  }
98
103
  for (const installer of installers) {
99
- const file = path.resolve(root, installer.file);
104
+ const file = artifactFile(installer.file);
100
105
  if (!uploadedKeys.has(installer.key)) {
101
106
  console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installer.key}`);
102
107
  if (!dryRun) await run(process.execPath, [UPLOAD, file, installer.key, "public"], root);
@@ -121,6 +126,7 @@ if (tier === "patch") {
121
126
  private: target.updater.artifacts.map((artifact) => artifact.key),
122
127
  },
123
128
  });
129
+ if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform });
124
130
  process.exit(0);
125
131
  }
126
132
  const version = config.version || target.updater.version;
@@ -158,6 +164,7 @@ if (tier === "patch") {
158
164
  private: [],
159
165
  },
160
166
  });
167
+ if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform });
161
168
  process.exit(0);
162
169
  }
163
170
 
@@ -167,8 +174,8 @@ const updaterManifestArtifacts = new Map();
167
174
  const uploadedKeys = new Set();
168
175
  for (const artifact of artifacts) {
169
176
  assertCurrentKey(artifact.key, "updater");
170
- const file = path.resolve(root, artifact.file);
171
- const signatureFile = path.resolve(root, artifact.signature);
177
+ const file = artifactFile(artifact.file);
178
+ const signatureFile = artifactFile(artifact.signature);
172
179
  if (!dryRun) {
173
180
  await access(file).catch(() => fail(`missing updater artifact: ${artifact.file}`));
174
181
  await access(signatureFile).catch(() => fail(`missing updater signature: ${artifact.signature}`));
@@ -231,6 +238,7 @@ await pruneReleaseObjects({
231
238
  private: [...registrations.keys()],
232
239
  },
233
240
  });
241
+ if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform });
234
242
 
235
243
  async function fileMetadata(file) {
236
244
  const bytes = await readFile(file);
package/release.mjs CHANGED
@@ -22,9 +22,12 @@ import { patchTauriBundleType } from "./tauri-bundle-marker.mjs";
22
22
  import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
23
23
  import { terminateProcessTree } from "./heavy-command.mjs";
24
24
  import { materializeHardeningEvidence } from "./hardening-evidence.mjs";
25
+ import { signingReceiptPath } from "./signing-receipts.mjs";
26
+ import { validateNotarizationReceipt } from "./macos-notarization.mjs";
25
27
  import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
26
28
  import { runConfiguredChecks } from "./release-checks.mjs";
27
29
  import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
30
+ import { DEFAULT_STORAGE_THRESHOLD_BYTES, findInstallerCopyScripts, findLargeRepositorySubdirectories } from "./installer-storage-doctor.mjs";
28
31
 
29
32
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
30
33
  const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
@@ -200,6 +203,20 @@ if (opts.doctor) {
200
203
  }
201
204
  if (target.sign?.files?.length) console.log(`sign: ${target.sign.files.join(", ")}`);
202
205
  if (target.publishBlocked) console.log(`publishBlocked: ${target.publishBlocked}`);
206
+ const managedRoot = process.env.RIGHTKIT_MANAGED_ROOT;
207
+ const installerCopyFindings = findInstallerCopyScripts({
208
+ appRoot: root,
209
+ workspaceRoot: process.env.RIGHTKIT_WORKSPACE_ROOT ?? path.resolve(doctorInvocation.repoRoot, ".."),
210
+ });
211
+ const storageFindings = [
212
+ ...installerCopyFindings,
213
+ ...findLargeRepositorySubdirectories({ appRoot: root, managedRoot, thresholdBytes: Number(process.env.RIGHTKIT_ARTIFACT_STORAGE_THRESHOLD_BYTES ?? DEFAULT_STORAGE_THRESHOLD_BYTES) }),
214
+ ];
215
+ if (storageFindings.length) {
216
+ console.log("artifact-storage: report-only storage findings detected:");
217
+ for (const finding of storageFindings) console.log(` - ${finding.path}: ${finding.reason}`);
218
+ fail(`artifact-storage found ${storageFindings.length} repository storage finding(s); move outputs to RIGHT_RELEASE_ARTIFACT_DIR`);
219
+ } else console.log("artifact-storage: no installer-copy scripts detected");
203
220
 
204
221
  // Config above, MACHINE below. Printing config never told anyone whether this
205
222
  // box could finish a build; these checks do.
@@ -284,13 +301,13 @@ if (opts.platform === "win") {
284
301
  if (opts.platform !== "mac" || runMacPhase("package")) await runCommand(command, root);
285
302
 
286
303
  for (const rel of target.artifacts ?? []) {
287
- await mustExist(path.resolve(root, rel), `missing release artifact: ${rel}`);
304
+ await mustExist(resolveReleaseArtifactPath(root, rel), `missing release artifact: ${rel}`);
288
305
  }
289
306
 
290
307
  if (nativeFinalization) await completeNativeFinalization(nativeFinalization, { root, target, nativeAssembly });
291
308
 
292
309
  if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT) {
293
- const files = target.sign.files.map((p) => path.resolve(root, p));
310
+ const files = target.sign.files.map((p) => resolveReleaseArtifactPath(root, p));
294
311
  for (const file of files) await mustExist(file, `missing signing artifact: ${file}`);
295
312
  // Before the installer earns its own signature, prove it carries the bytes we
296
313
  // signed. An installer-only signature on a mutated payload verifies perfectly
@@ -298,7 +315,7 @@ if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_C
298
315
  await verifyWindowsEmbedding({ root, target, files, markers: bundleMarkerReceipts });
299
316
  await signWindows(files, "installer", root);
300
317
  if (target.postSign) await runCommand(target.postSign, root);
301
- const updaterFiles = [...new Set((target.updater?.artifacts ?? []).map((artifact) => path.resolve(root, artifact.file)))];
318
+ const updaterFiles = [...new Set((target.updater?.artifacts ?? []).map((artifact) => resolveReleaseArtifactPath(root, artifact.file)))];
302
319
  if (!updaterFiles.length) fail(`${config.app} win must declare updater.artifacts for post-code-signing minisign`);
303
320
  await run("node", [SIGN_UPDATER, ...(opts.dryRun ? ["--dry-run"] : []), ...updaterFiles.flatMap((file) => ["--file", file])], root);
304
321
  }
@@ -308,7 +325,7 @@ if (target.postSign && opts.platform !== "win" && (opts.platform !== "mac" || ru
308
325
  }
309
326
 
310
327
  if (opts.platform === "mac") {
311
- const archive = path.resolve(root, target.notarize.file);
328
+ const archive = resolveReleaseArtifactPath(root, target.notarize.file);
312
329
  await mustExist(archive, `missing macOS notarization archive: ${archive}`);
313
330
  await notarizeMacos(archive, root);
314
331
  }
@@ -326,7 +343,7 @@ if (opts.platform !== "mac" || runMacPhase("harden")) {
326
343
  await run("node", [
327
344
  HARDENING_SCAN,
328
345
  ...(hardeningEvidence ? ["--allow-evidence", hardeningEvidence] : []),
329
- ...scanTargets.map((p) => path.resolve(root, p)),
346
+ ...scanTargets.map((p) => resolveReleaseArtifactPath(root, p)),
330
347
  ], root);
331
348
  }
332
349
  }
@@ -337,7 +354,7 @@ if (opts.upload && target.publish) {
337
354
 
338
355
  if (opts.upload && !target.publish) {
339
356
  for (const item of target.upload ?? []) {
340
- const file = path.resolve(root, item.file);
357
+ const file = resolveReleaseArtifactPath(root, item.file);
341
358
  await mustExist(file, `missing upload artifact: ${item.file}`);
342
359
  await run("node", [UPLOAD_LARGE, file, item.key, item.bucket ?? "public"], root);
343
360
  }
@@ -358,6 +375,20 @@ function usage(code, message) {
358
375
  process.exit(code);
359
376
  }
360
377
 
378
+ // Product package scripts may emit final installer bytes directly into the
379
+ // managed artifact directory. Keep legacy target-relative paths working while
380
+ // preferring managed output when the local path does not exist.
381
+ function resolveReleaseArtifactPath(root, declared) {
382
+ const local = path.resolve(root, declared);
383
+ if (existsSync(local)) return local;
384
+ const managed = process.env.RIGHT_RELEASE_ARTIFACT_DIR;
385
+ if (managed) {
386
+ const candidate = path.join(path.resolve(managed), path.basename(declared));
387
+ if (existsSync(candidate)) return candidate;
388
+ }
389
+ return local;
390
+ }
391
+
361
392
  function fail(message) {
362
393
  console.error(`right-release: ${message}`);
363
394
  process.exit(1);
@@ -793,9 +824,7 @@ async function signWindows(files, phase, root) {
793
824
  }
794
825
 
795
826
  function windowsReceiptPath(root, phase) {
796
- const configured = phase === "raw-exe" ? target.sign?.receipt : target.sign?.installerReceipt;
797
- if (configured) return path.resolve(root, configured);
798
- return path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `windows-${phase}.json`);
827
+ return signingReceiptPath({ appRoot: root, target, platform: "win", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
799
828
  }
800
829
 
801
830
  async function signMacos(files, root) {
@@ -815,6 +844,7 @@ async function notarizeMacos(archive, root) {
815
844
  if (opts.dryRun) args.push("--dry-run");
816
845
  args.push("--receipt", receipt);
817
846
  if (target.notarize.profile) args.push("--profile", target.notarize.profile);
847
+ if (target.notarize.app) args.push("--app", resolveReleaseArtifactPath(root, target.notarize.app));
818
848
  args.push(archive);
819
849
  if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
820
850
  console.log("right-release: notarizing macOS archive");
@@ -823,9 +853,7 @@ async function notarizeMacos(archive, root) {
823
853
  }
824
854
 
825
855
  function macosReceiptPath(root, phase) {
826
- const configured = phase === "signing" ? target.sign?.receipt : target.notarize?.receipt;
827
- if (configured) return path.resolve(root, configured);
828
- return path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `macos-${phase}.json`);
856
+ return signingReceiptPath({ appRoot: root, target, platform: "mac", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
829
857
  }
830
858
 
831
859
  function verifyMacosSigningReceipt(receipt, files) {
@@ -843,12 +871,8 @@ function verifyMacosSigningReceipt(receipt, files) {
843
871
  }
844
872
 
845
873
  function verifyMacosNotarizationReceipt(receipt, archive) {
846
- if (!existsSync(receipt)) fail(`missing macOS notarization receipt: ${receipt}`);
847
- let evidence;
848
- try { evidence = JSON.parse(readFileSync(receipt, "utf8")); } catch { fail(`invalid macOS notarization receipt: ${receipt}`); }
849
- const sha256 = createHash("sha256").update(readFileSync(archive)).digest("hex");
850
- if (evidence.schema !== 1 || evidence.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT || path.resolve(evidence.archive?.file ?? "") !== archive
851
- || evidence.archive?.sha256 !== sha256 || evidence.notarization?.status !== "Accepted") fail(`invalid macOS notarization receipt evidence: ${receipt}`);
874
+ try { validateNotarizationReceipt({ receiptPath: receipt, archive }); }
875
+ catch (error) { fail(error.message); }
852
876
  }
853
877
 
854
878
  /**
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from "node:fs";
3
+ import net from "node:net";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { randomBytes, timingSafeEqual, randomUUID } from "node:crypto";
7
+ import { fileURLToPath } from "node:url";
8
+ import { spawnSync } from "./background-process.mjs";
9
+
10
+ export const AGENT_LABEL = "dev.rightkit.signing-agent";
11
+ export const MAX_FRAME = 1024 * 1024;
12
+ export class SigningAgentError extends Error {
13
+ constructor(code, message) { super(message); this.name = "SigningAgentError"; this.code = code; }
14
+ }
15
+ const fail = (code, message) => { throw new SigningAgentError(code, message); };
16
+ export function signingAgentPaths(home = os.userInfo().homedir) {
17
+ const directory = path.join(home, "Library/Application Support/Orthic Labs/shared/signing-agent");
18
+ return { directory, socket: path.join(directory, "agent.sock"), token: path.join(directory, "token"),
19
+ config: path.join(directory, "config.json"), runtime: path.join(directory, "runtime"),
20
+ log: path.join(directory, "requests.jsonl"), plist: path.join(home, "Library/LaunchAgents", `${AGENT_LABEL}.plist`) };
21
+ }
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")];
24
+ }
25
+ export const within = (root, file) => file === root || file.startsWith(`${root}${path.sep}`);
26
+ export function privatePath(file, mode, { io = fs, uid = process.getuid?.(), kind = "file" } = {}) {
27
+ const info = io.lstatSync(file);
28
+ if (info.uid !== uid || (info.mode & 0o777) !== mode || info.isSymbolicLink()
29
+ || !(kind === "directory" ? info.isDirectory() : kind === "socket" ? info.isSocket() : info.isFile())) {
30
+ fail("MAC_SIGNING_AGENT_UNSAFE_PATH", "Signing agent path has invalid ownership, type, or permissions");
31
+ }
32
+ }
33
+ export function ensurePrivateDirectory(directory, { io = fs, uid = process.getuid?.() } = {}) {
34
+ // Reject symlink ancestors before mkdir/chmod. Existing public ancestors are
35
+ // fine only if no other uid can replace their children.
36
+ const parts = path.resolve(directory).split(path.sep).filter(Boolean);
37
+ let current = path.parse(directory).root;
38
+ for (const part of parts) {
39
+ current = path.join(current, part);
40
+ if (!io.existsSync(current)) io.mkdirSync(current, { mode: 0o700 });
41
+ const info = io.lstatSync(current);
42
+ if (!info.isDirectory() || info.isSymbolicLink() || ![0, uid].includes(info.uid)
43
+ || ((info.mode & 0o022) && !(info.mode & 0o1000))) fail("MAC_SIGNING_AGENT_UNSAFE_PATH", "Unsafe signing agent directory ancestor");
44
+ }
45
+ io.chmodSync(directory, 0o700);
46
+ privatePath(directory, 0o700, { io, uid, kind: "directory" });
47
+ }
48
+ function shape(value, fields) {
49
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !fields.includes(key))) {
50
+ fail("MAC_SIGNING_AGENT_PROTOCOL", "Unknown request fields or invalid object");
51
+ }
52
+ }
53
+ function absolute(value) {
54
+ if (typeof value !== "string" || !path.isAbsolute(value) || value.includes("\0")) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Paths must be absolute");
55
+ return path.resolve(value);
56
+ }
57
+ export function validateSigningRequest(request, { roots, stagingRoot, io = fs } = {}) {
58
+ const common = ["schema", "token", "type", "allowedRoots", "cwd", "timeoutMs"];
59
+ if (request?.type === "health") {
60
+ shape(request, ["schema", "token", "type"]);
61
+ if (request.schema !== 1) fail("MAC_SIGNING_AGENT_PROTOCOL", "Unsupported protocol schema");
62
+ return request;
63
+ }
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) {
67
+ fail("MAC_SIGNING_AGENT_PROTOCOL", "Invalid schema or signing deadline");
68
+ }
69
+ if (!Array.isArray(request.allowedRoots) || !request.allowedRoots.length || request.allowedRoots.length > 32) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Allowed roots required");
70
+ // Callers may narrow installed policy, never widen it. Check lexical & real
71
+ // containment, so symlinks cannot turn a workspace path into another root.
72
+ const installed = roots.map(absolute);
73
+ const allowed = request.allowedRoots.map((root) => {
74
+ const lexical = absolute(root);
75
+ 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);
77
+ if (![...installed, stagingRoot].filter(Boolean).some((base) => {
78
+ try { return within(io.realpathSync(base), canonical); } catch { return false; }
79
+ })) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Allowed root escapes installed policy");
80
+ return { lexical, canonical };
81
+ });
82
+ function checked(file, { directory = false, entitlements = false } = {}) {
83
+ const lexical = absolute(file);
84
+ const canonical = io.realpathSync(lexical);
85
+ if (!allowed.some((root) => within(root.lexical, lexical) && within(root.canonical, canonical))
86
+ || (!entitlements && stagingRoot && within(stagingRoot, canonical))) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Signing path escapes allowed roots");
87
+ const info = io.statSync(canonical);
88
+ if (directory ? !info.isDirectory() : !info.isFile() && (!info.isDirectory() || entitlements)) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Invalid signing path type");
89
+ return canonical;
90
+ }
91
+ const cwd = checked(request.cwd, { directory: true });
92
+ if (request.type === "updater") {
93
+ if (!Array.isArray(request.files) || !request.files.length || request.files.length > 4096) fail("MAC_SIGNING_AGENT_PROTOCOL", "Updater files required");
94
+ const files = request.files.map((file) => {
95
+ const canonical = checked(file);
96
+ if (!io.statSync(canonical).isFile()) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Updater artifact must be a file");
97
+ // The signer creates <artifact>.sig; reject existing output symlinks too.
98
+ if (io.existsSync(`${canonical}.sig`) || (() => { try { return io.lstatSync(`${canonical}.sig`).isSymbolicLink(); } catch { return false; } })()) {
99
+ if (io.lstatSync(`${canonical}.sig`).isSymbolicLink()) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Updater signature cannot be a symlink");
100
+ checked(`${canonical}.sig`);
101
+ }
102
+ return canonical;
103
+ });
104
+ return { ...request, cwd, files };
105
+ }
106
+ if (!Array.isArray(request.items) || !request.items.length || request.items.length > 4096) fail("MAC_SIGNING_AGENT_PROTOCOL", "Codesign items required");
107
+ const items = request.items.map((item) => {
108
+ shape(item, ["path", "identity", "entitlements", "runtime", "timestamp", "force", "preserveEntitlements"]);
109
+ if (typeof item.identity !== "string" || !item.identity.trim() || item.identity === "-" || item.identity.includes("\0") || item.identity.length > 512) fail("MAC_SIGNING_AGENT_PROTOCOL", "Real signing identity required");
110
+ for (const field of ["runtime", "timestamp", "force", "preserveEntitlements"]) if (item[field] !== undefined && typeof item[field] !== "boolean") fail("MAC_SIGNING_AGENT_PROTOCOL", "Signing options must be boolean");
111
+ return { ...item, path: checked(item.path), ...(item.entitlements !== undefined ? { entitlements: checked(item.entitlements, { entitlements: true }) } : {}) };
112
+ });
113
+ return { ...request, cwd, items };
114
+ }
115
+ export function authenticateSigningToken(candidate, token) {
116
+ if (typeof candidate !== "string" || !/^[a-f0-9]{64}$/.test(candidate) || !timingSafeEqual(Buffer.from(candidate), Buffer.from(token))) {
117
+ fail("MAC_SIGNING_AGENT_AUTH", "Signing agent authentication failed");
118
+ }
119
+ }
120
+ export function executeSigningRequest(request, { run = spawnSync, home = os.userInfo().homedir,
121
+ 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 };
123
+ const deadline = now() + request.timeoutMs;
124
+ const env = { HOME: home, PATH: environmentPath, TMPDIR: path.join(signingAgentPaths(home).directory, "tmp") };
125
+ const results = [];
126
+ const outputLimit = Math.min(4096, Math.floor(MAX_FRAME / (4 * (request.items?.length ?? request.files.length))));
127
+ const invoke = (command, args, sensitive = false) => {
128
+ const remaining = deadline - now();
129
+ if (remaining <= 0) fail("MAC_SIGNING_AGENT_TIMEOUT", "Signing deadline expired");
130
+ const r = run(command, args, { cwd: request.cwd, env, encoding: "utf8", stdio: "pipe", shell: false,
131
+ timeout: remaining, killSignal: "SIGKILL", maxBuffer: MAX_FRAME });
132
+ const result = { status: r.error ? 1 : r.status ?? 1, stdout: sensitive ? "" : String(r.stdout ?? "").slice(-outputLimit),
133
+ stderr: sensitive ? "" : String(r.stderr ?? "").slice(-outputLimit) };
134
+ if (result.status !== 0) fail("MAC_SIGNING_AGENT_JOB_FAILED", sensitive ? "Updater signing failed" : `Codesign failed (exit ${result.status}): ${result.stderr}`);
135
+ return { r, result };
136
+ };
137
+ try {
138
+ if (request.type === "updater") {
139
+ const { r } = invoke("/usr/bin/security", ["find-generic-password", "-a", username, "-s", "rightsuite-updater-key", "-w"], true);
140
+ const key = String(r.stdout ?? "").trim();
141
+ if (!key) fail("MAC_SIGNING_UPDATER_KEY_UNAVAILABLE", "Shared updater key unavailable");
142
+ env.TAURI_SIGNING_PRIVATE_KEY = key;
143
+ env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "";
144
+ for (const file of request.files) results.push(invoke(pnpm, ["exec", "tauri", "signer", "sign", file], true).result);
145
+ } else {
146
+ // Preserve caller's inside-out ordering. No arbitrary argv reaches spawn.
147
+ for (const item of request.items) {
148
+ const args = [...(item.force ? ["--force"] : []), ...(item.runtime ? ["--options", "runtime"] : []),
149
+ ...(item.timestamp ? ["--timestamp"] : []), "--sign", item.identity,
150
+ ...(item.entitlements ? ["--entitlements", item.entitlements] : []),
151
+ ...(item.preserveEntitlements ? ["--preserve-metadata=entitlements"] : []), item.path];
152
+ results.push(invoke("/usr/bin/codesign", args).result);
153
+ }
154
+ }
155
+ return { schema: 1, ok: true, results };
156
+ } catch (error) {
157
+ if (request.type === "updater") fail(error.code?.startsWith("MAC_SIGNING_") ? error.code : "MAC_SIGNING_AGENT_JOB_FAILED", "Updater signing failed inside signing agent");
158
+ throw error;
159
+ } finally { delete env.TAURI_SIGNING_PRIVATE_KEY; delete env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD; }
160
+ }
161
+ export function managedSigningLog(file, event, { io = fs } = {}) {
162
+ if (io.existsSync(file)) {
163
+ privatePath(file, 0o600, { io });
164
+ if (io.statSync(file).size > 5 * 1024 * 1024) {
165
+ const previous = `${file}.1`;
166
+ if (io.existsSync(previous)) { privatePath(previous, 0o600, { io }); io.unlinkSync(previous); }
167
+ io.renameSync(file, previous);
168
+ }
169
+ }
170
+ // Only service-authored metadata is logged, never request/token/key/argv/output.
171
+ io.appendFileSync(file, `${JSON.stringify({ at: new Date().toISOString(), ...event })}\n`, { mode: 0o600 });
172
+ }
173
+ export async function startSigningAgent({ paths = signingAgentPaths(), config, io = fs, run = spawnSync,
174
+ log = (event) => managedSigningLog(paths.log, event), createServer = net.createServer } = {}) {
175
+ privatePath(paths.directory, 0o700, { io, kind: "directory" });
176
+ privatePath(paths.config, 0o600, { io });
177
+ config ??= JSON.parse(io.readFileSync(paths.config, "utf8"));
178
+ const token = randomBytes(32).toString("hex"); // Rotate on every start, including every boot.
179
+ for (const file of [paths.socket, paths.token]) {
180
+ try { io.lstatSync(file); } catch (error) { if (error.code === "ENOENT") continue; throw error; }
181
+ privatePath(file, 0o600, { io, kind: file === paths.socket ? "socket" : "file" });
182
+ io.unlinkSync(file);
183
+ }
184
+ io.writeFileSync(paths.token, token, { mode: 0o600, flag: "wx" });
185
+ let queue = Promise.resolve();
186
+ let pending = 0;
187
+ const server = createServer((socket) => {
188
+ socket.on("error", () => {});
189
+ socket.setTimeout(200000, () => socket.destroy());
190
+ let input = Buffer.alloc(0), submitted = false;
191
+ socket.on("data", (chunk) => {
192
+ if (submitted) { log({ event: "request.rejected", code: "MAC_SIGNING_AGENT_PROTOCOL" }); socket.destroy(); return; }
193
+ input = Buffer.concat([input, chunk]);
194
+ if (input.length > MAX_FRAME) { log({ event: "request.rejected", code: "MAC_SIGNING_AGENT_PROTOCOL" }); socket.destroy(); return; }
195
+ const end = input.indexOf(10);
196
+ if (end < 0) return;
197
+ submitted = true;
198
+ const id = randomUUID();
199
+ let request;
200
+ const respond = (report) => { if (!socket.destroyed) socket.end(`${JSON.stringify(report)}\n`); };
201
+ try {
202
+ if (end !== input.length - 1) fail("MAC_SIGNING_AGENT_PROTOCOL", "One request per connection required");
203
+ request = JSON.parse(input.subarray(0, end).toString("utf8"));
204
+ authenticateSigningToken(request?.token, token);
205
+ request = validateSigningRequest(request, { roots: config.roots, stagingRoot: path.join(paths.directory, "requests"), io });
206
+ if (pending >= 32) fail("MAC_SIGNING_AGENT_BUSY", "Signing queue is full");
207
+ } catch (error) {
208
+ const code = error.code?.startsWith("MAC_SIGNING_") ? error.code : "MAC_SIGNING_AGENT_PROTOCOL";
209
+ log({ event: "request.rejected", id, code });
210
+ respond({ schema: 1, ok: false, code, message: "Signing request rejected" });
211
+ return;
212
+ }
213
+ const receivedAt = Date.now();
214
+ pending++;
215
+ log({ event: "request.accepted", id, type: request.type, count: request.items?.length ?? request.files?.length ?? 0 });
216
+ queue = queue.then(() => {
217
+ if (socket.destroyed) { log({ event: "request.cancelled", id, type: request.type }); return; }
218
+ log({ event: "request.started", id, type: request.type });
219
+ try {
220
+ if (request.type !== "health") {
221
+ request.timeoutMs -= Date.now() - receivedAt;
222
+ if (request.timeoutMs <= 0) fail("MAC_SIGNING_AGENT_TIMEOUT", "Signing deadline expired in queue");
223
+ // Revalidate immediately before effects, not merely on admission.
224
+ request = validateSigningRequest(request, { roots: config.roots, stagingRoot: path.join(paths.directory, "requests"), io });
225
+ }
226
+ const report = executeSigningRequest(request, { run, home: config.home, username: config.username, pnpm: config.pnpm, environmentPath: config.environmentPath });
227
+ respond(report);
228
+ log({ event: "request.completed", id, type: request.type });
229
+ } catch (error) {
230
+ 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 });
232
+ log({ event: "request.failed", id, type: request.type, code });
233
+ }
234
+ }).finally(() => { pending--; }).catch(() => { socket.destroy(); });
235
+ });
236
+ });
237
+ await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); });
238
+ io.chmodSync(paths.socket, 0o600);
239
+ log({ event: "service.started", pid: process.pid });
240
+ server.on("error", () => { log({ event: "service.failed", code: "MAC_SIGNING_AGENT_SOCKET" }); });
241
+ return server;
242
+ }
243
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
244
+ process.umask(0o077);
245
+ startSigningAgent().then((server) => {
246
+ const stop = () => { managedSigningLog(signingAgentPaths().log, { event: "service.stopping" }); server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 2000).unref(); };
247
+ process.on("SIGTERM", stop); process.on("SIGINT", stop);
248
+ }).catch((error) => {
249
+ const event = { event: "service.failed", code: error.code ?? "MAC_SIGNING_AGENT_START_FAILED" };
250
+ try { managedSigningLog(signingAgentPaths().log, event); } catch {}
251
+ console.error(JSON.stringify(event)); process.exitCode = 1;
252
+ });
253
+ }