@rightkit/release 0.2.0
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/cli/right-release.mjs +108 -0
- package/create-mac-updater.mjs +54 -0
- package/create-mac-updater.test.mjs +13 -0
- package/deps.mjs +89 -0
- package/hardeningscan.mjs +116 -0
- package/lsclean.sh +28 -0
- package/package.json +24 -0
- package/publish-update.mjs +148 -0
- package/publish-update.test.mjs +101 -0
- package/release.mjs +238 -0
- package/release.test.mjs +93 -0
- package/right-suite-contract.test.mjs +83 -0
- package/sign-updater.mjs +55 -0
- package/sign-updater.test.mjs +12 -0
- package/sign-windows.mjs +133 -0
- package/upload-large.mjs +74 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const commands = new Map([
|
|
8
|
+
["release", "release.mjs"],
|
|
9
|
+
["build", "release.mjs"],
|
|
10
|
+
["package", "release.mjs"],
|
|
11
|
+
["deps", "deps.mjs"],
|
|
12
|
+
["publish-update", "publish-update.mjs"],
|
|
13
|
+
["hardening", "hardeningscan.mjs"],
|
|
14
|
+
["harden", "hardeningscan.mjs"],
|
|
15
|
+
["hardeningscan", "hardeningscan.mjs"],
|
|
16
|
+
["sign-windows", "sign-windows.mjs"],
|
|
17
|
+
["sign-updater", "sign-updater.mjs"],
|
|
18
|
+
["create-mac-updater", "create-mac-updater.mjs"],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const first = args[0];
|
|
23
|
+
|
|
24
|
+
if (!first || first === "-h" || first === "--help") {
|
|
25
|
+
printHelp();
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (first === "--version" || first === "-v") {
|
|
30
|
+
run("release.mjs", ["--version"]);
|
|
31
|
+
} else if (first === "doctor") {
|
|
32
|
+
if (args.includes("--all")) {
|
|
33
|
+
runTest(["--test", path.join(packageRoot, "right-suite-contract.test.mjs")]);
|
|
34
|
+
} else {
|
|
35
|
+
run("release.mjs", ["--doctor", ...args.slice(1)]);
|
|
36
|
+
}
|
|
37
|
+
} else if (first === "publish") {
|
|
38
|
+
const rest = args.slice(1);
|
|
39
|
+
run("release.mjs", rest.includes("--upload") || rest.includes("--publish") ? rest : [...rest, "--upload"]);
|
|
40
|
+
} else if (first === "lsclean") {
|
|
41
|
+
runBinary("bash", [path.join(packageRoot, "lsclean.sh"), ...args.slice(1)]);
|
|
42
|
+
} else if (commands.has(first)) {
|
|
43
|
+
run(commands.get(first), args.slice(1));
|
|
44
|
+
} else if (first.startsWith("-")) {
|
|
45
|
+
run("release.mjs", args);
|
|
46
|
+
} else {
|
|
47
|
+
console.error(`right-release: unknown command: ${first}\n`);
|
|
48
|
+
printHelp();
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function run(script, scriptArgs) {
|
|
53
|
+
const child = spawn(process.execPath, [path.join(packageRoot, script), ...scriptArgs], {
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
env: process.env,
|
|
56
|
+
stdio: "inherit",
|
|
57
|
+
windowsHide: true,
|
|
58
|
+
});
|
|
59
|
+
child.on("error", (error) => {
|
|
60
|
+
console.error(`right-release: failed to start ${script}: ${error.message}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
63
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function runTest(testArgs) {
|
|
67
|
+
const child = spawn(process.execPath, testArgs, {
|
|
68
|
+
cwd: process.cwd(),
|
|
69
|
+
env: process.env,
|
|
70
|
+
stdio: "inherit",
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
});
|
|
73
|
+
child.on("error", (error) => {
|
|
74
|
+
console.error(`right-release: failed to start doctor --all: ${error.message}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
77
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runBinary(cmd, cmdArgs) {
|
|
81
|
+
const child = spawn(cmd, cmdArgs, {
|
|
82
|
+
cwd: process.cwd(),
|
|
83
|
+
env: process.env,
|
|
84
|
+
stdio: "inherit",
|
|
85
|
+
windowsHide: true,
|
|
86
|
+
});
|
|
87
|
+
child.on("error", (error) => {
|
|
88
|
+
console.error(`right-release: failed to start ${cmd}: ${error.message}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
91
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function printHelp() {
|
|
95
|
+
console.log(`right-release <command> [options]
|
|
96
|
+
|
|
97
|
+
Commands:
|
|
98
|
+
release [--platform mac|win] --tier patch|update Build/package through the signed release lane
|
|
99
|
+
publish [--platform mac|win] --tier patch|update Release plus R2 upload + RightApps registration
|
|
100
|
+
doctor [--platform mac|win] Inspect one app's release config
|
|
101
|
+
doctor --all Verify all Right Suite app release contracts
|
|
102
|
+
deps --check|--audit|--update Shared dependency lane
|
|
103
|
+
hardening <artifact...> Run the Right Suite hardening scan
|
|
104
|
+
lsclean <AppName.app> Clear macOS LaunchServices duplicates
|
|
105
|
+
|
|
106
|
+
Direct flags are treated as: right-release release <flags>.
|
|
107
|
+
Publishing requires an explicit tier. Unsigned/local smoke builds stay app-local unless declared in right-release.config.mjs.`);
|
|
108
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, rmSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
let app;
|
|
9
|
+
let output;
|
|
10
|
+
let cwd = process.cwd();
|
|
11
|
+
let dryRun = false;
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
if (args[i] === "--app") app = args[++i];
|
|
14
|
+
else if (args[i] === "--output") output = args[++i];
|
|
15
|
+
else if (args[i] === "--cwd") cwd = path.resolve(args[++i]);
|
|
16
|
+
else if (args[i] === "--dry-run") dryRun = true;
|
|
17
|
+
else fail(`unknown argument: ${args[i]}`);
|
|
18
|
+
}
|
|
19
|
+
if (!app || !output) fail("--app and --output are required");
|
|
20
|
+
|
|
21
|
+
const appPath = path.resolve(cwd, app);
|
|
22
|
+
const outputPath = path.resolve(cwd, output);
|
|
23
|
+
if (!dryRun && !existsSync(appPath)) fail(`signed app not found: ${appPath}`);
|
|
24
|
+
|
|
25
|
+
const env = { ...process.env };
|
|
26
|
+
if (!dryRun && !env.TAURI_SIGNING_PRIVATE_KEY) {
|
|
27
|
+
try {
|
|
28
|
+
env.TAURI_SIGNING_PRIVATE_KEY = execFileSync(
|
|
29
|
+
"security",
|
|
30
|
+
["find-generic-password", "-a", os.userInfo().username, "-s", "rightsuite-updater-key", "-w"],
|
|
31
|
+
{ encoding: "utf8" },
|
|
32
|
+
).trim();
|
|
33
|
+
} catch {
|
|
34
|
+
fail("shared updater key is missing from Keychain service rightsuite-updater-key");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD ??= "";
|
|
38
|
+
|
|
39
|
+
rmSync(outputPath, { force: true });
|
|
40
|
+
rmSync(`${outputPath}.sig`, { force: true });
|
|
41
|
+
run("tar", ["-czf", outputPath, "-C", path.dirname(appPath), path.basename(appPath)]);
|
|
42
|
+
run("pnpm", ["exec", "tauri", "signer", "sign", outputPath]);
|
|
43
|
+
|
|
44
|
+
function run(command, commandArgs) {
|
|
45
|
+
console.log(`${dryRun ? "[dry-run] " : ""}${command} ${commandArgs.join(" ")}`);
|
|
46
|
+
if (dryRun) return;
|
|
47
|
+
const result = spawnSync(command, commandArgs, { cwd, env, stdio: "inherit" });
|
|
48
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fail(message) {
|
|
52
|
+
console.error(`create-mac-updater: ${message}`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
const helper = fileURLToPath(new URL("./create-mac-updater.mjs", import.meta.url));
|
|
7
|
+
|
|
8
|
+
test("documents the final signed-app to signed-updater sequence in dry-run mode", () => {
|
|
9
|
+
const result = spawnSync(process.execPath, [helper, "--app", "bundle/Test.app", "--output", "bundle/Test.app.tar.gz", "--dry-run"], { encoding: "utf8" });
|
|
10
|
+
assert.equal(result.status, 0, result.stderr);
|
|
11
|
+
assert.match(result.stdout, /tar .*Test\.app\.tar\.gz.*Test\.app/);
|
|
12
|
+
assert.match(result.stdout, /tauri signer sign .*Test\.app\.tar\.gz/);
|
|
13
|
+
});
|
package/deps.mjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const opts = {
|
|
9
|
+
config: "right-release.config.mjs",
|
|
10
|
+
mode: "check",
|
|
11
|
+
latest: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < args.length; i++) {
|
|
15
|
+
const arg = args[i];
|
|
16
|
+
if (arg === "--") continue;
|
|
17
|
+
else if (arg === "--config") opts.config = args[++i];
|
|
18
|
+
else if (arg === "--check") opts.mode = "check";
|
|
19
|
+
else if (arg === "--audit") opts.mode = "audit";
|
|
20
|
+
else if (arg === "--update") opts.mode = "update";
|
|
21
|
+
else if (arg === "--latest") opts.latest = true;
|
|
22
|
+
else if (arg === "-h" || arg === "--help") usage(0);
|
|
23
|
+
else usage(2, `unknown argument: ${arg}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const configPath = path.resolve(opts.config);
|
|
27
|
+
const config = (await import(pathToFileURL(configPath))).default;
|
|
28
|
+
if (config?.schema !== 1) fail(`unsupported config schema: ${config?.schema ?? "<missing>"} (expected 1)`);
|
|
29
|
+
if (config.packageManager !== "pnpm") fail(`deps tool expects pnpm, got ${config.packageManager}`);
|
|
30
|
+
|
|
31
|
+
const root = path.dirname(configPath);
|
|
32
|
+
const workdirs = [...new Set([config.workdir ?? ".", ...(config.deps?.workdirs ?? [])])];
|
|
33
|
+
const resolvedWorkdirs = workdirs.map((rel) => ({ rel, cwd: path.resolve(root, rel) }));
|
|
34
|
+
const missingLocks = resolvedWorkdirs.filter(({ cwd }) => !existsSync(path.join(cwd, "pnpm-lock.yaml")));
|
|
35
|
+
if (missingLocks.length > 0) {
|
|
36
|
+
for (const { rel, cwd } of missingLocks) {
|
|
37
|
+
console.error(`right-deps: configured workdir ${rel} is missing ${path.join(cwd, "pnpm-lock.yaml")}`);
|
|
38
|
+
}
|
|
39
|
+
fail("dependency check refused: every configured workdir must have a pnpm-lock.yaml");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`right-deps: ${config.app} ${opts.mode}`);
|
|
43
|
+
const failures = [];
|
|
44
|
+
for (const { rel, cwd } of resolvedWorkdirs) {
|
|
45
|
+
const workspaceArgs = cwd === root ? [] : ["--ignore-workspace"];
|
|
46
|
+
if (opts.mode === "check") {
|
|
47
|
+
const status = run("pnpm", ["outdated", ...workspaceArgs], cwd, { allowFailure: true });
|
|
48
|
+
if (status == null) failures.push(`${rel}: pnpm outdated could not start`);
|
|
49
|
+
} else if (opts.mode === "audit") {
|
|
50
|
+
const status = run("pnpm", ["audit", ...workspaceArgs], cwd, { allowFailure: true });
|
|
51
|
+
if (status !== 0) failures.push(`${rel}: pnpm audit exited ${status ?? "without status"}`);
|
|
52
|
+
}
|
|
53
|
+
else if (opts.mode === "update") {
|
|
54
|
+
run("pnpm", ["update", ...workspaceArgs, ...(opts.latest ? ["--latest"] : [])], cwd, { ignoreScripts: true });
|
|
55
|
+
run("pnpm", ["install", ...workspaceArgs, "--lockfile-only", "--ignore-scripts"], cwd, { ignoreScripts: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (failures.length > 0) {
|
|
59
|
+
for (const failure of failures) console.error(`right-deps: ${failure}`);
|
|
60
|
+
fail(`${failures.length} dependency workdir(s) failed`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function usage(code, message) {
|
|
64
|
+
if (message) console.error(`right-deps: ${message}\n`);
|
|
65
|
+
console.error("usage: node deps.mjs [--config right-release.config.mjs] [--check|--audit|--update] [--latest]");
|
|
66
|
+
process.exit(code);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fail(message) {
|
|
70
|
+
console.error(`right-deps: ${message}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function run(cmd, runArgs, cwd, options = {}) {
|
|
75
|
+
console.log(`\n> ${cmd} ${runArgs.join(" ")} (${cwd})`);
|
|
76
|
+
const env = options.ignoreScripts
|
|
77
|
+
? { ...process.env, npm_config_ignore_scripts: "true", PNPM_CONFIG_IGNORE_SCRIPTS: "true" }
|
|
78
|
+
: process.env;
|
|
79
|
+
const executable = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : cmd;
|
|
80
|
+
const executableArgs = process.platform === "win32"
|
|
81
|
+
? ["/d", "/s", "/c", [cmd, ...runArgs].join(" ")]
|
|
82
|
+
: runArgs;
|
|
83
|
+
const result = spawnSync(executable, executableArgs, { cwd, env, stdio: "inherit" });
|
|
84
|
+
if (result.status !== 0 && !options.allowFailure) process.exit(result.status ?? 1);
|
|
85
|
+
if (result.status !== 0 && options.allowFailure) {
|
|
86
|
+
console.log(`right-deps: ${cmd} exited ${result.status}; continuing so all workdirs report`);
|
|
87
|
+
}
|
|
88
|
+
return result.status;
|
|
89
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
|
|
9
|
+
console.error("usage: node hardeningscan.mjs <artifact-path>...");
|
|
10
|
+
process.exit(args.length === 0 ? 2 : 0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const root = process.cwd();
|
|
14
|
+
const findings = [];
|
|
15
|
+
|
|
16
|
+
const fileNameRules = [
|
|
17
|
+
[/\.map$/i, "source map shipped"],
|
|
18
|
+
[/\.pdb$/i, "Windows debug symbols shipped"],
|
|
19
|
+
[/\.dSYM(?:$|[/\\])/i, "macOS debug symbols shipped"],
|
|
20
|
+
[/\.mlpackage(?:$|[/\\])/i, "raw CoreML package shipped"],
|
|
21
|
+
[/[/\\]docs[/\\]experiments(?:$|[/\\])/i, "experiment docs shipped"],
|
|
22
|
+
[/[/\\]\.scratch(?:$|[/\\])/i, "scratch directory shipped"],
|
|
23
|
+
[/[/\\]\.git(?:$|[/\\])/i, "git metadata shipped"],
|
|
24
|
+
[/[/\\]node_modules(?:$|[/\\])/i, "node_modules shipped"],
|
|
25
|
+
[/[/\\](?:src|tests?|benches|benchmarks)(?:$|[/\\])/i, "source/test tree shipped"],
|
|
26
|
+
[/[/\\]tdt_w15_p6(?:$|[/\\])/i, "stale TDT p6 model shipped"],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const contentRules = [
|
|
30
|
+
[/\/Users\/adrdsouza\/claude/gi, "local source path leaked"],
|
|
31
|
+
[/\/Users\/adrdsouza\/\.cargo/gi, "local cargo path leaked"],
|
|
32
|
+
[/C:\\Users\\adrdsouza\\(?:claude|\.cargo)/gi, "local Windows source path leaked"],
|
|
33
|
+
[/docs\/experiments/gi, "experiment path leaked"],
|
|
34
|
+
[/l3_cleanup_prompt/gi, "L3 cleanup prompt marker leaked"],
|
|
35
|
+
[/SYSTEM_PROMPT/gi, "system prompt marker leaked"],
|
|
36
|
+
[/OPENAI_API_KEY|ANTHROPIC_API_KEY|APPLE_PASSWORD|NOTARY_PROFILE/gi, "secret/env key name leaked"],
|
|
37
|
+
[/--enable-gpl|--enable-nonfree/gi, "GPL/nonfree ffmpeg flag leaked"],
|
|
38
|
+
[/tdt_w15_p6/gi, "stale TDT p6 string leaked"],
|
|
39
|
+
[/Canary-Qwen|Nemotron|dqlstm|bias-experiments/gi, "experiment/model-history marker leaked"],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const maxPatternLen = Math.max(...contentRules.map(([r]) => String(r).length), 256);
|
|
43
|
+
|
|
44
|
+
function add(kind, file, message, detail = "") {
|
|
45
|
+
findings.push({
|
|
46
|
+
kind,
|
|
47
|
+
file: path.relative(root, file) || file,
|
|
48
|
+
message,
|
|
49
|
+
detail,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function walk(p) {
|
|
54
|
+
const st = await lstat(p);
|
|
55
|
+
const normalized = p.split(path.sep).join("/");
|
|
56
|
+
for (const [rule, message] of fileNameRules) {
|
|
57
|
+
if (rule.test(normalized)) add("path", p, message);
|
|
58
|
+
}
|
|
59
|
+
if (st.isDirectory()) {
|
|
60
|
+
const entries = await readdir(p);
|
|
61
|
+
await Promise.all(entries.map((name) => walk(path.join(p, name))));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (st.isFile()) await scanFile(p);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scanFile(file) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const stream = createReadStream(file, { highWaterMark: 256 * 1024 });
|
|
70
|
+
let tail = "";
|
|
71
|
+
stream.on("data", (chunk) => {
|
|
72
|
+
const text = tail + chunk.toString("latin1");
|
|
73
|
+
for (const [rule, message] of contentRules) {
|
|
74
|
+
if (/ffmpeg-LICENSE\.txt$/i.test(file) && message === "GPL/nonfree ffmpeg flag leaked") {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
rule.lastIndex = 0;
|
|
78
|
+
if (rule.test(text)) add("content", file, message, rule.source);
|
|
79
|
+
}
|
|
80
|
+
tail = text.slice(-maxPatternLen);
|
|
81
|
+
});
|
|
82
|
+
stream.on("error", (err) => {
|
|
83
|
+
add("read", file, "could not scan file", err.message);
|
|
84
|
+
resolve();
|
|
85
|
+
});
|
|
86
|
+
stream.on("end", resolve);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const arg of args) {
|
|
91
|
+
const target = path.resolve(arg);
|
|
92
|
+
try {
|
|
93
|
+
await walk(target);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
add("target", target, "could not scan target", err.message);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const unique = new Map();
|
|
100
|
+
for (const finding of findings) {
|
|
101
|
+
unique.set(`${finding.kind}\0${finding.file}\0${finding.message}`, finding);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const deduped = [...unique.values()].sort((a, b) =>
|
|
105
|
+
`${a.file} ${a.message}`.localeCompare(`${b.file} ${b.message}`),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (deduped.length) {
|
|
109
|
+
console.error(`hardeningscan: ${deduped.length} finding(s)`);
|
|
110
|
+
for (const f of deduped) {
|
|
111
|
+
console.error(`- [${f.kind}] ${f.file}: ${f.message}`);
|
|
112
|
+
}
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log("hardeningscan: clean");
|
package/lsclean.sh
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# lsclean.sh — drop a built macOS app from LaunchServices (Launchpad/Spotlight).
|
|
3
|
+
#
|
|
4
|
+
# macOS auto-registers every .app bundle it discovers, so each build of a Right
|
|
5
|
+
# Suite app makes a build-folder copy show up in Launchpad even though it isn't
|
|
6
|
+
# installed in /Applications. This unregisters every registered copy of <Name.app>
|
|
7
|
+
# that is NOT under /Applications (i.e. build folders, .Trash, stale mounts).
|
|
8
|
+
#
|
|
9
|
+
# Usage: lsclean.sh HeardRight.app
|
|
10
|
+
# lsclean.sh ViewRight.app
|
|
11
|
+
# Run it after a build, or any time Launchpad shows phantom copies.
|
|
12
|
+
# Note: `lsregister -kill` was removed by Apple; per-path `-u` is the replacement.
|
|
13
|
+
|
|
14
|
+
set -eu
|
|
15
|
+
LSR=/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister
|
|
16
|
+
name="${1:?usage: lsclean.sh <AppName.app>}"
|
|
17
|
+
|
|
18
|
+
"$LSR" -dump \
|
|
19
|
+
| sed -n 's/^[[:space:]]*path:[[:space:]]*\(.*'"$name"'\) (0x[0-9a-f]*)$/\1/p' \
|
|
20
|
+
| sort -u \
|
|
21
|
+
| while IFS= read -r p; do
|
|
22
|
+
case "$p" in
|
|
23
|
+
/Applications/*) ;; # keep real installs
|
|
24
|
+
*) "$LSR" -u "$p" 2>/dev/null && echo "unregistered: $p" ;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
echo "done. (a fresh build re-registers the build-folder copy — re-run then.)"
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rightkit/release",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"right-release": "cli/right-release.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli",
|
|
11
|
+
"*.mjs",
|
|
12
|
+
"*.sh"
|
|
13
|
+
],
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test *.test.mjs",
|
|
17
|
+
"doctor:all": "node --test right-suite-contract.test.mjs"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"registry": "https://registry.npmjs.org/",
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"packageManager": "pnpm@11.9.0"
|
|
24
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
|
|
10
|
+
const API_BASE = process.env.RIGHTAPPS_API_URL || "https://api.spoares.com";
|
|
11
|
+
const DOWNLOAD_BASE = process.env.RIGHTAPPS_UPDATE_DOWNLOAD_BASE || "https://rightapps-license-gate.adrdsouza.workers.dev";
|
|
12
|
+
const PUBLIC_BASE = process.env.RIGHTAPPS_PUBLIC_DOWNLOAD_BASE || "https://pub-6c73208d46c245a9b4881d5e02f6b618.r2.dev";
|
|
13
|
+
const tier = process.env.RIGHT_RELEASE_TIER || "";
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
let configName = "right-release.config.mjs";
|
|
16
|
+
let platform = process.platform === "win32" ? "win" : "mac";
|
|
17
|
+
let dryRun = false;
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const arg = args[i];
|
|
21
|
+
if (arg === "--config") configName = args[++i];
|
|
22
|
+
else if (arg === "--platform") platform = args[++i];
|
|
23
|
+
else if (arg === "--dry-run") dryRun = true;
|
|
24
|
+
else fail(`unknown argument: ${arg}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (tier !== "patch" && tier !== "update") {
|
|
28
|
+
fail("RIGHT_RELEASE_TIER is required (patch|update)");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const configPath = path.resolve(configName);
|
|
32
|
+
const root = path.dirname(configPath);
|
|
33
|
+
const config = (await import(pathToFileURL(configPath))).default;
|
|
34
|
+
const target = config?.targets?.[platform];
|
|
35
|
+
const artifacts = target?.updater?.artifacts ?? [];
|
|
36
|
+
if (!artifacts.length) fail(`${config?.app ?? "app"} ${platform} has no updater.artifacts`);
|
|
37
|
+
const installers = target?.installer?.artifacts ?? [];
|
|
38
|
+
|
|
39
|
+
if (tier === "patch") {
|
|
40
|
+
if (!installers.length) fail(`${config?.app ?? "app"} ${platform} patch has no installer.artifacts`);
|
|
41
|
+
for (const installer of installers) {
|
|
42
|
+
assertCurrentKey(installer.key, "installer");
|
|
43
|
+
const file = path.resolve(root, installer.file);
|
|
44
|
+
if (!dryRun) await access(file).catch(() => fail(`missing signed installer: ${installer.file}`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const platforms = {};
|
|
49
|
+
const registrations = new Map();
|
|
50
|
+
const uploadedKeys = new Set();
|
|
51
|
+
for (const artifact of artifacts) {
|
|
52
|
+
assertCurrentKey(artifact.key, "updater");
|
|
53
|
+
const bucket = tier === "patch" ? "public" : "private";
|
|
54
|
+
const file = path.resolve(root, artifact.file);
|
|
55
|
+
const signatureFile = path.resolve(root, artifact.signature);
|
|
56
|
+
if (!dryRun) {
|
|
57
|
+
await access(file).catch(() => fail(`missing updater artifact: ${artifact.file}`));
|
|
58
|
+
await access(signatureFile).catch(() => fail(`missing updater signature: ${artifact.signature}`));
|
|
59
|
+
}
|
|
60
|
+
if (!uploadedKeys.has(artifact.key)) {
|
|
61
|
+
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD ${bucket}/${artifact.key}`);
|
|
62
|
+
if (!dryRun) await run(process.execPath, [UPLOAD, file, artifact.key, bucket], root);
|
|
63
|
+
uploadedKeys.add(artifact.key);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const signature = dryRun ? `<signature:${artifact.signature}>` : (await readFile(signatureFile, "utf8")).trim();
|
|
67
|
+
const artifactKey = artifact.artifactKey || artifact.key.replaceAll("/", "__");
|
|
68
|
+
const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
|
|
69
|
+
platforms[artifact.platform] = {
|
|
70
|
+
signature,
|
|
71
|
+
url: tier === "patch" ? `${PUBLIC_BASE}/${artifact.key}` : `${DOWNLOAD_BASE}/${artifact.key}`,
|
|
72
|
+
};
|
|
73
|
+
registrations.set(artifact.key, { artifactKey, path: artifact.key, ...metadata });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (tier === "patch") {
|
|
77
|
+
for (const installer of installers) {
|
|
78
|
+
const file = path.resolve(root, installer.file);
|
|
79
|
+
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installer.key}`);
|
|
80
|
+
if (!dryRun) await run(process.execPath, [UPLOAD, file, installer.key, "public"], root);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const version = config.version || target.updater.version;
|
|
85
|
+
if (!version) fail(`${config.app} config must expose version or targets.${platform}.updater.version`);
|
|
86
|
+
const body = {
|
|
87
|
+
appKey: config.app,
|
|
88
|
+
version,
|
|
89
|
+
channel: config.channel || "stable",
|
|
90
|
+
platform: null,
|
|
91
|
+
manifest: {
|
|
92
|
+
version,
|
|
93
|
+
tier,
|
|
94
|
+
notes: process.env.RIGHT_RELEASE_NOTES || `${config.app} ${version}`,
|
|
95
|
+
pub_date: new Date().toISOString(),
|
|
96
|
+
platforms,
|
|
97
|
+
},
|
|
98
|
+
artifacts: [...registrations.values()],
|
|
99
|
+
};
|
|
100
|
+
const route = tier === "patch" ? "/v1/admin/apps/patches" : "/v1/admin/apps/releases";
|
|
101
|
+
console.log(`${dryRun ? "[dry-run] " : ""}POST ${API_BASE}${route}`);
|
|
102
|
+
console.log(JSON.stringify(body));
|
|
103
|
+
|
|
104
|
+
if (!dryRun) {
|
|
105
|
+
const token = process.env.RIGHTAPPS_ADMIN_TOKEN;
|
|
106
|
+
if (!token) fail("RIGHTAPPS_ADMIN_TOKEN is required to register an update");
|
|
107
|
+
const response = await fetch(`${API_BASE}${route}`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
authorization: `Bearer ${token}`,
|
|
111
|
+
"content-type": "application/json",
|
|
112
|
+
"x-store-slug": process.env.RIGHTAPPS_STORE_SLUG || "rightapps",
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify(body),
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) fail(`registration failed: ${response.status} ${await response.text()}`);
|
|
117
|
+
console.log(await response.text());
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function fileMetadata(file) {
|
|
121
|
+
const bytes = await readFile(file);
|
|
122
|
+
return {
|
|
123
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
124
|
+
sizeBytes: (await stat(file)).size,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function run(cmd, runArgs, cwd) {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const child = spawn(cmd, runArgs, { cwd, env: process.env, stdio: "inherit", windowsHide: true });
|
|
131
|
+
child.on("error", reject);
|
|
132
|
+
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`)));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertCurrentKey(key, kind) {
|
|
137
|
+
const pattern = kind === "installer"
|
|
138
|
+
? /\/installers\/(?:mac|windows)\/current\//
|
|
139
|
+
: /\/updates\/(?:mac|windows)\/current\//;
|
|
140
|
+
if (!pattern.test(key)) {
|
|
141
|
+
fail(`${kind} key must target the replaceable current R2 object, got: ${key}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function fail(message) {
|
|
146
|
+
console.error(`publish-update: ${message}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|