@rightkit/release 0.2.82 → 0.2.83

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.
package/build-release.mjs CHANGED
@@ -42,7 +42,7 @@ import { cancelAndReapManagedRequest, firstManagedRequestId, releaseProgressLimi
42
42
  // These four files are the ones whose behaviour the cached target directory can
43
43
  // outlive.
44
44
 
45
- const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
45
+ const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "notarize-macos.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
46
46
  const PIPELINE_FINGERPRINT = createHash("sha256")
47
47
  .update(PIPELINE_FINGERPRINT_SOURCES.map((file) => `${file}:${createHash("sha256").update(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), file))).digest("hex")}`).join("\n"))
48
48
  .digest("hex")
@@ -177,6 +177,17 @@ try {
177
177
  const configured = phase === "raw-exe" ? target.sign?.receipt : phase === "installer" ? target.sign?.installerReceipt : undefined;
178
178
  return configured ? path.resolve(appRoot, configured) : path.join(receiptRoot, `windows-${phase}.json`);
179
179
  }),
180
+ } : platform === "mac" ? {
181
+ contract: target.signingContract ?? "<missing>",
182
+ pipeline: PIPELINE_FINGERPRINT,
183
+ configSha256: hashFile(configPath),
184
+ // Mac portable releases produce one Developer ID receipt before packaging
185
+ // and one Accepted notarization receipt after. Both must be part of cache
186
+ // identity and seal evidence, including configured receipt overrides.
187
+ receiptInputs: [
188
+ target.sign?.receipt ? path.resolve(appRoot, target.sign.receipt) : path.join(receiptRoot, "macos-signing.json"),
189
+ target.notarize?.receipt ? path.resolve(appRoot, target.notarize.receipt) : path.join(receiptRoot, "macos-notarization.json"),
190
+ ],
180
191
  } : null;
181
192
  if (signingIdentity) inputHashes[".right-release/signing-identity.json"] = hashFileText(JSON.stringify(signingIdentity));
182
193
  const cargoLock = nativeLayout.lockPath;
