@rightkit/release 0.2.107 → 0.2.109

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.
@@ -0,0 +1,230 @@
1
+ import { spawnSync } from "./background-process.mjs";
2
+ import { createHash } from "node:crypto";
3
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
7
+ import { assertReleaseExecutionAllowed } from "./execution-policy.mjs";
8
+ import { managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
9
+ import { installPortableDevelopment } from "./portable-development.mjs";
10
+
11
+ export const DEVELOPMENT_PROFILE = "release-iterate";
12
+
13
+ export function developmentEnvironment(env = process.env) {
14
+ const result = {
15
+ ...env,
16
+ RIGHTKIT_BUILD_MODE: "dev",
17
+ RIGHTKIT_CARGO_PROFILE: DEVELOPMENT_PROFILE,
18
+ TAURI_SIGNING_PRIVATE_KEY: "",
19
+ TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "",
20
+ APPLE_SIGNING_IDENTITY: "-",
21
+ APPLE_ID: "",
22
+ APPLE_PASSWORD: "",
23
+ APPLE_TEAM_ID: "",
24
+ CSC_IDENTITY_AUTO_DISCOVERY: "false",
25
+ };
26
+ delete result.CARGO_INCREMENTAL;
27
+ const client = managedRuntimeClient(result);
28
+ if (client) {
29
+ const managedRoot = result.RIGHTKIT_MANAGED_ROOT ?? result.RIGHTKIT_EXTERNAL_ROOT
30
+ ?? (process.platform === "win32" ? "D:\\.rightkit-managed" : process.platform === "darwin" ? "/Volumes/D/.rightkit-managed" : undefined);
31
+ const agentBin = managedRoot && path.join(managedRoot, "agent-bin");
32
+ if (agentBin && existsSync(agentBin)) {
33
+ const pathKey = Object.keys(result).find((key) => key.toLowerCase() === "path") ?? "PATH";
34
+ result[pathKey] = [agentBin, result[pathKey]].filter(Boolean).join(path.delimiter);
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+
40
+ export function validateDevelopmentConfig(config, platform) {
41
+ const target = config?.development?.targets?.[platform];
42
+ if (!target || !Array.isArray(target.build) || target.build.length === 0) throw new Error(`development.targets.${platform}.build must be a non-empty command array`);
43
+ for (const [index, command] of target.build.entries()) validateCommand(command, `development.targets.${platform}.build[${index}]`);
44
+ if (target.install?.kind === "command") validateCommand(target.install, `development.targets.${platform}.install`);
45
+ else if (target.install?.kind === "tauri") {
46
+ if (!target.install.productName || !target.install.targetTriple) throw new Error(`development.targets.${platform}.install requires productName and targetTriple`);
47
+ if (target.install.manifestPath && path.isAbsolute(target.install.manifestPath)) throw new Error("install.manifestPath must be relative to app root");
48
+ } else if (target.install?.kind === "portable") {
49
+ const descriptor = target.install;
50
+ if (descriptor.activation?.kind !== "current" || !path.isAbsolute(descriptor.installRoot ?? "")) throw new Error("portable development installer requires absolute installRoot and current activation");
51
+ for (const key of ["sourceDirectory", "entrypoint"]) {
52
+ if (typeof descriptor[key] !== "string" || !descriptor[key] || path.isAbsolute(descriptor[key]) || path.win32.isAbsolute(descriptor[key]) || descriptor[key].split(/[\\/]/).includes("..")) throw new Error(`portable development ${key} must be a contained relative path`);
53
+ }
54
+ } else if (target.install) throw new Error(`unsupported development installer kind: ${target.install.kind}`);
55
+ return target;
56
+ }
57
+
58
+ export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync }) {
59
+ if (!new Set(["win", "mac"]).has(platform)) throw new Error("--platform must be win or mac");
60
+ assertNativeDevelopmentHost(platform);
61
+ const target = validateDevelopmentConfig(config, platform);
62
+ const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
63
+ assertPrimaryReleaseCheckout(repoRoot, env);
64
+ if (dryRun) return { built: false, installed: false, plan: { build: target.build, install: buildOnly ? null : target.install ?? null, profile: DEVELOPMENT_PROFILE } };
65
+ assertReleaseExecutionAllowed({ cwd: repoRoot, environment: env, command: `right-release dev --platform ${platform}` });
66
+ const childEnv = developmentEnvironment(env);
67
+ for (const command of target.build) execute(command, { root, env: childEnv, dryRun, run, label: "build" });
68
+ if (buildOnly || !target.install) return { built: true, installed: false };
69
+ install(target.install, { root, platform, env: childEnv, run });
70
+ return { built: true, installed: true };
71
+ }
72
+
73
+ export function assertNativeDevelopmentHost(platform, host = process.platform) {
74
+ if ((platform === "win" && host !== "win32") || (platform === "mac" && host !== "darwin")) throw new Error(`development target ${platform} requires its native host`);
75
+ }
76
+
77
+ function install(descriptor, { root, platform, env, run }) {
78
+ if (descriptor.kind === "command") return execute(descriptor, { root, env, run, label: "install" });
79
+ if (descriptor.kind === "portable") return installPortableDevelopment(descriptor, { root, platform: platform === "win" ? "win32" : "darwin", env });
80
+ if (platform === "win") {
81
+ if (!descriptor.expectedInstalledPath) throw new Error("Windows Tauri installer requires expectedInstalledPath for installed verification");
82
+ const { bundleRoot, installer: app } = resolveTauriBundleDirectory(descriptor, { root, platform, env });
83
+ const built = path.resolve(bundleRoot, "..", "..", path.basename(descriptor.expectedInstalledPath));
84
+ assertNonEmptyExecutable(built);
85
+ const result = run(app, ["/S"], { cwd: root, env, stdio: "inherit", windowsHide: true, shell: false });
86
+ if (result.status !== 0) throw new Error(`development installer failed (${result.status}): ${app}`);
87
+ const installed = path.resolve(root, descriptor.expectedInstalledPath);
88
+ assertInstalledExecutableMatches(built, installed);
89
+ return;
90
+ }
91
+ const app = resolveTauriArtifact(descriptor, { root, platform, env });
92
+ const destination = path.resolve(descriptor.destination ?? path.join("/Applications", path.basename(app)));
93
+ const destinationRoot = path.dirname(destination);
94
+ mkdirSync(destinationRoot, { recursive: true });
95
+ const staging = `${destination}.rightkit-dev-${process.pid}`;
96
+ const backup = `${destination}.rightkit-backup-${process.pid}`;
97
+ rmSync(staging, { recursive: true, force: true });
98
+ rmSync(backup, { recursive: true, force: true });
99
+ cpSync(app, staging, { recursive: true });
100
+ const signature = run("codesign", ["--force", "--deep", "--preserve-metadata=entitlements", "--sign", "-", staging], { cwd: root, env, stdio: "inherit", windowsHide: true, shell: false });
101
+ if (signature.status !== 0) { rmSync(staging, { recursive: true, force: true }); throw new Error(`ad-hoc signing failed (${signature.status}): ${staging}`); }
102
+ const verified = run("codesign", ["--verify", "--deep", "--strict", staging], { cwd: root, env, stdio: "inherit", windowsHide: true, shell: false });
103
+ if (verified.status !== 0) { rmSync(staging, { recursive: true, force: true }); throw new Error(`ad-hoc signature verification failed (${verified.status}): ${staging}`); }
104
+ let backedUp = false;
105
+ try {
106
+ if (existsSync(destination)) { renameSync(destination, backup); backedUp = true; }
107
+ renameSync(staging, destination);
108
+ if (!existsSync(path.join(destination, "Contents", "Info.plist"))) throw new Error(`installed app verification failed: ${destination}`);
109
+ if (backedUp) rmSync(backup, { recursive: true, force: true });
110
+ } catch (error) {
111
+ rmSync(destination, { recursive: true, force: true });
112
+ if (backedUp && existsSync(backup)) renameSync(backup, destination);
113
+ rmSync(staging, { recursive: true, force: true });
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ export function resolveTauriArtifact(descriptor, { root, platform, env = process.env, runCargo = runCargoSync }) {
119
+ return resolveTauriBundleDirectory(descriptor, { root, platform, env, runCargo }).installer;
120
+ }
121
+
122
+ export function resolveTauriBundleDirectory(descriptor, { root, platform, env = process.env, runCargo = runCargoSync }) {
123
+ const bundleRoot = descriptor.bundleDirectory
124
+ ? path.resolve(root, descriptor.bundleDirectory)
125
+ : resolveBundleRoot(descriptor, { root, env, runCargo });
126
+ if (descriptor.bundleDirectory && !inside(root, bundleRoot)) throw new Error("Tauri bundleDirectory must stay inside app root");
127
+ const entries = readdirSync(bundleRoot, { withFileTypes: true });
128
+ const candidates = platform === "mac"
129
+ ? entries.filter((entry) => entry.isDirectory() && entry.name === `${descriptor.productName}.app`).map((entry) => path.join(bundleRoot, entry.name))
130
+ : entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".exe") && entry.name.toLowerCase().includes("setup")).map((entry) => path.join(bundleRoot, entry.name));
131
+ if (candidates.length !== 1) throw new Error(`expected exactly one ${platform} Tauri installer for ${descriptor.productName} in ${bundleRoot}; found ${candidates.length}`);
132
+ if (!statSync(candidates[0]).isFile() && platform === "win") throw new Error(`installer is not a file: ${candidates[0]}`);
133
+ return { bundleRoot, installer: candidates[0] };
134
+ }
135
+
136
+ export function assertInstalledExecutableMatches(built, installed, read = readFileSync) {
137
+ assertNonEmptyExecutable(built);
138
+ assertNonEmptyExecutable(installed);
139
+ const digest = (file) => createHash("sha256").update(read(file)).digest("hex");
140
+ if (digest(built) !== digest(installed)) throw new Error(`installed executable does not match this dev build: ${installed}`);
141
+ }
142
+ function assertNonEmptyExecutable(file) { if (!existsSync(file) || !statSync(file).isFile() || statSync(file).size === 0) throw new Error(`installed executable verification failed: missing or empty file ${file}`); }
143
+
144
+ function resolveBundleRoot(descriptor, { root, env, runCargo }) {
145
+ if (!descriptor.manifestPath) throw new Error("Tauri installer requires manifestPath when bundleDirectory is omitted");
146
+ const manifest = path.resolve(root, descriptor.manifestPath);
147
+ const cwd = process.cwd();
148
+ let metadata;
149
+ try {
150
+ process.chdir(root);
151
+ metadata = runCargo(["metadata", "--offline", "--no-deps", "--format-version", "1", "--manifest-path", manifest], { cwd: root, env, encoding: "utf8", windowsHide: true, shell: false });
152
+ } finally { process.chdir(cwd); }
153
+ if (metadata.error || metadata.status !== 0) throw new Error(`RightKit Cargo metadata failed (${metadata.status ?? "unavailable"})${metadata.error ? `: ${metadata.error.message}` : ""}`);
154
+ let targetDirectory;
155
+ try { targetDirectory = JSON.parse(metadata.stdout).target_directory; } catch { throw new Error("RightKit Cargo metadata returned invalid JSON"); }
156
+ if (!targetDirectory || !path.isAbsolute(targetDirectory)) throw new Error("Cargo metadata omitted absolute target_directory");
157
+ return path.join(targetDirectory, descriptor.targetTriple, DEVELOPMENT_PROFILE, "bundle", platformBundleKind(descriptor));
158
+ }
159
+
160
+ function platformBundleKind(descriptor) { return descriptor.targetTriple.includes("apple") ? "macos" : "nsis"; }
161
+ function validateCommand(command, label) { if (!command || typeof command.cmd !== "string" || !command.cmd.trim() || (command.args !== undefined && !Array.isArray(command.args))) throw new Error(`${label} must declare cmd and args`); }
162
+ function execute(command, { root, env, dryRun, run, label }) {
163
+ validateCommand(command, `development ${label}`);
164
+ const cwd = path.resolve(root, command.cwd ?? ".");
165
+ if (!inside(root, cwd)) throw new Error(`development ${label} cwd must stay inside app root`);
166
+ const args = (command.args ?? []).map(String);
167
+ if (dryRun) return { cmd: command.cmd, args, cwd };
168
+ const launch = resolveCommand(command.cmd, args, { root, env });
169
+ const result = run(launch.command, launch.args, { cwd, env, stdio: "inherit", windowsHide: true, shell: false, timeout: command.timeoutMs });
170
+ if (result.status !== 0) throw new Error(`development ${label} command failed (${result.status}): ${command.name ?? command.cmd}`);
171
+ }
172
+ export function resolveCommand(command, args, { root, env = process.env } = {}) {
173
+ if (command === "node") return { command: process.execPath, args };
174
+ if (command === "pnpm") return { command: process.execPath, args: [resolvePnpmCli({ root, env }), ...args] };
175
+ if (command === "rightkit") {
176
+ const client = managedRuntimeClient(env);
177
+ if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
178
+ return { command: process.execPath, args: [client, ...args] };
179
+ }
180
+ return { command, args };
181
+ }
182
+ export function resolvePnpmCli({ root, env = process.env } = {}) {
183
+ const candidates = [];
184
+ if (env.npm_execpath && /pnpm/i.test(path.basename(env.npm_execpath))) candidates.push(env.npm_execpath);
185
+ const nodeDir = path.dirname(process.execPath);
186
+ candidates.push(path.join(nodeDir, "node_modules", "corepack", "dist", "pnpm.js"));
187
+ candidates.push(path.join(nodeDir, "node_modules", "pnpm", "bin", "pnpm.cjs"));
188
+ for (let dir = path.resolve(root ?? process.cwd()); ; dir = path.dirname(dir)) {
189
+ const store = path.join(dir, "node_modules", ".pnpm");
190
+ if (existsSync(store)) {
191
+ for (const entry of readdirSync(store).filter((name) => /^pnpm@/.test(name)).sort().reverse()) {
192
+ candidates.push(path.join(store, entry, "node_modules", "pnpm", "bin", "pnpm.cjs"));
193
+ }
194
+ }
195
+ const parent = path.dirname(dir);
196
+ if (parent === dir) break;
197
+ }
198
+ const selected = candidates.find((candidate) => existsSync(candidate));
199
+ if (!selected) throw new Error("could not resolve pnpm JavaScript CLI entrypoint; install/pin pnpm through Corepack");
200
+ return selected;
201
+ }
202
+ function git(cwd, args) { const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true }); if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed`); return result.stdout.trim(); }
203
+ function inside(root, child) { const rel = path.relative(path.resolve(root), path.resolve(child)); return rel === "" || (rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel)); }
204
+ export async function main(args = process.argv.slice(2)) {
205
+ let platform, configPath = "right-release.config.mjs", dryRun = false, buildOnly = false;
206
+ for (let i = 0; i < args.length; i++) {
207
+ if (args[i] === "-h" || args[i] === "--help") { console.log("usage: right-release dev --platform win|mac [--config right-release.config.mjs] [--dry-run] [--build-only]"); return; }
208
+ if (args[i] === "--platform") platform = args[++i];
209
+ else if (args[i] === "--config") configPath = args[++i];
210
+ else if (args[i] === "--dry-run") dryRun = true;
211
+ else if (args[i] === "--build-only") buildOnly = true;
212
+ else throw new Error(`unknown dev option: ${args[i]}`);
213
+ }
214
+ if (!platform) throw new Error("right-release dev requires --platform win|mac");
215
+ const absoluteConfig = path.resolve(configPath);
216
+ const safeEnv = developmentEnvironment(process.env);
217
+ for (const [key, value] of Object.entries(safeEnv)) process.env[key] = value;
218
+ delete process.env.CARGO_INCREMENTAL;
219
+ const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path");
220
+ if (pathKey) process.env[pathKey] = safeEnv[pathKey] ?? process.env[pathKey];
221
+ const loaded = await import(pathToFileURL(absoluteConfig));
222
+ const config = loaded.default ?? loaded.config ?? loaded;
223
+ const result = runDevelopment({ root: path.dirname(absoluteConfig), platform, config, dryRun, buildOnly });
224
+ if (dryRun) console.log(JSON.stringify(result.plan, null, 2));
225
+ return result;
226
+ }
227
+
228
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
229
+ main().catch((error) => { console.error(`right-release dev: ${error.message}`); process.exitCode = 1; });
230
+ }
package/hardeningscan.mjs CHANGED
@@ -1,144 +1,144 @@
1
- #!/usr/bin/env node
2
- import { createHash } from "node:crypto";
3
- import { createReadStream, readFileSync, realpathSync } from "node:fs";
4
- import { lstat, readdir } from "node:fs/promises";
5
- import path from "node:path";
6
- import { HARDENING_RULES } from "./hardening-evidence.mjs";
7
-
8
- const rawArgs = process.argv.slice(2);
9
- if (rawArgs.length === 0 || rawArgs.includes("-h") || rawArgs.includes("--help")) {
10
- console.error("usage: node hardeningscan.mjs [--allow-evidence <json>] <artifact-path>...");
11
- process.exit(rawArgs.length === 0 ? 2 : 0);
12
- }
13
- const allowIndex = rawArgs.indexOf("--allow-evidence");
14
- const allowEvidencePath = allowIndex >= 0 ? rawArgs[allowIndex + 1] : null;
15
- if (allowIndex >= 0 && !allowEvidencePath) throw new Error("--allow-evidence requires a JSON path");
16
- // Only strip the flag pair when the flag is present: with allowIndex = -1,
17
- // filtering out index `allowIndex + 1` would silently drop the first
18
- // artifact path (a lone artifact arg then failed as "no artifact path").
19
- const args = allowIndex >= 0
20
- ? rawArgs.filter((_, index) => index !== allowIndex && index !== allowIndex + 1)
21
- : rawArgs;
22
- if (!args.length) throw new Error("at least one artifact path is required");
23
-
24
- const root = realpathSync(process.cwd());
25
- const findings = [];
26
- const evidence = allowEvidencePath ? JSON.parse(readFileSync(allowEvidencePath, "utf8")) : { allowances: [] };
27
- if (allowEvidencePath && (evidence.schemaVersion !== 1 || evidence.kind !== "rightkit-hardening-evidence")) {
28
- throw new Error("invalid hardening evidence");
29
- }
30
- const allowances = evidence.allowances ?? [];
31
- for (const allowance of allowances) {
32
- const sourceMatch = typeof allowance.sourceEvidence === "string" ? /^(.*):([1-9]\d*)$/.exec(allowance.sourceEvidence) : null;
33
- const knownRule = HARDENING_RULES[allowance.rule];
34
- if (!knownRule || allowance.exact !== knownRule.exact || !sourceMatch || !/^[a-f0-9]{64}$/.test(allowance.artifactSha256 ?? "") || !/^[a-f0-9]{64}$/.test(allowance.sourceSha256 ?? "") || allowance.sourceLine !== Number(sourceMatch[2]) || typeof allowance.rationale !== "string" || allowance.rationale.trim().length < 12) {
35
- throw new Error("invalid hardening allowance source binding");
36
- }
37
- const sourcePath = realpathSync(path.resolve(root, sourceMatch[1]));
38
- const sourceRelative = path.relative(root, sourcePath);
39
- if (sourceRelative === ".." || sourceRelative.startsWith("../") || sourceRelative.startsWith("..\\") || path.isAbsolute(sourceRelative)) {
40
- throw new Error("hardening allowance source escapes release root");
41
- }
42
- const sourceBytes = readFileSync(sourcePath);
43
- if (createHash("sha256").update(sourceBytes).digest("hex") !== allowance.sourceSha256) throw new Error("hardening allowance source digest mismatch");
44
- const line = sourceBytes.toString("utf8").split(/\r?\n/)[allowance.sourceLine - 1];
45
- if (typeof line !== "string" || !line.includes(allowance.exact)) throw new Error("hardening allowance source token mismatch");
46
- }
47
-
48
- const fileNameRules = [
49
- [/\.map$/i, "source map shipped"],
50
- [/\.pdb$/i, "Windows debug symbols shipped"],
51
- [/\.dSYM(?:$|[/\\])/i, "macOS debug symbols shipped"],
52
- [/\.mlpackage(?:$|[/\\])/i, "raw CoreML package shipped"],
53
- [/[/\\]docs[/\\]experiments(?:$|[/\\])/i, "experiment docs shipped"],
54
- [/[/\\]\.scratch(?:$|[/\\])/i, "scratch directory shipped"],
55
- [/[/\\]\.git(?:$|[/\\])/i, "git metadata shipped"],
56
- [/[/\\]node_modules(?:$|[/\\])/i, "node_modules shipped"],
57
- [/[/\\](?:src|tests?|benches|benchmarks)(?:$|[/\\])/i, "source/test tree shipped"],
58
- [/[/\\]tdt_w15_p6(?:$|[/\\])/i, "stale TDT p6 model shipped"],
59
- ];
60
-
61
- const contentRules = [
62
- ["local-source-path", /\/Users\/adrdsouza\/claude/gi, "local source path leaked"],
63
- ["local-cargo-path", /\/Users\/adrdsouza\/\.cargo/gi, "local cargo path leaked"],
64
- ["local-windows-path", /C:\\Users\\adrdsouza\\(?:claude|\.cargo)/gi, "local Windows source path leaked"],
65
- ["experiment-path", /docs\/experiments/gi, "experiment path leaked"],
66
- ["cleanup-prompt-marker", /l3_cleanup_prompt/gi, "L3 cleanup prompt marker leaked"],
67
- ["system-prompt-marker", /SYSTEM_PROMPT/gi, "system prompt marker leaked"],
68
- ["secret-env-name", /OPENAI_API_KEY|ANTHROPIC_API_KEY|APPLE_PASSWORD|NOTARY_PROFILE/gi, "secret/env key name leaked"],
69
- ["gpl-flag", /--enable-gpl|--enable-nonfree/gi, "GPL/nonfree ffmpeg flag leaked"],
70
- ["stale-model", /tdt_w15_p6/gi, "stale TDT p6 string leaked"],
71
- ["experiment-model", /Canary-Qwen|Nemotron|dqlstm|bias-experiments/gi, "experiment/model-history marker leaked"],
72
- ];
73
-
74
- const maxPatternLen = Math.max(...contentRules.map(([, rule]) => String(rule).length), 256);
75
-
76
- function add(kind, file, message, detail = "") {
77
- findings.push({ kind, file: path.relative(root, file) || file, message, detail });
78
- }
79
-
80
- async function walk(target) {
81
- const st = await lstat(target);
82
- const normalized = target.split(path.sep).join("/");
83
- for (const [rule, message] of fileNameRules) if (rule.test(normalized)) add("path", target, message);
84
- if (st.isDirectory()) {
85
- const entries = await readdir(target);
86
- await Promise.all(entries.map((name) => walk(path.join(target, name))));
87
- return;
88
- }
89
- if (st.isFile()) await scanFile(target);
90
- }
91
-
92
- function scanFile(file) {
93
- return new Promise((resolveScan) => {
94
- const artifactSha256 = createHash("sha256").update(readFileSync(file)).digest("hex");
95
- const stream = createReadStream(file, { highWaterMark: 256 * 1024 });
96
- let tail = "";
97
- stream.on("data", (chunk) => {
98
- const text = tail + chunk.toString("latin1");
99
- for (const [ruleId, rule, message] of contentRules) {
100
- if (/ffmpeg-LICENSE\.txt$/i.test(file) && ruleId === "gpl-flag") continue;
101
- rule.lastIndex = 0;
102
- for (const match of text.matchAll(rule)) {
103
- const allowed = allowances.some((allowance) =>
104
- allowance.rule === ruleId &&
105
- allowance.artifactSha256 === artifactSha256 &&
106
- allowance.exact === match[0] &&
107
- typeof allowance.sourceEvidence === "string" &&
108
- typeof allowance.rationale === "string"
109
- );
110
- if (!allowed) add("content", file, message, rule.source);
111
- }
112
- }
113
- tail = text.slice(-maxPatternLen);
114
- });
115
- stream.on("error", (error) => {
116
- add("read", file, "could not scan file", error.message);
117
- resolveScan();
118
- });
119
- stream.on("end", resolveScan);
120
- });
121
- }
122
-
123
- for (const arg of args) {
124
- const target = path.resolve(arg);
125
- try {
126
- await walk(target);
127
- } catch (error) {
128
- add("target", target, "could not scan target", error.message);
129
- }
130
- }
131
-
132
- const unique = new Map();
133
- for (const finding of findings) unique.set([finding.kind, finding.file, finding.message].join("\0"), finding);
134
- const deduped = [...unique.values()].sort((left, right) =>
135
- (left.file + " " + left.message).localeCompare(right.file + " " + right.message),
136
- );
137
-
138
- if (deduped.length) {
139
- console.error("hardeningscan: " + deduped.length + " finding(s)");
140
- for (const finding of deduped) console.error("- [" + finding.kind + "] " + finding.file + ": " + finding.message);
141
- process.exit(1);
142
- }
143
-
144
- console.log("hardeningscan: clean");
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { createReadStream, readFileSync, realpathSync } from "node:fs";
4
+ import { lstat, readdir } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { HARDENING_RULES } from "./hardening-evidence.mjs";
7
+
8
+ const rawArgs = process.argv.slice(2);
9
+ if (rawArgs.length === 0 || rawArgs.includes("-h") || rawArgs.includes("--help")) {
10
+ console.error("usage: node hardeningscan.mjs [--allow-evidence <json>] <artifact-path>...");
11
+ process.exit(rawArgs.length === 0 ? 2 : 0);
12
+ }
13
+ const allowIndex = rawArgs.indexOf("--allow-evidence");
14
+ const allowEvidencePath = allowIndex >= 0 ? rawArgs[allowIndex + 1] : null;
15
+ if (allowIndex >= 0 && !allowEvidencePath) throw new Error("--allow-evidence requires a JSON path");
16
+ // Only strip the flag pair when the flag is present: with allowIndex = -1,
17
+ // filtering out index `allowIndex + 1` would silently drop the first
18
+ // artifact path (a lone artifact arg then failed as "no artifact path").
19
+ const args = allowIndex >= 0
20
+ ? rawArgs.filter((_, index) => index !== allowIndex && index !== allowIndex + 1)
21
+ : rawArgs;
22
+ if (!args.length) throw new Error("at least one artifact path is required");
23
+
24
+ const root = realpathSync(process.cwd());
25
+ const findings = [];
26
+ const evidence = allowEvidencePath ? JSON.parse(readFileSync(allowEvidencePath, "utf8")) : { allowances: [] };
27
+ if (allowEvidencePath && (evidence.schemaVersion !== 1 || evidence.kind !== "rightkit-hardening-evidence")) {
28
+ throw new Error("invalid hardening evidence");
29
+ }
30
+ const allowances = evidence.allowances ?? [];
31
+ for (const allowance of allowances) {
32
+ const sourceMatch = typeof allowance.sourceEvidence === "string" ? /^(.*):([1-9]\d*)$/.exec(allowance.sourceEvidence) : null;
33
+ const knownRule = HARDENING_RULES[allowance.rule];
34
+ if (!knownRule || allowance.exact !== knownRule.exact || !sourceMatch || !/^[a-f0-9]{64}$/.test(allowance.artifactSha256 ?? "") || !/^[a-f0-9]{64}$/.test(allowance.sourceSha256 ?? "") || allowance.sourceLine !== Number(sourceMatch[2]) || typeof allowance.rationale !== "string" || allowance.rationale.trim().length < 12) {
35
+ throw new Error("invalid hardening allowance source binding");
36
+ }
37
+ const sourcePath = realpathSync(path.resolve(root, sourceMatch[1]));
38
+ const sourceRelative = path.relative(root, sourcePath);
39
+ if (sourceRelative === ".." || sourceRelative.startsWith("../") || sourceRelative.startsWith("..\\") || path.isAbsolute(sourceRelative)) {
40
+ throw new Error("hardening allowance source escapes release root");
41
+ }
42
+ const sourceBytes = readFileSync(sourcePath);
43
+ if (createHash("sha256").update(sourceBytes).digest("hex") !== allowance.sourceSha256) throw new Error("hardening allowance source digest mismatch");
44
+ const line = sourceBytes.toString("utf8").split(/\r?\n/)[allowance.sourceLine - 1];
45
+ if (typeof line !== "string" || !line.includes(allowance.exact)) throw new Error("hardening allowance source token mismatch");
46
+ }
47
+
48
+ const fileNameRules = [
49
+ [/\.map$/i, "source map shipped"],
50
+ [/\.pdb$/i, "Windows debug symbols shipped"],
51
+ [/\.dSYM(?:$|[/\\])/i, "macOS debug symbols shipped"],
52
+ [/\.mlpackage(?:$|[/\\])/i, "raw CoreML package shipped"],
53
+ [/[/\\]docs[/\\]experiments(?:$|[/\\])/i, "experiment docs shipped"],
54
+ [/[/\\]\.scratch(?:$|[/\\])/i, "scratch directory shipped"],
55
+ [/[/\\]\.git(?:$|[/\\])/i, "git metadata shipped"],
56
+ [/[/\\]node_modules(?:$|[/\\])/i, "node_modules shipped"],
57
+ [/[/\\](?:src|tests?|benches|benchmarks)(?:$|[/\\])/i, "source/test tree shipped"],
58
+ [/[/\\]tdt_w15_p6(?:$|[/\\])/i, "stale TDT p6 model shipped"],
59
+ ];
60
+
61
+ const contentRules = [
62
+ ["local-source-path", /\/Users\/adrdsouza\/claude/gi, "local source path leaked"],
63
+ ["local-cargo-path", /\/Users\/adrdsouza\/\.cargo/gi, "local cargo path leaked"],
64
+ ["local-windows-path", /C:\\Users\\adrdsouza\\(?:claude|\.cargo)/gi, "local Windows source path leaked"],
65
+ ["experiment-path", /docs\/experiments/gi, "experiment path leaked"],
66
+ ["cleanup-prompt-marker", /l3_cleanup_prompt/gi, "L3 cleanup prompt marker leaked"],
67
+ ["system-prompt-marker", /SYSTEM_PROMPT/gi, "system prompt marker leaked"],
68
+ ["secret-env-name", /OPENAI_API_KEY|ANTHROPIC_API_KEY|APPLE_PASSWORD|NOTARY_PROFILE/gi, "secret/env key name leaked"],
69
+ ["gpl-flag", /--enable-gpl|--enable-nonfree/gi, "GPL/nonfree ffmpeg flag leaked"],
70
+ ["stale-model", /tdt_w15_p6/gi, "stale TDT p6 string leaked"],
71
+ ["experiment-model", /Canary-Qwen|Nemotron|dqlstm|bias-experiments/gi, "experiment/model-history marker leaked"],
72
+ ];
73
+
74
+ const maxPatternLen = Math.max(...contentRules.map(([, rule]) => String(rule).length), 256);
75
+
76
+ function add(kind, file, message, detail = "") {
77
+ findings.push({ kind, file: path.relative(root, file) || file, message, detail });
78
+ }
79
+
80
+ async function walk(target) {
81
+ const st = await lstat(target);
82
+ const normalized = target.split(path.sep).join("/");
83
+ for (const [rule, message] of fileNameRules) if (rule.test(normalized)) add("path", target, message);
84
+ if (st.isDirectory()) {
85
+ const entries = await readdir(target);
86
+ await Promise.all(entries.map((name) => walk(path.join(target, name))));
87
+ return;
88
+ }
89
+ if (st.isFile()) await scanFile(target);
90
+ }
91
+
92
+ function scanFile(file) {
93
+ return new Promise((resolveScan) => {
94
+ const artifactSha256 = createHash("sha256").update(readFileSync(file)).digest("hex");
95
+ const stream = createReadStream(file, { highWaterMark: 256 * 1024 });
96
+ let tail = "";
97
+ stream.on("data", (chunk) => {
98
+ const text = tail + chunk.toString("latin1");
99
+ for (const [ruleId, rule, message] of contentRules) {
100
+ if (/ffmpeg-LICENSE\.txt$/i.test(file) && ruleId === "gpl-flag") continue;
101
+ rule.lastIndex = 0;
102
+ for (const match of text.matchAll(rule)) {
103
+ const allowed = allowances.some((allowance) =>
104
+ allowance.rule === ruleId &&
105
+ allowance.artifactSha256 === artifactSha256 &&
106
+ allowance.exact === match[0] &&
107
+ typeof allowance.sourceEvidence === "string" &&
108
+ typeof allowance.rationale === "string"
109
+ );
110
+ if (!allowed) add("content", file, message, rule.source);
111
+ }
112
+ }
113
+ tail = text.slice(-maxPatternLen);
114
+ });
115
+ stream.on("error", (error) => {
116
+ add("read", file, "could not scan file", error.message);
117
+ resolveScan();
118
+ });
119
+ stream.on("end", resolveScan);
120
+ });
121
+ }
122
+
123
+ for (const arg of args) {
124
+ const target = path.resolve(arg);
125
+ try {
126
+ await walk(target);
127
+ } catch (error) {
128
+ add("target", target, "could not scan target", error.message);
129
+ }
130
+ }
131
+
132
+ const unique = new Map();
133
+ for (const finding of findings) unique.set([finding.kind, finding.file, finding.message].join("\0"), finding);
134
+ const deduped = [...unique.values()].sort((left, right) =>
135
+ (left.file + " " + left.message).localeCompare(right.file + " " + right.message),
136
+ );
137
+
138
+ if (deduped.length) {
139
+ console.error("hardeningscan: " + deduped.length + " finding(s)");
140
+ for (const finding of deduped) console.error("- [" + finding.kind + "] " + finding.file + ": " + finding.message);
141
+ process.exit(1);
142
+ }
143
+
144
+ console.log("hardeningscan: clean");
@@ -1,13 +1,13 @@
1
- import { spawnSync } from "./background-process.mjs";
2
-
3
- export function resolveMacosDeveloperIdIdentity({ env = process.env, run = spawnSync } = {}) {
4
- const configured = env.APPLE_DEVELOPER_ID?.trim();
5
- if (configured) return configured;
6
- const result = run("security", ["find-identity", "-v", "-p", "codesigning"], { encoding: "utf8" });
7
- if (result.status !== 0) throw new Error("could not inspect macOS signing identities in Keychain");
8
- const identities = [...String(result.stdout ?? "").matchAll(/"([^"\n]*Developer ID Application:[^"]+)"/g)]
9
- .map((match) => match[1]);
10
- if (identities.length === 1) return identities[0];
11
- if (!identities.length) throw new Error("no Developer ID Application signing identity found in Keychain");
12
- throw new Error(`multiple Developer ID Application signing identities found in Keychain; set APPLE_DEVELOPER_ID explicitly (${identities.join(", ")})`);
13
- }
1
+ import { spawnSync } from "./background-process.mjs";
2
+
3
+ export function resolveMacosDeveloperIdIdentity({ env = process.env, run = spawnSync } = {}) {
4
+ const configured = env.APPLE_DEVELOPER_ID?.trim();
5
+ if (configured) return configured;
6
+ const result = run("security", ["find-identity", "-v", "-p", "codesigning"], { encoding: "utf8" });
7
+ if (result.status !== 0) throw new Error("could not inspect macOS signing identities in Keychain");
8
+ const identities = [...String(result.stdout ?? "").matchAll(/"([^"\n]*Developer ID Application:[^"]+)"/g)]
9
+ .map((match) => match[1]);
10
+ if (identities.length === 1) return identities[0];
11
+ if (!identities.length) throw new Error("no Developer ID Application signing identity found in Keychain");
12
+ throw new Error(`multiple Developer ID Application signing identities found in Keychain; set APPLE_DEVELOPER_ID explicitly (${identities.join(", ")})`);
13
+ }
package/managed-cargo.mjs CHANGED
@@ -1,29 +1,29 @@
1
- import { existsSync } from "node:fs";
2
- import path from "node:path";
3
- import { spawnSync } from "./background-process.mjs";
4
-
5
- export function managedRuntimeClient(env = process.env, platform = process.platform, exists = existsSync) {
6
- const roots = [env.RIGHTKIT_MANAGED_ROOT, env.RIGHTKIT_EXTERNAL_ROOT,
7
- platform === "win32" ? "D:\\.rightkit-managed" : platform === "darwin" ? "/Volumes/D/.rightkit-managed" : undefined]
8
- .filter((root) => typeof root === "string" && root.length > 0);
9
- for (const root of roots) {
10
- const client = path.join(root, "runtime-current", "build-control-clients", "src", "rightkit.mjs");
11
- if (exists(client)) return client;
12
- }
13
- return undefined;
14
- }
15
-
16
- function managedHost(env) {
17
- return Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET)
18
- || /(?:^|[;:])[^;:]*[/\\](?:\.rightkit-managed|rightkitmanagedagent)[/\\]agent-bin(?:[;:]|$)/i.test(String(env.PATH ?? ""));
19
- }
20
-
21
- export function runCargoSync(args, options = {}) {
22
- const env = options.env ?? process.env;
23
- const client = managedRuntimeClient(env);
24
- if (client) return spawnSync(process.execPath, [client, "cargo", ...args], { ...options, cwd: process.cwd() });
25
- if (managedHost(env) && process.platform === "win32") {
26
- return { status: null, stdout: "", stderr: "", error: new Error("installed RightKit runtime client is unavailable for managed Cargo metadata") };
27
- }
28
- return spawnSync("cargo", args, options);
29
- }
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "./background-process.mjs";
4
+
5
+ export function managedRuntimeClient(env = process.env, platform = process.platform, exists = existsSync) {
6
+ const roots = [env.RIGHTKIT_MANAGED_ROOT, env.RIGHTKIT_EXTERNAL_ROOT,
7
+ platform === "win32" ? "D:\\.rightkit-managed" : platform === "darwin" ? "/Volumes/D/.rightkit-managed" : undefined]
8
+ .filter((root) => typeof root === "string" && root.length > 0);
9
+ for (const root of roots) {
10
+ const client = path.join(root, "runtime-current", "build-control-clients", "src", "rightkit.mjs");
11
+ if (exists(client)) return client;
12
+ }
13
+ return undefined;
14
+ }
15
+
16
+ function managedHost(env) {
17
+ return Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET)
18
+ || /(?:^|[;:])[^;:]*[/\\](?:\.rightkit-managed|rightkitmanagedagent)[/\\]agent-bin(?:[;:]|$)/i.test(String(env.PATH ?? ""));
19
+ }
20
+
21
+ export function runCargoSync(args, options = {}) {
22
+ const env = options.env ?? process.env;
23
+ const client = managedRuntimeClient(env);
24
+ if (client) return spawnSync(process.execPath, [client, "cargo", ...args], { ...options, cwd: process.cwd() });
25
+ if (managedHost(env) && process.platform === "win32") {
26
+ return { status: null, stdout: "", stderr: "", error: new Error("installed RightKit runtime client is unavailable for managed Cargo metadata") };
27
+ }
28
+ return spawnSync("cargo", args, options);
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.107",
3
+ "version": "0.2.109",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",