@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
package/install-app.mjs
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// Never uses a shell, osascript, AppleEvents, or foreground activation.
|
|
5
5
|
import { execFile as nodeExecFile, execFileSync as nodeExecFileSync } from "./background-process.mjs";
|
|
6
6
|
import { detachWithRetry } from "./hdiutil-detach.mjs";
|
|
7
|
+
import { previousPathOf } from "./in-place-install.mjs";
|
|
8
|
+
import { assertShippedSwapHelperSigned } from "./swap-helper-bundle.mjs";
|
|
9
|
+
import { installAppWindows, InstallAppWindowsError } from "./install-app-windows.mjs";
|
|
7
10
|
import fs from "node:fs";
|
|
8
11
|
import os from "node:os";
|
|
9
12
|
import path from "node:path";
|
|
@@ -19,11 +22,11 @@ export class InstallAppError extends Error {
|
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
const VALUE_FLAGS = new Set(["--dmg", "--zip", "--app", "--app-name", "--bundle-id", "--dest"]);
|
|
25
|
+
const VALUE_FLAGS = new Set(["--dmg", "--zip", "--app", "--app-name", "--bundle-id", "--dest", "--installer", "--exe"]);
|
|
23
26
|
const BOOL_FLAGS = new Set(["--relaunch", "--no-relaunch", "--json", "--dev", "--recover"]);
|
|
24
27
|
|
|
25
28
|
export function parseArgs(argv) {
|
|
26
|
-
const opts = { dmg: null, zip: null, app: null, dev: false, appName: null, bundleId: null, dest: "/Applications", relaunch: null, json: false, recover: false };
|
|
29
|
+
const opts = { dmg: null, zip: null, app: null, installer: null, exe: null, dev: false, appName: null, bundleId: null, dest: "/Applications", relaunch: null, json: false, recover: false };
|
|
27
30
|
for (let i = 0; i < argv.length; i += 1) {
|
|
28
31
|
const flag = argv[i];
|
|
29
32
|
if (VALUE_FLAGS.has(flag)) {
|
|
@@ -35,6 +38,8 @@ export function parseArgs(argv) {
|
|
|
35
38
|
else if (flag === "--app") opts.app = value;
|
|
36
39
|
else if (flag === "--app-name") opts.appName = value;
|
|
37
40
|
else if (flag === "--bundle-id") opts.bundleId = value;
|
|
41
|
+
else if (flag === "--installer") opts.installer = value;
|
|
42
|
+
else if (flag === "--exe") opts.exe = value;
|
|
38
43
|
else opts.dest = value;
|
|
39
44
|
} else if (BOOL_FLAGS.has(flag)) {
|
|
40
45
|
if (flag === "--json") opts.json = true;
|
|
@@ -45,6 +50,12 @@ export function parseArgs(argv) {
|
|
|
45
50
|
throw new InstallAppError("usage", `unknown argument: ${flag}`);
|
|
46
51
|
}
|
|
47
52
|
}
|
|
53
|
+
if (opts.installer !== null) {
|
|
54
|
+
// Windows: run the sealed NSIS installer in place at one fixed folder, keeping <dest>.previous.
|
|
55
|
+
if (opts.dmg !== null || opts.zip !== null || opts.app !== null || opts.recover || opts.dev) throw new InstallAppError("usage", "--installer cannot be combined with --dmg, --zip, --app, --dev or --recover");
|
|
56
|
+
if (!argv.includes("--dest") || opts.exe === null) throw new InstallAppError("usage", "--installer requires --dest <fixed install folder> and --exe <App.exe>");
|
|
57
|
+
return opts;
|
|
58
|
+
}
|
|
48
59
|
if (argv.includes("--relaunch") && argv.includes("--no-relaunch")) throw new InstallAppError("usage", "--relaunch and --no-relaunch are mutually exclusive");
|
|
49
60
|
const inputs = [opts.dmg, opts.zip, opts.app].filter((v) => v !== null).length;
|
|
50
61
|
if (opts.recover) {
|
|
@@ -220,10 +231,15 @@ function coded(code, message) {
|
|
|
220
231
|
|
|
221
232
|
// Sync on purpose: signal-handler rollback must be able to swap back. Throws an error whose `code`
|
|
222
233
|
// is in SWAP_FALLBACK_CODES when no atomic swap is possible here (no helper, or exit 3 / ENOTSUP / EINVAL).
|
|
223
|
-
|
|
234
|
+
// A helper that exists must be code-signed (#258): an unsigned one is a typed SWAP_HELPER_UNSIGNED error naming the fix,
|
|
235
|
+
// never a silent downgrade. No helper at all keeps the documented journaled two-rename fallback, which is logged
|
|
236
|
+
// (`swap-fallback`, then `replaced {swap}`).
|
|
237
|
+
export function createSwapPaths({ env = process.env, resolve = resolveSwapHelper, execFileSync = nodeExecFileSync, assertSigned = assertShippedSwapHelperSigned } = {}) {
|
|
238
|
+
const checked = new Set();
|
|
224
239
|
return (a, b) => {
|
|
225
240
|
const helper = resolve({ env });
|
|
226
241
|
if (!helper) throw coded("ENOHELPER", `${SWAP_HELPER_NAME} helper not found`);
|
|
242
|
+
if (!checked.has(helper)) { assertSigned({ file: helper, execFileSync }); checked.add(helper); }
|
|
227
243
|
try { execFileSync(helper, [a, b], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); }
|
|
228
244
|
catch (error) {
|
|
229
245
|
const text = `${error?.stderr || ""}`.trim();
|
|
@@ -251,12 +267,15 @@ const defaultIsAlive = (pid) => {
|
|
|
251
267
|
try { process.kill(pid, 0); return true; } catch (e) { return e.code === "EPERM"; }
|
|
252
268
|
};
|
|
253
269
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
return
|
|
270
|
+
// Keep the bundle it replaced as `<installed>.previous` beside the fixed path (replacing any older one), by rename.
|
|
271
|
+
// Rollback is renaming it back; nothing ever runs from it. It is never sent to the Trash: a Trash copy is a second
|
|
272
|
+
// path macOS can resolve for the same bundle id.
|
|
273
|
+
async function keepPreviousBundle(run, fsImpl, bundle, installed) {
|
|
274
|
+
const previous = previousPathOf(installed);
|
|
275
|
+
if (bundle === previous) return previous;
|
|
276
|
+
fsImpl.rmSync(previous, { recursive: true, force: true });
|
|
277
|
+
await moveAside(run, fsImpl, bundle, previous);
|
|
278
|
+
return previous;
|
|
260
279
|
}
|
|
261
280
|
|
|
262
281
|
// Deterministic roll-forward/back of any interrupted install journal in `dest`. Idempotent.
|
|
@@ -292,8 +311,8 @@ export async function recoverInstalls({ dest, appName = null, run, fsImpl, home,
|
|
|
292
311
|
let ok = j.phase === "verified";
|
|
293
312
|
if (!ok) { try { await verifyBundle(run, j.installed, { assess: !j.dev, staple: false }); ok = true; } catch { ok = false; } }
|
|
294
313
|
if (ok) {
|
|
295
|
-
try { await
|
|
296
|
-
catch { action = "rolled-forward-
|
|
314
|
+
try { await keepPreviousBundle(run, fsImpl, j.staged, j.installed); action = "rolled-forward"; }
|
|
315
|
+
catch { action = "rolled-forward-previous-pending"; keepJournal = true; keepStaged = true; }
|
|
297
316
|
} else {
|
|
298
317
|
swapBack(swapPaths, fsImpl, j.staged, j.installed); // staged now holds the bad new bundle
|
|
299
318
|
action = "rolled-back";
|
|
@@ -306,8 +325,8 @@ export async function recoverInstalls({ dest, appName = null, run, fsImpl, home,
|
|
|
306
325
|
let ok = j.phase === "verified";
|
|
307
326
|
if (!ok) { try { await verifyBundle(run, j.installed, { assess: !j.dev, staple: false }); ok = true; } catch { ok = false; } }
|
|
308
327
|
if (ok) {
|
|
309
|
-
try { await
|
|
310
|
-
catch { action = "rolled-forward-
|
|
328
|
+
try { await keepPreviousBundle(run, fsImpl, j.backup, j.installed); action = "rolled-forward"; }
|
|
329
|
+
catch { action = "rolled-forward-previous-pending"; keepJournal = true; }
|
|
311
330
|
} else {
|
|
312
331
|
rm(j.installed);
|
|
313
332
|
fsImpl.renameSync(j.backup, j.installed);
|
|
@@ -376,7 +395,7 @@ export async function installApp(opts, deps = {}) {
|
|
|
376
395
|
if (s.mode === "atomic") {
|
|
377
396
|
// installed was never absent. Before the exchange only the staged copy needs removing; after it,
|
|
378
397
|
// exchange back (installed = old again, staged = the new copy) and drop the new copy. Once final,
|
|
379
|
-
// staged holds the verified-old bundle awaiting
|
|
398
|
+
// staged holds the verified-old bundle awaiting retirement to .previous, so it must not be touched.
|
|
380
399
|
if (!s.final) {
|
|
381
400
|
if (s.swapped) swapBack(swapPaths, fsImpl, s.staged, s.installed);
|
|
382
401
|
removeQuiet(s.staged);
|
|
@@ -486,8 +505,9 @@ export async function installApp(opts, deps = {}) {
|
|
|
486
505
|
// Stage a verified copy on the destination volume so the swap is rename(2) only.
|
|
487
506
|
failedStage = "stage";
|
|
488
507
|
const base = appName.replace(/\.app$/, "");
|
|
508
|
+
// Staging is never run from: it is verified, then renamed into place. The rollback copy is the fixed `<installed>.previous`.
|
|
489
509
|
const staged = path.join(opts.dest, `.${base}.rightkit-install-${pid}.app`);
|
|
490
|
-
const backup = hadInstalled ?
|
|
510
|
+
const backup = hadInstalled ? previousPathOf(installed) : null;
|
|
491
511
|
const journal = journalPathFor(opts.dest, appName);
|
|
492
512
|
const journalData = { schema: 1, appName, pid, phase: "staging", dev, installed, backup, staged, mount: mountPoint, mountRoot };
|
|
493
513
|
removeQuiet(staged);
|
|
@@ -512,7 +532,7 @@ export async function installApp(opts, deps = {}) {
|
|
|
512
532
|
|
|
513
533
|
// Swap. Preferred: one atomic exchange (renamex_np RENAME_SWAP), so `installed` is never absent even
|
|
514
534
|
// under SIGKILL; the old bundle then sits at the staged path. Fallback (no helper, or a volume without
|
|
515
|
-
// RENAME_SWAP): old -> backup, staged -> installed, journaled. The old bundle is never
|
|
535
|
+
// RENAME_SWAP): old -> backup, staged -> installed, journaled. The old bundle is never retired before
|
|
516
536
|
// the new one is verified.
|
|
517
537
|
failedStage = "replace";
|
|
518
538
|
let jd = journalData;
|
|
@@ -535,6 +555,7 @@ export async function installApp(opts, deps = {}) {
|
|
|
535
555
|
swap.mode = "two-rename";
|
|
536
556
|
jd = { ...journalData, swap: "two-rename" };
|
|
537
557
|
writeJournal(fsImpl, journal, { ...jd, phase: "swapping" });
|
|
558
|
+
removeQuiet(backup); // an older .previous; the one kept copy is the bundle moving aside now
|
|
538
559
|
fsImpl.renameSync(installed, backup); swap.backedUp = true;
|
|
539
560
|
fsImpl.renameSync(staged, installed);
|
|
540
561
|
swap.stagedIn = true;
|
|
@@ -555,16 +576,18 @@ export async function installApp(opts, deps = {}) {
|
|
|
555
576
|
writeJournal(fsImpl, journal, { ...jd, phase: "verified", mount: null, mountRoot: null });
|
|
556
577
|
swap.final = true;
|
|
557
578
|
|
|
558
|
-
// Only now
|
|
559
|
-
let
|
|
579
|
+
// Only now is the old bundle retired, to <installed>.previous (never the Trash).
|
|
580
|
+
let kept = null;
|
|
581
|
+
let keepFailed = false;
|
|
560
582
|
const oldBundle = swapMode === "atomic" ? staged : backup; // atomic: the exchange left the old bundle at the staged path
|
|
561
583
|
if (hadInstalled && oldBundle) {
|
|
562
|
-
try {
|
|
563
|
-
catch (error) { event("
|
|
584
|
+
try { kept = await keepPreviousBundle(run, fsImpl, oldBundle, installed); }
|
|
585
|
+
catch (error) { event("previous-keep-failed", { backup: oldBundle, reason: errText(error) }); kept = oldBundle; }
|
|
564
586
|
}
|
|
565
|
-
if (!oldBundle ||
|
|
587
|
+
if (!oldBundle || kept !== previousPathOf(installed)) { if (!oldBundle) removeQuiet(journal); }
|
|
588
|
+
else removeQuiet(journal);
|
|
566
589
|
swap = null;
|
|
567
|
-
event("replaced", { installed, previous:
|
|
590
|
+
event("replaced", { installed, previous: kept, swap: swapMode });
|
|
568
591
|
|
|
569
592
|
// Relaunch hidden and in the background only.
|
|
570
593
|
const relaunch = opts.relaunch === null ? wasRunning : opts.relaunch;
|
|
@@ -573,7 +596,7 @@ export async function installApp(opts, deps = {}) {
|
|
|
573
596
|
await run("/usr/bin/open", ["-g", "-j", "-b", bundleId]);
|
|
574
597
|
event("relaunched", { bundleId });
|
|
575
598
|
}
|
|
576
|
-
const result = { ok: true, app: appName, installed, bundleId, teamId: newTeam, previous:
|
|
599
|
+
const result = { ok: true, app: appName, installed, bundleId, teamId: newTeam, previous: kept, swap: swapMode, relaunched: relaunch, ...(dev ? { dev: true } : {}) };
|
|
577
600
|
event("done", result);
|
|
578
601
|
return result;
|
|
579
602
|
} catch (error) {
|
|
@@ -685,6 +708,7 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
685
708
|
stderr(`right-release install-app: ${error.message}`);
|
|
686
709
|
stderr("usage: right-release install-app (--dmg <path.dmg> | --dev (--zip <path.zip> | --app <path.app> | --dmg <path.dmg>)) [--app-name Pulse.app] [--bundle-id dev.orthic.pulse] [--dest /Applications] [--relaunch|--no-relaunch] [--json]");
|
|
687
710
|
stderr(" right-release install-app --recover [--dest /Applications] [--app-name Pulse.app] [--json]");
|
|
711
|
+
stderr(" right-release install-app --installer <Setup.exe> --dest <fixed install folder> --exe <App.exe> [--json] (Windows: in place, keeps <dest>.previous)");
|
|
688
712
|
stderr(" --dev installs a Developer ID-signed, NOT notarized build (skips spctl/stapler, clears quarantine on the new bundle).");
|
|
689
713
|
return 2;
|
|
690
714
|
}
|
|
@@ -696,10 +720,16 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
696
720
|
else stdout(`install-app: ${event}${Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : ""}`);
|
|
697
721
|
};
|
|
698
722
|
try {
|
|
723
|
+
if (opts.installer !== null) {
|
|
724
|
+
installAppWindows(opts, { ...io.deps, event: (name, data = {}) => emit({ event: name, ...data }) });
|
|
725
|
+
emit({ event: "done", dest: opts.dest });
|
|
726
|
+
return 0;
|
|
727
|
+
}
|
|
699
728
|
if (opts.recover) await recoverApp(opts, { ...io.deps, emit });
|
|
700
729
|
else await installApp(opts, { ...io.deps, emit });
|
|
701
730
|
return 0;
|
|
702
731
|
} catch (error) {
|
|
732
|
+
if (error instanceof InstallAppWindowsError) { stderr(`install-app: ${error.code}: ${error.message}`); return error.code === "unsupported" ? 3 : 1; }
|
|
703
733
|
return error.code === "unsupported" ? 3 : 1;
|
|
704
734
|
}
|
|
705
735
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// `install.kind: "tauri-devid"`: install a freshly built Tauri .app at ONE fixed path, signed with the stable
|
|
2
|
+
// Developer ID, so macOS privacy grants (TCC) and Keychain code requirements survive every rebuild.
|
|
3
|
+
//
|
|
4
|
+
// Pipeline (installTauriDevId): copy the built .app to a private staging copy -> sign nested code, then the app,
|
|
5
|
+
// with the stable Developer ID (signMacosDevelopmentBundle) -> assert the designated requirement equals the
|
|
6
|
+
// installed copy's (assertStableDesignatedRequirement) -> install at the fixed path keeping `<dest>.previous`
|
|
7
|
+
// (installApp, dev mode) -> verify the installed copy; any verify failure restores `.previous`.
|
|
8
|
+
// resetTauriDevId restores `.previous` or removes the install, only when asked.
|
|
9
|
+
//
|
|
10
|
+
// Every external call (copy, sign, run, install, file system) is injectable through `deps`; the defaults are the
|
|
11
|
+
// real RightKit pieces. macOS paths are POSIX, so this module uses path.posix throughout.
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { spawnSync } from "./background-process.mjs";
|
|
17
|
+
import { assertStableDesignatedRequirement, signMacosDevelopmentBundle } from "./development-signing.mjs";
|
|
18
|
+
import { installApp } from "./install-app.mjs";
|
|
19
|
+
import { previousPathOf } from "./in-place-install.mjs";
|
|
20
|
+
|
|
21
|
+
export const TAURI_DEVID_KIND = "tauri-devid";
|
|
22
|
+
|
|
23
|
+
export class TauriDevIdError extends Error {
|
|
24
|
+
constructor(code, message, details = {}) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "TauriDevIdError";
|
|
27
|
+
this.code = code;
|
|
28
|
+
Object.assign(this, details);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const P = path.posix;
|
|
33
|
+
const absolute = (appRoot, value) => (P.isAbsolute(value) ? P.normalize(value) : P.join(appRoot, value));
|
|
34
|
+
const text = (result) => `${result?.stdout ?? ""}\n${result?.stderr ?? ""}`.trim();
|
|
35
|
+
|
|
36
|
+
/** Real implementations of every external call. Tests replace any of them. */
|
|
37
|
+
export function defaultDeps() {
|
|
38
|
+
return {
|
|
39
|
+
run: spawnSync,
|
|
40
|
+
sign: signMacosDevelopmentBundle,
|
|
41
|
+
assertRequirement: assertStableDesignatedRequirement,
|
|
42
|
+
installApp,
|
|
43
|
+
exists: existsSync,
|
|
44
|
+
makeTemp: () => mkdtempSync(P.join(os.tmpdir(), "rightkit-tauri-devid-")),
|
|
45
|
+
copyApp: (from, to) => cpSync(from, to, { recursive: true, verbatimSymlinks: true }),
|
|
46
|
+
rename: renameSync,
|
|
47
|
+
remove: (target) => rmSync(target, { recursive: true, force: true }),
|
|
48
|
+
readFile: readFileSync,
|
|
49
|
+
log: (line) => console.log(line),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The `tauri-devid` descriptor, validated. Accepts the config root or the descriptor itself. */
|
|
54
|
+
export function resolveTauriDevIdDescriptor(config, { appRoot = "" } = {}) {
|
|
55
|
+
const descriptor = config?.development?.targets?.mac?.install ?? config;
|
|
56
|
+
if (!descriptor || descriptor.kind !== TAURI_DEVID_KIND) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", `development.targets.mac.install.kind must be "${TAURI_DEVID_KIND}"`);
|
|
57
|
+
if (typeof descriptor.productName !== "string" || !descriptor.productName) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "install.productName is required");
|
|
58
|
+
if (typeof descriptor.destination !== "string" || !P.isAbsolute(descriptor.destination) || !descriptor.destination.endsWith(".app") || descriptor.destination.split("/").includes("..")) {
|
|
59
|
+
throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "install.destination must be an absolute /…/<Product>.app path (the one fixed install path, for example /Applications/<Product>.app)");
|
|
60
|
+
}
|
|
61
|
+
if (descriptor.signing !== undefined) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", `install.signing is not allowed for ${TAURI_DEVID_KIND}: ad hoc signing changes every build and resets privacy grants`);
|
|
62
|
+
const binaries = descriptor.verifyBinaries ?? [];
|
|
63
|
+
if (!Array.isArray(binaries) || binaries.some((item) => typeof item !== "string" || !item || P.isAbsolute(item) || item.split("/").includes(".."))) {
|
|
64
|
+
throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "install.verifyBinaries must be relative paths inside the app, for example Contents/MacOS/<name>");
|
|
65
|
+
}
|
|
66
|
+
return { ...descriptor, destination: P.normalize(descriptor.destination), verifyBinaries: binaries, appRoot };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const builtPathOf = (built, appRoot) => {
|
|
70
|
+
const value = typeof built === "string" ? built : built?.app;
|
|
71
|
+
if (typeof value !== "string" || !value.replace(/\/+$/, "").endsWith(".app")) throw new TauriDevIdError("TAURI_DEVID_BUILT_INVALID", "built must be the path of the built .app (a string, or { app })");
|
|
72
|
+
return absolute(appRoot, value.replace(/\/+$/, ""));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const digestOf = (deps, file) => createHash("sha256").update(deps.readFile(file)).digest("hex");
|
|
76
|
+
|
|
77
|
+
/** Put `.previous` back at the fixed path. Renames only; the bad copy is removed last. */
|
|
78
|
+
function restorePrevious(destination, deps) {
|
|
79
|
+
const previous = previousPathOf(destination);
|
|
80
|
+
if (!deps.exists(previous)) throw new TauriDevIdError("TAURI_DEVID_NO_PREVIOUS", `no rollback copy at ${previous}`);
|
|
81
|
+
const aside = `${destination}.reset`;
|
|
82
|
+
deps.remove(aside);
|
|
83
|
+
const hadCurrent = deps.exists(destination);
|
|
84
|
+
if (hadCurrent) deps.rename(destination, aside);
|
|
85
|
+
try { deps.rename(previous, destination); }
|
|
86
|
+
catch (error) { if (hadCurrent) deps.rename(aside, destination); throw error; }
|
|
87
|
+
deps.remove(aside);
|
|
88
|
+
return { destination, restoredFrom: previous };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function verifyInstalled({ appRoot, destination, staged, expectedRequirement, binaries, env, deps }) {
|
|
92
|
+
const strict = deps.run("codesign", ["--verify", "--strict", "--deep", destination], { cwd: appRoot, env, encoding: "utf8", windowsHide: true, shell: false });
|
|
93
|
+
if (strict.error || strict.status !== 0) throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `installed ${destination} failed codesign --verify --strict --deep: ${strict.error?.message ?? text(strict)}`);
|
|
94
|
+
const display = deps.run("codesign", ["--display", "-r-", destination], { cwd: appRoot, env, encoding: "utf8", windowsHide: true, shell: false });
|
|
95
|
+
const actual = /^designated => (.+)$/m.exec(text(display))?.[1]?.trim();
|
|
96
|
+
if (display.status !== 0 || actual !== expectedRequirement) {
|
|
97
|
+
throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `installed ${destination} designated requirement ${JSON.stringify(actual)} is not the signed ${JSON.stringify(expectedRequirement)}`, { installedRequirement: actual, designatedRequirement: expectedRequirement });
|
|
98
|
+
}
|
|
99
|
+
for (const binary of binaries) {
|
|
100
|
+
let want; let got;
|
|
101
|
+
try { want = digestOf(deps, P.join(staged, binary)); got = digestOf(deps, P.join(destination, binary)); }
|
|
102
|
+
catch (error) { throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `cannot read ${binary} to verify the install: ${error.message}`); }
|
|
103
|
+
if (want !== got) throw new TauriDevIdError("TAURI_DEVID_VERIFY_FAILED", `installed ${binary} does not match the signed staged copy`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {{appRoot: string, config: object, built: string|{app: string}, deps?: object, env?: object}} options
|
|
109
|
+
* `config` is the app's release config (development.targets.mac.install is the `tauri-devid` descriptor).
|
|
110
|
+
* @returns {Promise<{installed: string, previous: string|null, designatedRequirement: string, identity: string, teamIdentifier: string, signing: object}>}
|
|
111
|
+
*/
|
|
112
|
+
export async function installTauriDevId({ appRoot, config, built, deps = {}, env = {} }) {
|
|
113
|
+
const d = { ...defaultDeps(), ...deps };
|
|
114
|
+
const descriptor = resolveTauriDevIdDescriptor(config, { appRoot });
|
|
115
|
+
const destination = descriptor.destination;
|
|
116
|
+
const source = builtPathOf(built, appRoot);
|
|
117
|
+
if (!d.exists(source)) throw new TauriDevIdError("TAURI_DEVID_BUILT_INVALID", `built app not found: ${source}`);
|
|
118
|
+
const name = P.basename(destination);
|
|
119
|
+
const tempRoot = d.makeTemp();
|
|
120
|
+
// The staging copy carries the final bundle name so installApp installs <dest dir>/<name> without renaming.
|
|
121
|
+
const staged = P.join(tempRoot, name);
|
|
122
|
+
try {
|
|
123
|
+
d.copyApp(source, staged);
|
|
124
|
+
// 1. Sign: nested code first, then the app, with the stable Developer ID (development-signing orders it deepest first).
|
|
125
|
+
const signing = await d.sign({ root: appRoot, bundle: staged, descriptor, config, env });
|
|
126
|
+
if (signing.adhoc || !signing.designatedRequirement) throw new TauriDevIdError("TAURI_DEVID_SIGNING_UNSTABLE", `signing produced no stable Developer ID designated requirement for ${staged}; refusing to install`);
|
|
127
|
+
// 2. Same identity as the installed copy, or fail: a changed designated requirement resets every privacy grant.
|
|
128
|
+
if (d.exists(destination)) await d.assertRequirement({ root: appRoot, installed: destination, expected: signing.designatedRequirement, env, run: d.run });
|
|
129
|
+
// 3. Install at the fixed path; the replaced copy stays as <dest>.previous.
|
|
130
|
+
const outcome = await d.installApp({ app: staged, dev: true, dest: P.dirname(destination), appName: name, bundleId: descriptor.bundleId ?? null, relaunch: null }, { env });
|
|
131
|
+
// 4. Verify the installed copy; a failure puts .previous back (or removes a first install) so the path is never left broken.
|
|
132
|
+
try {
|
|
133
|
+
verifyInstalled({ appRoot, destination, staged, expectedRequirement: signing.designatedRequirement, binaries: descriptor.verifyBinaries, env, deps: d });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
let rolledBack = "none";
|
|
136
|
+
if (outcome?.previous && d.exists(previousPathOf(destination))) { restorePrevious(destination, d); rolledBack = "restored-previous"; }
|
|
137
|
+
else { d.remove(destination); rolledBack = "removed-first-install"; }
|
|
138
|
+
error.rolledBack = rolledBack;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
d.log(`right-release dev: installed ${destination}; identity=${signing.identity}; TeamIdentifier=${signing.teamIdentifier}; designated=${signing.designatedRequirement}`);
|
|
142
|
+
return { installed: destination, previous: outcome?.previous ?? null, designatedRequirement: signing.designatedRequirement, identity: signing.identity, teamIdentifier: signing.teamIdentifier, signing };
|
|
143
|
+
} finally { d.remove(tempRoot); }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Deliberate reset of a fixed install. mode "restore" (default) makes `.previous` the install again; mode "remove"
|
|
148
|
+
* deletes the install and its `.previous` (and, with `resetPrivacy`, runs `tccutil reset All <bundleId>`).
|
|
149
|
+
* Quit the app first; nothing here kills processes.
|
|
150
|
+
*/
|
|
151
|
+
export async function resetTauriDevId({ appRoot, config, mode = "restore", resetPrivacy = false, deps = {}, env = {} }) {
|
|
152
|
+
const d = { ...defaultDeps(), ...deps };
|
|
153
|
+
const descriptor = resolveTauriDevIdDescriptor(config, { appRoot });
|
|
154
|
+
const destination = descriptor.destination;
|
|
155
|
+
if (mode === "restore") {
|
|
156
|
+
const result = restorePrevious(destination, d);
|
|
157
|
+
d.log(`right-release dev: restored ${destination} from ${result.restoredFrom}`);
|
|
158
|
+
return { mode, ...result };
|
|
159
|
+
}
|
|
160
|
+
if (mode !== "remove") throw new TauriDevIdError("TAURI_DEVID_RESET_MODE_INVALID", `mode must be "restore" or "remove", got ${JSON.stringify(mode)}`);
|
|
161
|
+
d.remove(destination);
|
|
162
|
+
d.remove(previousPathOf(destination));
|
|
163
|
+
let privacyReset = false;
|
|
164
|
+
if (resetPrivacy) {
|
|
165
|
+
if (!descriptor.bundleId) throw new TauriDevIdError("TAURI_DEVID_CONFIG_INVALID", "resetPrivacy requires install.bundleId");
|
|
166
|
+
const reset = d.run("tccutil", ["reset", "All", descriptor.bundleId], { cwd: appRoot, env, encoding: "utf8", windowsHide: true, shell: false });
|
|
167
|
+
if (reset.error || reset.status !== 0) throw new TauriDevIdError("TAURI_DEVID_RESET_FAILED", `tccutil reset failed: ${reset.error?.message ?? text(reset)}`);
|
|
168
|
+
privacyReset = true;
|
|
169
|
+
}
|
|
170
|
+
d.log(`right-release dev: removed ${destination}${privacyReset ? " and reset its privacy grants" : ""}`);
|
|
171
|
+
return { mode, destination, removed: true, privacyReset };
|
|
172
|
+
}
|
package/inventory-check.mjs
CHANGED
|
@@ -24,7 +24,7 @@ const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
|
24
24
|
const CONCURRENCY = 8;
|
|
25
25
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
26
26
|
const CRATES_API_INTERVAL_MS = 1000;
|
|
27
|
-
const USER_AGENT = "rightkit-inventory-check/1 (+https://github.com/
|
|
27
|
+
const USER_AGENT = "rightkit-inventory-check/1 (+https://github.com/bogusyogi/rightkit)";
|
|
28
28
|
const EXCLUDED_PREFIX = "build-control-";
|
|
29
29
|
const OWNER_PROBE_CRATE = "rightkit-fs";
|
|
30
30
|
const EXTRA_ACCOUNT_CRATES = Object.freeze([]);
|
package/local-publish.mjs
CHANGED
|
@@ -17,7 +17,8 @@ import { GATE_SCHEMA, defaultEvidencePath, enforcePublicationGate, publishGatedT
|
|
|
17
17
|
import { runCargoPublish } from "./publish-cargo.mjs";
|
|
18
18
|
import { cargoIndexUrl } from "./cargo-registry-readback.mjs";
|
|
19
19
|
import { readCargoPackage } from "./crate-graph.mjs";
|
|
20
|
-
import { recordPublished } from "./rightkit-sync.mjs";
|
|
20
|
+
import { recordPublished, stageSourceVersions } from "./rightkit-sync.mjs";
|
|
21
|
+
import { prepareSwapHelper, SWAP_HELPER_CARGO_ARGS, SWAP_HELPER_PACKAGE } from "./swap-helper-bundle.mjs";
|
|
21
22
|
|
|
22
23
|
export const LOCAL_OPERATOR = "agent (local-build grant)";
|
|
23
24
|
export const LOCAL_HOST_POLICY_REASON = "Adrian 2026-10-14 local-build grant (.rightkit-local-build.json); Windows covered by the next CI run";
|
|
@@ -194,7 +195,16 @@ export async function runLocalPublish(kind, argv, deps = {}) {
|
|
|
194
195
|
const cleanup = () => fs.rmSync(scratch, { recursive: true, force: true });
|
|
195
196
|
try {
|
|
196
197
|
let artifactFile; const packCommands = [];
|
|
198
|
+
if (kind === "npm" && unit === SWAP_HELPER_PACKAGE) {
|
|
199
|
+
// #258: the package ships a Developer-ID-signed bin/darwin/rightkit-swap; build, sign and verify it before packing (fails closed).
|
|
200
|
+
try { (deps.prepareSwapHelper ?? prepareSwapHelper)({ workspaceRoot, packageDir: unitPath, platform, run, env, log }); }
|
|
201
|
+
catch (error) { refuse(`${error.code ? `[${error.code}] ` : ""}${error.message}`); }
|
|
202
|
+
packCommands.push(`rightkit cargo ${SWAP_HELPER_CARGO_ARGS.join(" ")} && codesign --verify --strict bin/darwin/rightkit-swap`);
|
|
203
|
+
}
|
|
197
204
|
if (kind === "npm") {
|
|
205
|
+
// The feed a tarball ships must list the version it ships with (#319): stage it before packing.
|
|
206
|
+
const feedFile = path.join(workspaceRoot, "packages", "release", "rightkit-versions.json");
|
|
207
|
+
if (fs.existsSync(feedFile)) (deps.stageSourceVersions ?? stageSourceVersions)([{ registry: "npm", name: unit, version }], { file: feedFile, root: workspaceRoot, log });
|
|
198
208
|
const args = ["--dir", dir, "pack", "--pack-destination", scratch];
|
|
199
209
|
packCommands.push(`pnpm ${args.join(" ")}`);
|
|
200
210
|
const packed = run("pnpm", args, { cwd: workspaceRoot, env });
|