@@ -0,0 +1 @@
1
+ export const MACOS_PORTABLE_SIGNING_CONTRACT = "macos-developer-id-notarized-portable-v1";
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { notarytoolAuthArgs } from "./notary-auth.mjs";
7
+ import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
8
+
9
+ const args = process.argv.slice(2);
10
+ const dryRun = args.includes("--dry-run");
11
+ const receiptIndex = args.indexOf("--receipt");
12
+ const profileIndex = args.indexOf("--profile");
13
+ const receiptPath = receiptIndex >= 0 ? args[receiptIndex + 1] : null;
14
+ const profile = profileIndex >= 0 ? args[profileIndex + 1] : undefined;
15
+ const archiveArgs = args.filter((arg, index) => arg !== "--dry-run" && arg !== "--receipt" && arg !== "--profile" && (receiptIndex < 0 || index !== receiptIndex + 1) && (profileIndex < 0 || index !== profileIndex + 1));
16
+ if (archiveArgs.length !== 1) fail("usage: node notarize-macos.mjs [--dry-run] --receipt <path> [--profile <name>] <archive>");
17
+ if (!receiptPath) fail("--receipt requires a path");
18
+ const archive = path.resolve(archiveArgs[0]);
19
+ if (dryRun) {
20
+ console.log(`dry-run: notarize ${archive}`);
21
+ process.exit(0);
22
+ }
23
+ if (process.platform !== "darwin") fail("macOS notarization must run on macOS");
24
+ if (!existsSync(archive)) fail(`missing archive: ${archive}`);
25
+
26
+ let authArgs;
27
+ try { authArgs = notarytoolAuthArgs({ profile }); } catch (error) { fail(error.message); }
28
+ const output = run("xcrun", ["notarytool", "submit", archive, "--wait", "--output-format", "json", ...authArgs]);
29
+ let result;
30
+ try { result = JSON.parse(output); } catch { fail(`notarytool did not return JSON for ${archive}`); }
31
+ if (result.status !== "Accepted") fail(`notarization was not accepted for ${archive}: ${result.status ?? "missing status"}`);
32
+
33
+ const absoluteReceipt = path.resolve(receiptPath);
34
+ mkdirSync(path.dirname(absoluteReceipt), { recursive: true });
35
+ writeFileSync(absoluteReceipt, `${JSON.stringify({
36
+ schema: 1,
37
+ signingContract: MACOS_PORTABLE_SIGNING_CONTRACT,
38
+ archive: { file: archive, ...fileEvidence(archive) },
39
+ notarization: { status: result.status, id: result.id ?? result.submissionId ?? null },
40
+ }, null, 2)}\n`);
41
+
42
+ function fileEvidence(file) {
43
+ const bytes = readFileSync(file);
44
+ return { sha256: createHash("sha256").update(bytes).digest("hex"), sizeBytes: statSync(file).size };
45
+ }
46
+
47
+ function run(cmd, runArgs) {
48
+ const result = spawnSync(cmd, runArgs, { encoding: "utf8" });
49
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
50
+ if (result.status !== 0) fail(`${cmd} ${runArgs.join(" ")} failed: ${output}`);
51
+ return output;
52
+ }
53
+
54
+ function fail(message) {
55
+ console.error(`right-notarize-macos: ${message}`);
56
+ process.exit(1);
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.82",
3
+ "version": "0.2.83",
4
4
  "description": "Portable Right Suite release CLI/SDK: signed native archives, direct bootstrap transactions, immutable GitHub Release upload, R2 bootstrap publication, and add-on adoption.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
package/release.mjs CHANGED
@@ -22,11 +22,14 @@ 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 { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
25
26
 
26
27
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
27
28
  const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
28
29
  const UPLOAD_LARGE = path.resolve(TOOL_ROOT, "upload-large.mjs");
29
30
  const SIGN_WINDOWS = path.resolve(TOOL_ROOT, "sign-windows.mjs");
31
+ const SIGN_MACOS = path.resolve(TOOL_ROOT, "sign-macos.mjs");
32
+ const NOTARIZE_MACOS = path.resolve(TOOL_ROOT, "notarize-macos.mjs");
30
33
  const SIGN_UPDATER = path.resolve(TOOL_ROOT, "sign-updater.mjs");
31
34
  const RIGHTKIT_VERSIONS = JSON.parse(readFileSync(path.resolve(TOOL_ROOT, "rightkit-versions.json"), "utf8"));
32
35
  const VERSION = RIGHTKIT_VERSIONS.stagedNpm?.["@rightkit/release"] ?? RIGHTKIT_VERSIONS.npm["@rightkit/release"];
@@ -124,6 +127,12 @@ if (opts.platform === "win") {
124
127
  if (isNsis && target.sign.prePackageFiles.some((file) => target.sign.files.includes(file))) fail(`${config.app ?? "app"} win raw EXE and installer signing files must be distinct`);
125
128
  if (isPortable && target.sign?.files?.length) fail(`${config.app ?? "app"} win portable release must sign every executable through sign.prePackageFiles and omit sign.files`);
126
129
  }
130
+ if (opts.platform === "mac") {
131
+ if (target.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT) fail(`${config.app ?? "app"} mac must declare signingContract: ${MACOS_PORTABLE_SIGNING_CONTRACT}`);
132
+ if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} mac must declare prePackage to materialize Developer ID signing files`);
133
+ if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} mac must declare at least one sign.prePackageFiles`);
134
+ if (!target.notarize?.file) fail(`${config.app ?? "app"} mac must declare notarize.file`);
135
+ }
127
136
  if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
