@rightkit/release 0.2.165 → 0.2.173
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/README.md +121 -4
- package/addon-command.mjs +6 -13
- package/android-aab-scan.mjs +265 -0
- package/android-dev.mjs +111 -0
- package/android-ndk.mjs +77 -0
- package/android-play-upload.mjs +262 -0
- package/android-release.mjs +344 -0
- package/android-test-fixtures.mjs +79 -0
- package/apple-profile-create.mjs +363 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/build-release.mjs +73 -13
- package/cargo-guard.mjs +2 -0
- package/cargo-profile-contract.mjs +183 -0
- package/cargo-target.mjs +8 -0
- package/cli/right-release.mjs +16 -4
- package/committed-source.mjs +1 -1
- package/dev-artifact-refresh.mjs +176 -0
- package/dev-artifact-support.mjs +61 -0
- package/development-signing.mjs +71 -4
- package/development.mjs +93 -47
- package/direct-bootstrap.mjs +26 -41
- package/disk-command.mjs +298 -5
- package/disk-policy.json +7 -5
- package/disk-policy.mjs +105 -16
- package/disk-stable-root.mjs +19 -16
- package/engine-locator.mjs +116 -0
- package/external-bin.mjs +133 -0
- package/github-release.mjs +2 -2
- package/github-updater-release.mjs +154 -0
- package/hardeningscan.mjs +1 -1
- package/in-place-install.mjs +56 -0
- package/install-app-windows.mjs +63 -0
- package/install-app.mjs +53 -23
- package/install-tauri-devid.mjs +172 -0
- package/inventory-check.mjs +1 -1
- package/local-publish.mjs +11 -1
- package/mac-package-recipe.mjs +304 -0
- package/mac-signing-matrix.mjs +23 -11
- package/managed-cargo.mjs +6 -0
- package/media-sidecar-catalog.mjs +45 -0
- package/media-sidecar.mjs +306 -0
- package/ownership-approval-command.mjs +10 -44
- package/ownership-approval.mjs +3 -42
- package/ownership-catalog-config.json +2 -1
- package/ownership-catalog.mjs +4 -2
- package/ownership-command.mjs +7 -9
- package/ownership-dependency-allowlist.json +25 -1
- package/ownership-evidence.mjs +8 -5
- package/ownership-git-inventory.mjs +41 -7
- package/ownership-lockfiles.mjs +91 -8
- package/ownership-mandatory-set.json +36 -16
- package/ownership-report.mjs +5 -6
- package/ownership-scan-worker.mjs +2 -2
- package/ownership-verdict.mjs +10 -7
- package/ownership.mjs +47 -29
- package/package-recipe-targets.mjs +95 -0
- package/package.json +7 -4
- package/portable-development.mjs +32 -115
- package/preflight.mjs +4 -6
- package/project-type.mjs +1 -1
- package/publication-artifacts.mjs +34 -0
- package/publish-update.mjs +72 -11
- package/qa-adoption-exemptions.json +5 -0
- package/qa-build-features.mjs +7 -0
- package/qa-contract.mjs +182 -0
- package/qa-control-scan.mjs +3 -3
- package/release-qa-command.mjs +38 -12
- package/release-qa-installed.mjs +136 -0
- package/release-qa-receipt.mjs +23 -2
- package/release.mjs +285 -14
- package/rightkit-ownership.json +537 -5
- package/rightkit-sync.mjs +27 -1
- package/rightkit-versions.json +111 -63
- package/runtime-budget-classes.json +28 -0
- package/runtime-budgets.mjs +4 -1
- package/sign-candidate.mjs +161 -0
- package/sign-dev-artifact.mjs +198 -0
- package/stage-binary.mjs +60 -0
- package/stage-sidecar-command.mjs +41 -0
- package/stage-sidecar.mjs +424 -0
- package/swap-helper-bundle.mjs +105 -0
- package/swift-mac-dmg.mjs +2 -1
- package/templates/cargo-profiles.toml +28 -0
- package/upload-release.mjs +11 -1
- package/win-package-recipe.mjs +321 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Declarative macOS packaging recipe (bogusyogi/rightkit#345). A mac target that sets `macPackage` and has no
|
|
2
|
+
// `prePackage` / `package` hook scripts is packaged by this module instead of a per-app mac-dmg.mjs:
|
|
3
|
+
//
|
|
4
|
+
// 1. universal check every Mach-O (main executable + nested) must hold x86_64 and arm64 (macPackage.universal)
|
|
5
|
+
// 2. sign nested code Developer ID, hardened runtime, no entitlements, deepest first
|
|
6
|
+
// 3. sign the app Developer ID, hardened runtime, optional entitlements
|
|
7
|
+
// 4. build the DMG branded, from the signed app (dmg-build)
|
|
8
|
+
// 5. sign the DMG Developer ID with a secure timestamp (no hardened runtime, as for a disk image)
|
|
9
|
+
// 6. notarize + staple the DMG and the app (macos-notarization), when macPackage.notarize
|
|
10
|
+
// 7. updater tarball tar.gz of the stapled app's Contents + the updater signature, when macPackage.updater
|
|
11
|
+
//
|
|
12
|
+
// Every external effect is injectable through `deps`; nothing here reads the host unless a default is used.
|
|
13
|
+
// Fails closed: every problem throws MacPackageRecipeError, and a failure after the DMG exists removes the
|
|
14
|
+
// half-processed DMG / updater files so a later seal cannot pick up an artifact that skipped a step.
|
|
15
|
+
import * as nodeFs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { buildDmg } from "./dmg-build.mjs";
|
|
18
|
+
import { spawnSync } from "./background-process.mjs";
|
|
19
|
+
import { signMacosEntries } from "./mac-sign-core.mjs";
|
|
20
|
+
import { mainExecutableName } from "./mac-bundle-signing.mjs";
|
|
21
|
+
import { notarizeMacosArtifact } from "./macos-notarization.mjs";
|
|
22
|
+
import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
23
|
+
import { runMacCodesignCommands, signMacUpdaterFiles } from "./macos-signing-session.mjs";
|
|
24
|
+
import { discoverNestedCode } from "./swift-mac-app.mjs";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Typed error. Codes: MAC_PACKAGE_CONFIG_INVALID, MAC_PACKAGE_HOST_UNSUPPORTED, MAC_PACKAGE_INPUT_MISSING,
|
|
28
|
+
* MAC_PACKAGE_UNIVERSAL_FAILED, MAC_PACKAGE_SIGN_FAILED, MAC_PACKAGE_DMG_FAILED, MAC_PACKAGE_NOTARIZE_FAILED,
|
|
29
|
+
* MAC_PACKAGE_UPDATER_FAILED.
|
|
30
|
+
*/
|
|
31
|
+
export class MacPackageRecipeError extends Error {
|
|
32
|
+
constructor(code, message, options) { super(message, options); this.name = "MacPackageRecipeError"; this.code = code; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const MAC_PACKAGE_STEPS = Object.freeze(["universal", "sign-nested", "sign-app", "dmg", "sign-dmg", "notarize", "updater"]);
|
|
36
|
+
const UNIVERSAL_ARCHS = ["x86_64", "arm64"];
|
|
37
|
+
const MACPACKAGE_KEYS = ["universal", "dmg", "notarize", "updater", "nested", "entitlements"];
|
|
38
|
+
const DMG_KEYS = ["name", "background", "windowSize", "iconPositions"];
|
|
39
|
+
const ICON_KEYS = ["app", "applications"];
|
|
40
|
+
|
|
41
|
+
const invalid = (message) => new MacPackageRecipeError("MAC_PACKAGE_CONFIG_INVALID", `macPackage: ${message}`);
|
|
42
|
+
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
43
|
+
const rejectUnknown = (object, allowed, label) => {
|
|
44
|
+
const unknown = Object.keys(object).filter((key) => !allowed.includes(key));
|
|
45
|
+
if (unknown.length) throw invalid(`${label} has unknown key(s): ${unknown.join(", ")} (allowed: ${allowed.join(", ")})`);
|
|
46
|
+
};
|
|
47
|
+
const integerPair = (value, label, min) => {
|
|
48
|
+
if (!Array.isArray(value) || value.length !== 2 || value.some((n) => !Number.isInteger(n) || n < min || n > 32767)) {
|
|
49
|
+
throw invalid(`${label} must be [x, y] integers ${min}..32767`);
|
|
50
|
+
}
|
|
51
|
+
return [...value];
|
|
52
|
+
};
|
|
53
|
+
const requireBoolean = (value, label) => {
|
|
54
|
+
if (typeof value !== "boolean") throw invalid(`${label} is required and must be true or false`);
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** A bundle-relative glob: forward slashes only, no `..`, no absolute path, only `*`, `**` and `?` wildcards. */
|
|
59
|
+
function validateGlob(pattern, label) {
|
|
60
|
+
if (typeof pattern !== "string" || !pattern.trim()) throw invalid(`${label} must be a non-empty string`);
|
|
61
|
+
if (pattern.startsWith("/") || /^[A-Za-z]:/.test(pattern) || pattern.includes("\\")) throw invalid(`${label} must be a bundle-relative path with forward slashes: ${pattern}`);
|
|
62
|
+
if (/[[\]{}()!]/.test(pattern)) throw invalid(`${label} supports only *, ** and ? wildcards: ${pattern}`);
|
|
63
|
+
const segments = pattern.split("/");
|
|
64
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) throw invalid(`${label} must not contain empty, '.' or '..' segments: ${pattern}`);
|
|
65
|
+
if (segments.some((segment) => segment.includes("**") && segment !== "**")) throw invalid(`${label}: '**' must be a whole path segment: ${pattern}`);
|
|
66
|
+
return pattern;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Strictly validate and normalize a `macPackage` value. Unknown keys, wrong types and missing fields throw. */
|
|
70
|
+
export function validateMacPackageConfig(macPackage) {
|
|
71
|
+
if (!isPlainObject(macPackage)) throw invalid("must be an object");
|
|
72
|
+
rejectUnknown(macPackage, MACPACKAGE_KEYS, "macPackage");
|
|
73
|
+
const universal = macPackage.universal === undefined ? false : requireBoolean(macPackage.universal, "universal");
|
|
74
|
+
const notarize = requireBoolean(macPackage.notarize, "notarize");
|
|
75
|
+
const updater = requireBoolean(macPackage.updater, "updater");
|
|
76
|
+
const dmg = macPackage.dmg;
|
|
77
|
+
if (!isPlainObject(dmg)) throw invalid("dmg is required and must be an object");
|
|
78
|
+
rejectUnknown(dmg, DMG_KEYS, "dmg");
|
|
79
|
+
if (typeof dmg.name !== "string" || !dmg.name.trim() || /[\0\r\n/:]/.test(dmg.name) || Buffer.byteLength(dmg.name) > 255) {
|
|
80
|
+
throw invalid("dmg.name must be a non-empty volume name without NUL, CR, LF, '/' or ':'");
|
|
81
|
+
}
|
|
82
|
+
const normalized = { name: dmg.name };
|
|
83
|
+
if (dmg.background !== undefined) {
|
|
84
|
+
if (typeof dmg.background !== "string" || !/\.(png|tiff?)$/i.test(dmg.background)) throw invalid("dmg.background must be a path to a PNG or TIFF file");
|
|
85
|
+
normalized.background = dmg.background;
|
|
86
|
+
}
|
|
87
|
+
if (dmg.windowSize !== undefined) normalized.windowSize = integerPair(dmg.windowSize, "dmg.windowSize", 1);
|
|
88
|
+
if (dmg.iconPositions !== undefined) {
|
|
89
|
+
if (!isPlainObject(dmg.iconPositions)) throw invalid("dmg.iconPositions must be an object with app and/or applications");
|
|
90
|
+
rejectUnknown(dmg.iconPositions, ICON_KEYS, "dmg.iconPositions");
|
|
91
|
+
normalized.iconPositions = Object.fromEntries(Object.entries(dmg.iconPositions).map(([key, value]) => [key, integerPair(value, `dmg.iconPositions.${key}`, 0)]));
|
|
92
|
+
}
|
|
93
|
+
let nested = [];
|
|
94
|
+
if (macPackage.nested !== undefined) {
|
|
95
|
+
if (!Array.isArray(macPackage.nested) || !macPackage.nested.length) throw invalid("nested must be a non-empty array of bundle-relative globs");
|
|
96
|
+
nested = macPackage.nested.map((pattern, index) => validateGlob(pattern, `nested[${index}]`));
|
|
97
|
+
if (new Set(nested).size !== nested.length) throw invalid("nested globs must be unique");
|
|
98
|
+
}
|
|
99
|
+
if (macPackage.entitlements !== undefined && (typeof macPackage.entitlements !== "string" || !macPackage.entitlements.trim())) throw invalid("entitlements must be a path string");
|
|
100
|
+
return { universal, dmg: normalized, notarize, updater, nested, ...(macPackage.entitlements ? { entitlements: macPackage.entitlements } : {}) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate the mac target around `macPackage`: the declarative recipe and the hook scripts are mutually exclusive, and the
|
|
105
|
+
* app / DMG paths come from the existing `notarize.app` and `notarize.file` fields so there is one source of truth.
|
|
106
|
+
*/
|
|
107
|
+
export function validateMacPackageTarget(target) {
|
|
108
|
+
if (!isPlainObject(target)) throw invalid("the mac target must be an object");
|
|
109
|
+
if (target.prePackage !== undefined || target.package !== undefined) throw invalid("a target with macPackage must not also declare prePackage or package; delete the hook scripts");
|
|
110
|
+
if (target.dmg !== undefined) throw invalid("a target with macPackage must not also declare targets.mac.dmg; use macPackage.dmg");
|
|
111
|
+
const recipe = validateMacPackageConfig(target.macPackage);
|
|
112
|
+
const app = target.notarize?.app;
|
|
113
|
+
const file = target.notarize?.file;
|
|
114
|
+
if (typeof app !== "string" || !/\.app\/?$/.test(app)) throw invalid("notarize.app must name the .app bundle the recipe signs and packs");
|
|
115
|
+
if (typeof file !== "string" || !/\.dmg$/i.test(file)) throw invalid("notarize.file must name the .dmg the recipe produces");
|
|
116
|
+
return { recipe, app, dmgFile: file };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Bundle-relative glob to RegExp; `**` spans path segments, `*` and `?` stay inside one. */
|
|
120
|
+
export function globToRegExp(pattern) {
|
|
121
|
+
const segments = pattern.split("/");
|
|
122
|
+
const body = segments.map((segment, index) => {
|
|
123
|
+
const last = index === segments.length - 1;
|
|
124
|
+
if (segment === "**") return last ? ".+" : "(?:[^/]+/)*";
|
|
125
|
+
return `${segment.replace(/[.+^$|\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")}${last ? "" : "/"}`;
|
|
126
|
+
}).join("");
|
|
127
|
+
return new RegExp(`^${body}$`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function listTree(root, fs) {
|
|
131
|
+
const found = [];
|
|
132
|
+
const walk = (dir, prefix) => {
|
|
133
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
134
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
135
|
+
found.push(relative);
|
|
136
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) walk(path.join(dir, entry.name), relative);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
walk(root, "");
|
|
140
|
+
return found;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const depth = (file) => file.split(/[\\/]/).length;
|
|
144
|
+
const tail = (text, lines = 20) => String(text ?? "").split(/\r?\n/).slice(-lines).join("\n");
|
|
145
|
+
|
|
146
|
+
const defaultInspect = (cmd, args) => {
|
|
147
|
+
const result = spawnSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
148
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
149
|
+
if (result.error || result.status !== 0) throw new Error(`${cmd} ${args.join(" ")} failed: ${output.trim() || result.error?.message}`);
|
|
150
|
+
return output;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Run the declarative mac package recipe. Returns { steps, app, dmg, updater, receipt, signing } where `steps` lists the
|
|
155
|
+
* steps that actually ran, in order. Throws MacPackageRecipeError on the first failure.
|
|
156
|
+
*
|
|
157
|
+
* deps (all optional): platform, env, fs, resolveArtifact(root, p), resolveIdentity({env}), signOne(codesignArgs),
|
|
158
|
+
* inspect(cmd, args) -> output, evidence(file), hashFile(file), discover(app, mainName), mainName(app),
|
|
159
|
+
* listFiles(dir) -> bundle-relative posix paths, buildDmg(options), notarize(options), run(cmd, args, opts),
|
|
160
|
+
* signUpdater(files, options), receiptPath, log(event).
|
|
161
|
+
*/
|
|
162
|
+
export function runMacPackageRecipe({ appRoot, config = {}, target, deps = {} }) {
|
|
163
|
+
const { recipe, app: declaredApp, dmgFile } = validateMacPackageTarget(target);
|
|
164
|
+
const {
|
|
165
|
+
platform = process.platform, env = process.env, fs = nodeFs,
|
|
166
|
+
resolveArtifact = (root, p) => path.resolve(root, p),
|
|
167
|
+
resolveIdentity = resolveMacosDeveloperIdIdentity,
|
|
168
|
+
signOne = (codesignArgs) => runMacCodesignCommands([codesignArgs], { env })[0],
|
|
169
|
+
inspect = defaultInspect, evidence, hashFile,
|
|
170
|
+
discover = discoverNestedCode, mainName = mainExecutableName,
|
|
171
|
+
listFiles = (dir) => listTree(dir, fs),
|
|
172
|
+
buildDmg: build = buildDmg, notarize = notarizeMacosArtifact,
|
|
173
|
+
run = spawnSync, signUpdater = (files, options) => signMacUpdaterFiles(files, options),
|
|
174
|
+
log = () => {},
|
|
175
|
+
} = deps;
|
|
176
|
+
const appName = config.app ?? path.basename(appRoot ?? ".");
|
|
177
|
+
if (typeof appRoot !== "string" || !path.isAbsolute(appRoot)) throw invalid("appRoot must be an absolute path");
|
|
178
|
+
if (platform !== "darwin") throw new MacPackageRecipeError("MAC_PACKAGE_HOST_UNSUPPORTED", `macPackage signs and notarizes on macOS only (host: ${platform})`);
|
|
179
|
+
|
|
180
|
+
const app = path.resolve(resolveArtifact(appRoot, declaredApp));
|
|
181
|
+
const dmg = path.resolve(resolveArtifact(appRoot, dmgFile));
|
|
182
|
+
const updater = `${app}.tar.gz`;
|
|
183
|
+
const receiptPath = deps.receiptPath ?? path.join(path.resolve(env.RIGHT_RELEASE_STATE_ROOT ?? path.join(appRoot, ".right-release"), "receipts"), `${appName}-mac-notarization.json`);
|
|
184
|
+
const missing = (what, file) => new MacPackageRecipeError("MAC_PACKAGE_INPUT_MISSING", `${what} not found: ${file}`);
|
|
185
|
+
const wrap = (code, step, error) => (error instanceof MacPackageRecipeError ? error
|
|
186
|
+
: new MacPackageRecipeError(code, `macPackage ${step} failed: ${error?.message ?? error}`, { cause: error }));
|
|
187
|
+
|
|
188
|
+
// Resolve every input before any effect so a typo cannot leave a half-signed app.
|
|
189
|
+
if (!fs.existsSync(app)) throw missing("app bundle", app);
|
|
190
|
+
const infoPlist = path.join(app, "Contents", "Info.plist");
|
|
191
|
+
if (!fs.existsSync(infoPlist)) throw missing("Contents/Info.plist in the app bundle", infoPlist);
|
|
192
|
+
const entitlements = recipe.entitlements ? path.resolve(appRoot, recipe.entitlements) : undefined;
|
|
193
|
+
if (entitlements && !fs.existsSync(entitlements)) throw missing("entitlements file", entitlements);
|
|
194
|
+
const background = recipe.dmg.background ? path.resolve(appRoot, recipe.dmg.background) : undefined;
|
|
195
|
+
if (background && !fs.existsSync(background)) throw missing("dmg.background file", background);
|
|
196
|
+
|
|
197
|
+
const executable = mainName(app);
|
|
198
|
+
const mainBinary = path.join(app, "Contents", "MacOS", executable);
|
|
199
|
+
const nestedSet = new Set(discover(app, executable).map((file) => path.resolve(file)));
|
|
200
|
+
if (recipe.nested.length) {
|
|
201
|
+
const tree = listFiles(app);
|
|
202
|
+
for (const pattern of recipe.nested) {
|
|
203
|
+
const matcher = globToRegExp(pattern);
|
|
204
|
+
const hits = tree.filter((relative) => matcher.test(relative));
|
|
205
|
+
if (!hits.length) throw new MacPackageRecipeError("MAC_PACKAGE_INPUT_MISSING", `macPackage.nested glob matched nothing inside ${path.basename(app)}: ${pattern}`);
|
|
206
|
+
for (const hit of hits) nestedSet.add(path.resolve(app, hit));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
nestedSet.delete(mainBinary);
|
|
210
|
+
nestedSet.delete(app);
|
|
211
|
+
// Inside-out: a path nested deeper is always signed before the code that contains it.
|
|
212
|
+
const nested = [...nestedSet].sort((a, b) => depth(b) - depth(a));
|
|
213
|
+
|
|
214
|
+
const steps = [];
|
|
215
|
+
const record = (step) => { steps.push(step); log({ event: "mac-package-step", step, app: appName }); };
|
|
216
|
+
const signOptions = { signOne, inspect, ...(evidence ? { evidence } : {}), ...(hashFile ? { hashFile } : {}) };
|
|
217
|
+
let signing = [];
|
|
218
|
+
let receipt;
|
|
219
|
+
let updaterFiles = [];
|
|
220
|
+
try {
|
|
221
|
+
// 1) universal: verify every Mach-O holds both architectures. A thin slice makes the whole app Intel- or
|
|
222
|
+
// Apple-Silicon-only, which compiles, signs and notarizes fine and then fails on half the users' Macs.
|
|
223
|
+
if (recipe.universal) {
|
|
224
|
+
const archsOf = (file) => String(inspect("lipo", ["-archs", file])).trim().split(/\s+/);
|
|
225
|
+
const assertFat = (file) => {
|
|
226
|
+
const archs = archsOf(file);
|
|
227
|
+
const lacking = UNIVERSAL_ARCHS.filter((arch) => !archs.includes(arch));
|
|
228
|
+
if (lacking.length) throw new Error(`${path.relative(app, file)} is not universal (has: ${archs.join(" ")}; missing: ${lacking.join(" ")})`);
|
|
229
|
+
};
|
|
230
|
+
try {
|
|
231
|
+
assertFat(mainBinary); // an unreadable main executable is a failure, not a skip
|
|
232
|
+
for (const file of nested) {
|
|
233
|
+
let isFile = false;
|
|
234
|
+
try { isFile = fs.statSync(file).isFile(); } catch { /* absent: signing reports it */ }
|
|
235
|
+
if (!isFile) continue; // code bundles are covered by the Mach-O files inside them
|
|
236
|
+
try { archsOf(file); } catch { continue; } // lipo rejects non-Mach-O files (scripts, resources matched by a glob)
|
|
237
|
+
assertFat(file);
|
|
238
|
+
}
|
|
239
|
+
} catch (error) { throw wrap("MAC_PACKAGE_UNIVERSAL_FAILED", "universal check", error); }
|
|
240
|
+
record("universal");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 2-3) sign nested code, then the app. One identity for the whole run.
|
|
244
|
+
let identity;
|
|
245
|
+
try { identity = resolveIdentity({ env }); } catch (error) { throw wrap("MAC_PACKAGE_SIGN_FAILED", "identity lookup", error); }
|
|
246
|
+
if (typeof identity !== "string" || !identity.trim()) throw new MacPackageRecipeError("MAC_PACKAGE_SIGN_FAILED", "macPackage signing identity is empty");
|
|
247
|
+
if (nested.length) {
|
|
248
|
+
try { signing.push(...signMacosEntries(nested.map((file) => ({ file, hardenedRuntime: true, options: [] })), { identity, ...signOptions })); }
|
|
249
|
+
catch (error) { throw wrap("MAC_PACKAGE_SIGN_FAILED", "nested code signing", error); }
|
|
250
|
+
record("sign-nested");
|
|
251
|
+
}
|
|
252
|
+
try { signing.push(...signMacosEntries([{ file: app, hardenedRuntime: true, options: [], ...(entitlements ? { entitlements } : {}) }], { identity, ...signOptions })); }
|
|
253
|
+
catch (error) { throw wrap("MAC_PACKAGE_SIGN_FAILED", "app signing", error); }
|
|
254
|
+
record("sign-app");
|
|
255
|
+
|
|
256
|
+
// 4) branded DMG from the signed app.
|
|
257
|
+
fs.rmSync(dmg, { force: true });
|
|
258
|
+
try {
|
|
259
|
+
build({
|
|
260
|
+
app, out: dmg, volname: recipe.dmg.name,
|
|
261
|
+
...(background ? { background } : {}),
|
|
262
|
+
...(recipe.dmg.windowSize ? { window: recipe.dmg.windowSize } : {}),
|
|
263
|
+
...(recipe.dmg.iconPositions?.app ? { appPosition: recipe.dmg.iconPositions.app } : {}),
|
|
264
|
+
...(recipe.dmg.iconPositions?.applications ? { applicationsPosition: recipe.dmg.iconPositions.applications } : {}),
|
|
265
|
+
});
|
|
266
|
+
} catch (error) { throw wrap("MAC_PACKAGE_DMG_FAILED", "DMG build", error); }
|
|
267
|
+
if (!fs.existsSync(dmg)) throw new MacPackageRecipeError("MAC_PACKAGE_DMG_FAILED", `DMG build reported success but ${dmg} does not exist`);
|
|
268
|
+
record("dmg");
|
|
269
|
+
|
|
270
|
+
// 5) sign the DMG: secure timestamp, no hardened runtime (a disk image has none).
|
|
271
|
+
try { signing.push(...signMacosEntries([{ file: dmg, hardenedRuntime: false, options: [] }], { identity, ...signOptions })); }
|
|
272
|
+
catch (error) { throw wrap("MAC_PACKAGE_SIGN_FAILED", "DMG signing", error); }
|
|
273
|
+
record("sign-dmg");
|
|
274
|
+
|
|
275
|
+
// 6) notarize + staple. The DMG submission covers the app inside it; the app is stapled too so the updater tarball carries the ticket.
|
|
276
|
+
if (recipe.notarize) {
|
|
277
|
+
try { receipt = notarize({ archive: dmg, app, receiptPath, env, run }); }
|
|
278
|
+
catch (error) { throw wrap("MAC_PACKAGE_NOTARIZE_FAILED", "notarization", error); }
|
|
279
|
+
if (!receipt || receipt.notarization?.status !== "Accepted") throw new MacPackageRecipeError("MAC_PACKAGE_NOTARIZE_FAILED", `macPackage notarization did not report Accepted for ${dmg}`);
|
|
280
|
+
record("notarize");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 7) updater tarball from the final (stapled) app, then its signature.
|
|
284
|
+
if (recipe.updater) {
|
|
285
|
+
updaterFiles = [updater, `${updater}.sig`];
|
|
286
|
+
for (const file of updaterFiles) fs.rmSync(file, { force: true });
|
|
287
|
+
// bsdtar otherwise serializes extended attributes as AppleDouble `._*` entries, which Tauri's extractor writes
|
|
288
|
+
// into the bundle and invalidates the seal. The explicit App.app/ directory entry is omitted for the same extractor.
|
|
289
|
+
let packed;
|
|
290
|
+
try { packed = run("tar", ["-czf", updater, "-C", path.dirname(app), path.join(path.basename(app), "Contents")], { cwd: appRoot, env: { ...env, COPYFILE_DISABLE: "1" }, encoding: "utf8", stdio: "pipe" }); }
|
|
291
|
+
catch (error) { throw wrap("MAC_PACKAGE_UPDATER_FAILED", "updater tar", error); }
|
|
292
|
+
if (!packed || packed.error || packed.status !== 0) throw new MacPackageRecipeError("MAC_PACKAGE_UPDATER_FAILED", `macPackage updater tar failed: ${packed?.error?.message ?? (tail(packed?.stderr) || packed?.status)}`);
|
|
293
|
+
try { signUpdater([updater], { cwd: appRoot, env }); }
|
|
294
|
+
catch (error) { throw wrap("MAC_PACKAGE_UPDATER_FAILED", "updater signing", error); }
|
|
295
|
+
for (const file of updaterFiles) if (!fs.existsSync(file)) throw missing("updater artifact", file);
|
|
296
|
+
record("updater");
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
// Fail closed: never leave a DMG or updater that skipped a step where the seal can find it.
|
|
300
|
+
for (const file of [dmg, ...updaterFiles]) { try { fs.rmSync(file, { force: true }); } catch { /* best effort */ } }
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
return { steps, app, dmg, updater: recipe.updater ? updater : null, receipt: receipt ?? null, receiptPath: receipt ? receiptPath : null, signing };
|
|
304
|
+
}
|
package/mac-signing-matrix.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Per-file macOS signing matrix for `sign.prePackageFiles[]` object entries and `sign.nestedCode`.
|
|
2
2
|
//
|
|
3
3
|
// sign: {
|
|
4
|
-
// prePackageFiles: ["dist/tool", { file: "dist/App.app", entitlements: "release/app.entitlements.plist" }
|
|
5
|
-
//
|
|
4
|
+
// prePackageFiles: ["dist/tool", { file: "dist/App.app", entitlements: "release/app.entitlements.plist" },
|
|
5
|
+
// { file: "dist/legion-hook", identifier: "com.example.legion-hook" }],
|
|
6
|
+
// nestedCode: { "Contents/Helpers/Helper": { options: ["library"], identifier: "com.example.helper" },
|
|
6
7
|
// "Contents/PlugIns/Ext.appex": { entitlements: "release/ext.entitlements.plist" } },
|
|
7
8
|
// }
|
|
8
9
|
//
|
|
@@ -16,8 +17,12 @@ const invalid = (message) => Object.assign(new Error(message), { code: "MAC_SIGN
|
|
|
16
17
|
|
|
17
18
|
/** codesign `--options` names accepted in `options[]` (man codesign, OPTION FLAGS). `runtime` is always on. */
|
|
18
19
|
export const CODESIGN_OPTION_FLAGS = Object.freeze(["expires", "hard", "host", "kill", "library", "runtime"]);
|
|
19
|
-
const ENTRY_KEYS = new Set(["file", "entitlements", "hardenedRuntime", "options"]);
|
|
20
|
-
const SETTING_KEYS = new Set(["entitlements", "hardenedRuntime", "options"]);
|
|
20
|
+
const ENTRY_KEYS = new Set(["file", "entitlements", "hardenedRuntime", "options", "identifier"]);
|
|
21
|
+
const SETTING_KEYS = new Set(["entitlements", "hardenedRuntime", "options", "identifier"]);
|
|
22
|
+
// A stable per-file code identity: codesign `--identifier`. A loose tool otherwise gets an identifier derived from its file
|
|
23
|
+
// name or cdhash, and macOS keys privacy grants (TCC) to the code identity, so it must not change between builds.
|
|
24
|
+
// Same pattern as mac-sign-core.mjs (not imported: that module imports this one).
|
|
25
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9.-]{0,254}$/;
|
|
21
26
|
|
|
22
27
|
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
28
|
|
|
@@ -33,7 +38,11 @@ function looksLikePlist(file) {
|
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
function validateSettings(settings, label, { root, errors, isFile }) {
|
|
36
|
-
const out = { entitlements: undefined, hardenedRuntime: true, options: [] };
|
|
41
|
+
const out = { entitlements: undefined, hardenedRuntime: true, options: [], identifier: undefined };
|
|
42
|
+
if (settings.identifier !== undefined) {
|
|
43
|
+
if (typeof settings.identifier !== "string" || !IDENTIFIER_PATTERN.test(settings.identifier)) errors.push(`${label}.identifier must be 1-255 characters of letters, digits, '.' and '-', starting with a letter or digit`);
|
|
44
|
+
else out.identifier = settings.identifier;
|
|
45
|
+
}
|
|
37
46
|
if (settings.entitlements !== undefined) {
|
|
38
47
|
const value = settings.entitlements;
|
|
39
48
|
if (typeof value !== "string" || !value.trim()) errors.push(`${label}.entitlements must be a non-empty path string`);
|
|
@@ -79,8 +88,8 @@ function validateNestedKey(key) {
|
|
|
79
88
|
/**
|
|
80
89
|
* Validate and normalize `sign` for a macOS target. Collects every problem, then throws one
|
|
81
90
|
* MAC_SIGN_CONFIG_INVALID error. Returns { entries, nestedCode } where
|
|
82
|
-
* entries: [{ file, entitlements?: absolute, options: [] }] (file stays as declared)
|
|
83
|
-
* nestedCode: [{ key, entitlements?: absolute, options: [] }]
|
|
91
|
+
* entries: [{ file, identifier?, entitlements?: absolute, options: [] }] (file stays as declared)
|
|
92
|
+
* nestedCode: [{ key, identifier?, entitlements?: absolute, options: [] }]
|
|
84
93
|
* Injectable: isFile.
|
|
85
94
|
*/
|
|
86
95
|
export function validateMacSigningConfig(sign, { root, isFile = (file) => existsSync(file) && statSync(file).isFile() } = {}) {
|
|
@@ -106,7 +115,7 @@ export function validateMacSigningConfig(sign, { root, isFile = (file) => exists
|
|
|
106
115
|
if (seen.has(file)) errors.push(`${label}: duplicate signing entry ${file}`);
|
|
107
116
|
seen.add(file);
|
|
108
117
|
const normalized = validateSettings(settings, label, context);
|
|
109
|
-
entries.push({ file, ...(normalized.entitlements ? { entitlements: normalized.entitlements } : {}), options: normalized.options });
|
|
118
|
+
entries.push({ file, ...(normalized.identifier ? { identifier: normalized.identifier } : {}), ...(normalized.entitlements ? { entitlements: normalized.entitlements } : {}), options: normalized.options });
|
|
110
119
|
});
|
|
111
120
|
}
|
|
112
121
|
if (sign.nestedCode !== undefined) {
|
|
@@ -119,7 +128,7 @@ export function validateMacSigningConfig(sign, { root, isFile = (file) => exists
|
|
|
119
128
|
if (!isPlainObject(settings)) { errors.push(`${label} must be an object { entitlements?, hardenedRuntime?, options? }`); continue; }
|
|
120
129
|
for (const name of Object.keys(settings)) if (!SETTING_KEYS.has(name)) errors.push(`${label}: unknown key ${JSON.stringify(name)} (allowed: ${[...SETTING_KEYS].join(", ")})`);
|
|
121
130
|
const normalized = validateSettings(settings, label, context);
|
|
122
|
-
nestedCode.push({ key, ...(normalized.entitlements ? { entitlements: normalized.entitlements } : {}), options: normalized.options });
|
|
131
|
+
nestedCode.push({ key, ...(normalized.identifier ? { identifier: normalized.identifier } : {}), ...(normalized.entitlements ? { entitlements: normalized.entitlements } : {}), options: normalized.options });
|
|
123
132
|
}
|
|
124
133
|
if (nestedCode.length) {
|
|
125
134
|
const apps = entries.filter((entry) => /\.app\/?$/.test(entry.file));
|
|
@@ -132,7 +141,7 @@ export function validateMacSigningConfig(sign, { root, isFile = (file) => exists
|
|
|
132
141
|
}
|
|
133
142
|
|
|
134
143
|
/** True when an entry or nested item carries anything beyond the default (hardened runtime, no entitlements). */
|
|
135
|
-
export const hasNonDefaultSettings = (item) => Boolean(item.entitlements) || (item.options?.length ?? 0) > 0;
|
|
144
|
+
export const hasNonDefaultSettings = (item) => Boolean(item.entitlements) || Boolean(item.identifier) || (item.options?.length ?? 0) > 0;
|
|
136
145
|
|
|
137
146
|
const depth = (file) => file.split(path.sep).length;
|
|
138
147
|
|
|
@@ -162,7 +171,7 @@ export function planMacSigning({ entries, nestedCode = [], expand = expandMacSig
|
|
|
162
171
|
const real = realpath(file);
|
|
163
172
|
if (real !== appReal && !real.startsWith(appReal + path.sep)) throw invalid(`${label}: resolves outside the bundle`);
|
|
164
173
|
if (real === mainReal) throw invalid(`${label}: the main executable is signed with the bundle; set entitlements on its sign.prePackageFiles entry`);
|
|
165
|
-
byPath.set(file, { file, ...(item.entitlements ? { entitlements: item.entitlements } : {}), options: item.options });
|
|
174
|
+
byPath.set(file, { file, ...(item.identifier ? { identifier: item.identifier } : {}), ...(item.entitlements ? { entitlements: item.entitlements } : {}), options: item.options });
|
|
166
175
|
}
|
|
167
176
|
}
|
|
168
177
|
const nested = [...byPath.values()].sort((a, b) => depth(b.file) - depth(a.file) || a.file.localeCompare(b.file));
|
|
@@ -178,6 +187,9 @@ export function matrixReceiptProblems(evidenceFiles, entries, hashFile) {
|
|
|
178
187
|
if (!item) { problems.push(`missing receipt entry for ${entry.file}`); continue; }
|
|
179
188
|
const wantOptions = ["runtime", ...(entry.options ?? [])].join(",");
|
|
180
189
|
if ((item.codesignOptions ?? "runtime") !== wantOptions) problems.push(`${entry.file}: signed with options ${item.codesignOptions ?? "runtime"}, expected ${wantOptions}`);
|
|
190
|
+
if (entry.identifier) {
|
|
191
|
+
if (item.identifier !== entry.identifier) problems.push(`${entry.file}: signed with identifier ${item.identifier ?? "(none recorded)"}, expected ${entry.identifier}`);
|
|
192
|
+
} else if (item.identifier) problems.push(`${entry.file}: signed with identifier ${item.identifier} the plan did not declare`);
|
|
181
193
|
if (entry.entitlements) {
|
|
182
194
|
if (item.entitlements?.sha256 !== hashFile(entry.entitlements)) problems.push(`${entry.file}: receipt entitlements digest does not match ${entry.entitlements}`);
|
|
183
195
|
} else if (item.entitlements) problems.push(`${entry.file}: signed with entitlements the plan did not declare`);
|
package/managed-cargo.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { discoverBrokerSocket, managedControlCandidates, resolveManagedRoots } from './managed-roots.mjs';
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { spawnSync } from "./background-process.mjs";
|
|
@@ -12,6 +13,11 @@ export function managedRuntimeClient(env = process.env, platform = process.platf
|
|
|
12
13
|
const client = paths.join(root, "runtime-current", platform === 'win32' ? "rustright.exe" : "rustright");
|
|
13
14
|
if (exists(client)) return client;
|
|
14
15
|
}
|
|
16
|
+
// RustRight's own install (2026-10-17 cutover): the Mac agent-bin shims exec this client directly.
|
|
17
|
+
if (platform === 'darwin') {
|
|
18
|
+
const client = path.posix.join(env.HOME || homedir(), 'Library', 'Application Support', 'RustRight', 'runtime', 'rustright');
|
|
19
|
+
if (exists(client)) return client;
|
|
20
|
+
}
|
|
15
21
|
return undefined;
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Find the rightkit-ffmpeg crate (its catalog and notice files) for a release.
|
|
2
|
+
//
|
|
3
|
+
// Order: `mediaSidecar.catalog` in the release config (the crate's catalog/artifacts.json; the crate directory is the
|
|
4
|
+
// catalog's grandparent), otherwise the crate Cargo resolves for the app's native manifest (`cargo metadata --offline`,
|
|
5
|
+
// so the catalog is exactly the one the app was compiled against, at the version in its Cargo.lock).
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { runCargoSync } from "./managed-cargo.mjs";
|
|
9
|
+
|
|
10
|
+
const invalid = (code, message) => Object.assign(new Error(message), { code });
|
|
11
|
+
|
|
12
|
+
export function cargoFfmpegCrate(manifestPath, { run = runCargoSync } = {}) {
|
|
13
|
+
const result = run(["metadata", "--offline", "--format-version", "1", "--manifest-path", manifestPath], {
|
|
14
|
+
cwd: path.dirname(manifestPath),
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
});
|
|
19
|
+
if (result.error) throw invalid("MEDIA_SIDECAR_CRATE_NOT_FOUND", `could not run cargo metadata: ${result.error.message}`);
|
|
20
|
+
if (result.status !== 0) throw invalid("MEDIA_SIDECAR_CRATE_NOT_FOUND", `cargo metadata failed: ${String(result.stderr ?? "").trim()}`);
|
|
21
|
+
const metadata = JSON.parse(result.stdout);
|
|
22
|
+
const crates = metadata.packages.filter((pkg) => pkg.name === "rightkit-ffmpeg");
|
|
23
|
+
if (crates.length !== 1) throw invalid("MEDIA_SIDECAR_CRATE_NOT_FOUND", `the app's dependency graph has ${crates.length} rightkit-ffmpeg packages (expected exactly one)`);
|
|
24
|
+
return { crateDir: path.dirname(crates[0].manifest_path), crateVersion: crates[0].version };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Returns { catalog, catalogBytes, crateDir, crateVersion }. Injectable: read, crateFor. */
|
|
28
|
+
export function loadSidecarCatalog({ config, root, manifestPath, read = readFileSync, crateFor = cargoFfmpegCrate }) {
|
|
29
|
+
let crateDir;
|
|
30
|
+
let crateVersion;
|
|
31
|
+
let catalogPath;
|
|
32
|
+
if (config.catalog) {
|
|
33
|
+
catalogPath = path.resolve(root, config.catalog);
|
|
34
|
+
crateDir = path.dirname(path.dirname(catalogPath));
|
|
35
|
+
} else {
|
|
36
|
+
if (!manifestPath) throw invalid("MEDIA_SIDECAR_CRATE_NOT_FOUND", "no mediaSidecar.catalog and no native Cargo manifest to resolve rightkit-ffmpeg from");
|
|
37
|
+
({ crateDir, crateVersion } = crateFor(manifestPath));
|
|
38
|
+
catalogPath = path.join(crateDir, "catalog", "artifacts.json");
|
|
39
|
+
}
|
|
40
|
+
let catalogBytes;
|
|
41
|
+
try { catalogBytes = read(catalogPath); } catch (error) { throw invalid("MEDIA_SIDECAR_CATALOG_INVALID", `cannot read ${catalogPath}: ${error.message}`); }
|
|
42
|
+
let catalog;
|
|
43
|
+
try { catalog = JSON.parse(String(catalogBytes)); } catch { throw invalid("MEDIA_SIDECAR_CATALOG_INVALID", `${catalogPath} is not JSON`); }
|
|
44
|
+
return { catalog, catalogBytes, crateDir, crateVersion };
|
|
45
|
+
}
|