128
137
  /**
129
138
  * Assemble preflight inputs from the app's own files. Kept here (not in
@@ -240,6 +249,11 @@ if (opts.platform === "win") {
240
249
  signedFiles: rawFiles,
241
250
  });
242
251
  }
252
+ } else if (opts.platform === "mac") {
253
+ const signingFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
254
+ await runCommand(target.prePackage, root);
255
+ for (const file of signingFiles) await mustExist(file, `missing macOS Developer ID signing artifact: ${file}`);
256
+ await signMacos(signingFiles, root);
243
257
  } else if (nativeAssembly) {
244
258
  if (nativeAssembly.packageHook) await runCommand(nativeAssembly.packageHook, root);
245
259
  if (nativeFinalizer) {
@@ -279,6 +293,12 @@ if (target.postSign && opts.platform !== "win") {
279
293
  await runCommand(target.postSign, root);
280
294
  }
281
295
 
296
+ if (opts.platform === "mac") {
297
+ const archive = path.resolve(root, target.notarize.file);
298
+ await mustExist(archive, `missing macOS notarization archive: ${archive}`);
299
+ await notarizeMacos(archive, root);
300
+ }
301
+
282
302
  {
283
303
  const scanTargets = target.hardening ?? target.artifacts ?? [];
284
304
  if (scanTargets.length) {
@@ -651,9 +671,14 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
651
671
  if (!source.startsWith("# Managed by right-git")) {
652
672
  fail(`${appName} ${workflow} is not marked as right-git managed`);
653
673
  }
674
+ const requestedSecrets = [...source.matchAll(/\bsecrets\.([A-Z0-9_]+)/gi)].map((match) => match[1]);
675
+ const allowedMacosSecrets = new Set(["APPLE_CERTIFICATE_BASE64", "APPLE_CERTIFICATE_PASSWORD", "APPLE_KEYCHAIN_PASSWORD", "APPLE_API_KEY_BASE64", "APPLE_API_KEY", "APPLE_API_ISSUER"]);
676
+ const macosSigningAllowed = workflow === "release-candidate.yml" && manifest?.candidate?.signMacos === true;
654
677
  if (/^\s*permissions:\s*write-all\s*$/mi.test(source)
655
678
  || /^\s*(?:actions|attestations|checks|contents|deployments|discussions|id-token|issues|models|packages|pages|pull-requests|security-events|statuses):\s*write\s*$/mi.test(source)
656
- || /\bsecrets\s*(?:\.|\[)/i.test(source)) {
679
+ || /pull_request_target\s*:/i.test(source)
680
+ || /\bsecrets\s*\[/i.test(source)
681
+ || requestedSecrets.some((name) => !macosSigningAllowed || !allowedMacosSecrets.has(name))) {
657
682
  fail(`${appName} right-git managed ${workflow} requests release-capable permissions or secrets`);
658
683
  }
659
684
  }
@@ -705,6 +730,58 @@ function windowsReceiptPath(root, phase) {
705
730
  return path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `windows-${phase}.json`);
706
731
  }
707
732
 
733
+ async function signMacos(files, root) {
734
+ const receipt = macosReceiptPath(root, "signing");
735
+ const args = [SIGN_MACOS];
736
+ if (opts.dryRun) args.push("--dry-run");
737
+ args.push("--receipt", receipt, ...files);
738
+ if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
739
+ console.log("right-release: signing macOS Developer ID files");
740
+ await run("node", args, root);
741
+ if (!opts.dryRun) verifyMacosSigningReceipt(receipt, files);
742
+ }
743
+
744
+ async function notarizeMacos(archive, root) {
745
+ const receipt = macosReceiptPath(root, "notarization");
746
+ const args = [NOTARIZE_MACOS];
747
+ if (opts.dryRun) args.push("--dry-run");
748
+ args.push("--receipt", receipt);
749
+ if (target.notarize.profile) args.push("--profile", target.notarize.profile);
750
+ args.push(archive);
751
+ if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
752
+ console.log("right-release: notarizing macOS archive");
753
+ await run("node", args, root);
754
+ if (!opts.dryRun) verifyMacosNotarizationReceipt(receipt, archive);
755
+ }
756
+
757
+ function macosReceiptPath(root, phase) {
758
+ const configured = phase === "signing" ? target.sign?.receipt : target.notarize?.receipt;
759
+ if (configured) return path.resolve(root, configured);
760
+ return path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `macos-${phase}.json`);
761
+ }
762
+
763
+ function verifyMacosSigningReceipt(receipt, files) {
764
+ if (!existsSync(receipt)) fail(`missing macOS Developer ID signing receipt: ${receipt}`);
765
+ let evidence;
766
+ try { evidence = JSON.parse(readFileSync(receipt, "utf8")); } catch { fail(`invalid macOS Developer ID signing receipt: ${receipt}`); }
767
+ const expectedIdentity = process.env.APPLE_DEVELOPER_ID?.trim();
768
+ const expected = files.map((file) => path.resolve(file)).sort();
769
+ const actual = Array.isArray(evidence.files) ? evidence.files.map((item) => path.resolve(item.file)).sort() : [];
770
+ const hashes = new Map(files.map((file) => [path.resolve(file), createHash("sha256").update(readFileSync(file)).digest("hex")]));
771
+ if (evidence.schema !== 1 || evidence.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT || !expectedIdentity || evidence.identity !== expectedIdentity
772
+ || JSON.stringify(expected) !== JSON.stringify(actual)
773
+ || evidence.files.some((item) => item.codesign !== "strict-valid" || item.identity !== expectedIdentity || item.timestampPresent !== true || item.after?.sha256 !== hashes.get(path.resolve(item.file)))) fail(`invalid macOS Developer ID signing receipt evidence: ${receipt}`);
774
+ }
775
+
776
+ function verifyMacosNotarizationReceipt(receipt, archive) {
777
+ if (!existsSync(receipt)) fail(`missing macOS notarization receipt: ${receipt}`);
778
+ let evidence;
779
+ try { evidence = JSON.parse(readFileSync(receipt, "utf8")); } catch { fail(`invalid macOS notarization receipt: ${receipt}`); }
780
+ const sha256 = createHash("sha256").update(readFileSync(archive)).digest("hex");
781
+ if (evidence.schema !== 1 || evidence.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT || path.resolve(evidence.archive?.file ?? "") !== archive
782
+ || evidence.archive?.sha256 !== sha256 || evidence.notarization?.status !== "Accepted") fail(`invalid macOS notarization receipt evidence: ${receipt}`);
783
+ }
784
+
708
785
  /**
709
786
  * Close the gap between "the raw EXE was signed" and "the installer ships that
710
787
  * EXE". Three independent facts, each able to fail on its own:
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "stagedNpm": {
16
16
  "@rightkit/ax": "0.2.1",
17
- "@rightkit/git": "0.2.5",
17
+ "@rightkit/git": "0.2.7",
18
18
  "@rightkit/hooks": "0.1.1",
19
19
  "@rightkit/legal": "0.3.1",
20
20
  "@rightkit/legal-ui": "0.1.1",
@@ -22,7 +22,7 @@
22
22
  "@rightkit/logs": "0.1.4",
23
23
  "@rightkit/platform-ui": "0.1.1",
24
24
  "@rightkit/qa": "0.2.1",
25
- "@rightkit/release": "0.2.82",
25
+ "@rightkit/release": "0.2.83",
26
26
  "@rightkit/tauri": "0.1.1",
27
27
  "@rightkit/updates": "0.2.4"
28
28
  },
@@ -34,7 +34,9 @@
34
34
  "0.2.1",
35
35
  "0.2.2",
36
36
  "0.2.3",
37
- "0.2.4"
37
+ "0.2.4",
38
+ "0.2.5",
39
+ "0.2.6"
38
40
  ],
39
41
  "@rightkit/legal-ui": [
40
42
  "0.1.0"
@@ -81,7 +83,8 @@
81
83
  "0.2.73",
82
84
  "0.2.74",
83
85
  "0.2.75",
84
- "0.2.76"
86
+ "0.2.76",
87
+ "0.2.82"
85
88
  ],
86
89
  "@rightkit/qa": [
87
90
  "0.1.0",
package/sign-macos.mjs ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
7
+
8
+ const args = process.argv.slice(2);
9
+ const dryRun = args.includes("--dry-run");
10
+ const receiptIndex = args.indexOf("--receipt");
11
+ const receiptPath = receiptIndex >= 0 ? args[receiptIndex + 1] : null;
12
+ const files = args
13
+ .filter((arg, index) => arg !== "--dry-run" && arg !== "--receipt" && (receiptIndex < 0 || index !== receiptIndex + 1))
14
+ .map((arg) => path.resolve(arg));
15
+
16
+ if (!files.length) fail("usage: node sign-macos.mjs [--dry-run] --receipt <path> <file>...");
17
+ if (!receiptPath) fail("--receipt requires a path");
18
+ if (dryRun) {
19
+ console.log(`dry-run: Developer ID signing ${files.join(", ")}`);
20
+ process.exit(0);
21
+ }
22
+ if (process.platform !== "darwin") fail("macOS signing must run on macOS");
23
+ const identity = process.env.APPLE_DEVELOPER_ID?.trim();
24
+ if (!identity) fail("set APPLE_DEVELOPER_ID to an installed Developer ID Application signing identity");
25
+ for (const file of files) if (!existsSync(file)) fail(`missing file: ${file}`);
26
+
27
+ const evidence = [];
28
+ for (const file of files) {
29
+ const before = fileEvidence(file);
30
+ run("codesign", ["--force", "--options", "runtime", "--timestamp", "--sign", identity, file]);
31
+ const verification = run("codesign", ["--verify", "--strict", "--verbose=4", file]);
32
+ const details = run("codesign", ["-dvv", file]);
33
+ if (!/valid on disk/i.test(verification) || !/satisfies its designated requirement/i.test(verification)) {
34
+ fail(`strict codesign verification failed for ${file}`);
35
+ }
36
+ if (!new RegExp(`^Authority=${escapeRegExp(identity)}$`, "m").test(details) && !details.includes(`Authority=${identity}`)) {
37
+ fail(`codesign identity mismatch for ${file}: expected ${identity}`);
38
+ }
39
+ evidence.push({ file, before, after: fileEvidence(file), codesign: "strict-valid", identity, timestampPresent: /(?:Timestamp|Signed Time)[=:]/i.test(details) });
40
+ if (!evidence.at(-1).timestampPresent) fail(`Developer ID timestamp is missing for ${file}`);
41
+ }
42
+
43
+ const absoluteReceipt = path.resolve(receiptPath);
44
+ mkdirSync(path.dirname(absoluteReceipt), { recursive: true });
45
+ writeFileSync(absoluteReceipt, `${JSON.stringify({ schema: 1, signingContract: MACOS_PORTABLE_SIGNING_CONTRACT, identity, files: evidence }, null, 2)}\n`);
46
+
47
+ function fileEvidence(file) {
48
+ const bytes = readFileSync(file);
49
+ return { sha256: createHash("sha256").update(bytes).digest("hex"), sizeBytes: statSync(file).size };
50
+ }
51
+
52
+ function run(cmd, runArgs) {
53
+ const result = spawnSync(cmd, runArgs, { encoding: "utf8" });
54
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
55
+ if (result.status !== 0) fail(`${cmd} ${runArgs.join(" ")} failed: ${output.trim()}`);
56
+ return output;
57
+ }
58
+
59
+ function escapeRegExp(value) {
60
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
61
+ }
62
+
63
+ function fail(message) {
64
+ console.error(`right-sign-macos: ${message}`);
65
+ process.exit(1);
66
+ }
@@ -13,7 +13,6 @@ export const RIGHTRELEASE_MANIFEST_SIGNER = Object.freeze({
13
13
 
14
14
  const EXCLUDE_CREDENTIALS = [
15
15
  "ManagedIdentityCredential",
16
- "WorkloadIdentityCredential",
17
16
  "SharedTokenCacheCredential",
18
17
  "VisualStudioCredential",
19
18
  "VisualStudioCodeCredential",
package/sign-windows.mjs CHANGED
@@ -7,7 +7,6 @@ import { spawnSync } from "node:child_process";
7
7
 
8
8
  const EXCLUDE_CREDENTIALS = [
9
9
  "ManagedIdentityCredential",
10
- "WorkloadIdentityCredential",
11
10
  "SharedTokenCacheCredential",
12
11
  "VisualStudioCredential",
13
12
  "VisualStudioCodeCredential",