@rightkit/release 0.6.6 → 0.6.7
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 +122 -9
- package/admission-files.mjs +3 -2
- package/advisory-db.json +4 -0
- package/advisory-db.mjs +124 -0
- package/advisory-exceptions.mjs +13 -10
- package/app-check.mjs +257 -38
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-agent-hook.mjs +9 -0
- package/bin/rightkit-gate.mjs +495 -344
- package/build-release.mjs +13 -6
- package/cli/right-release.mjs +29 -6
- package/compile-inputs.mjs +66 -0
- package/consumer-contracts.json +1044 -0
- package/consumer-contracts.mjs +627 -0
- package/consumer-input-contracts.mjs +422 -0
- package/consumer-policy.mjs +588 -0
- package/consumer-source-contracts.mjs +469 -0
- package/deny-default.toml +42 -0
- package/deterministic-scans.mjs +240 -29
- package/disk-command.mjs +270 -30
- package/disk-policy.json +50 -10
- package/disk-policy.mjs +482 -35
- package/disk-report.mjs +129 -0
- package/door-policy.mjs +228 -0
- package/gate-executor.mjs +30 -0
- package/gate-stages.mjs +110 -0
- package/github-updater-release.mjs +48 -8
- package/init-command.mjs +41 -0
- package/legion-semantic-contracts.mjs +428 -0
- package/ownership-class-rules.mjs +277 -0
- package/ownership-key-authority.mjs +3 -1
- package/ownership-mandatory-set.mjs +3 -2
- package/package.json +10 -3
- package/publish-cargo.mjs +5 -2
- package/publish-update.mjs +14 -11
- package/release-config.mjs +51 -6
- package/rightkit-versions.json +172 -167
- package/shell-contract-bridge.json +251 -0
- package/shell-contract.mjs +1069 -0
- package/sign-updater.mjs +24 -4
- package/source-closure-rules.mjs +204 -0
- package/stage-sidecar-command.mjs +2 -1
- package/stage-sidecar.mjs +437 -114
- package/suite-api.generated.mjs +96 -6
- package/suite-trust-roots.mjs +198 -0
package/build-release.mjs
CHANGED
|
@@ -52,7 +52,7 @@ import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
|
52
52
|
import { ANDROID_RECEIPT_NAME, androidSealEntry, validateAndroidTarget, verifyAndroidReceipt } from "./android-release.mjs";
|
|
53
53
|
import { IOS_RECEIPT_NAME, iosSealEntry, verifyIosReceipt } from "./ios-package-recipe.mjs";
|
|
54
54
|
import { expandIosPackageTarget } from "./package-recipe-targets.mjs";
|
|
55
|
-
import { assertUpdaterSignature } from "./github-updater-release.mjs";
|
|
55
|
+
import { assertUpdaterKeyId, assertUpdaterSignature, isSuiteUpdaterApp, resolveUpdaterTrust } from "./github-updater-release.mjs";
|
|
56
56
|
|
|
57
57
|
// The iOS lane's own packaging code, kept out of the win/mac fingerprint for the same reason as the Android one.
|
|
58
58
|
const IOS_PIPELINE_FINGERPRINT = createHash("sha256")
|
|
@@ -688,17 +688,24 @@ function sealRelease({ configRoot, managedCargoTarget, androidTarget, iosSpec, n
|
|
|
688
688
|
}
|
|
689
689
|
if (sources.size === 0) fail(`nothing to seal: declare installer.artifacts, updater.artifacts or artifacts for the ${platform} target`);
|
|
690
690
|
for (const file of sources.keys()) if (!existsSync(file)) fail(`cannot seal missing artifact: ${file}`);
|
|
691
|
-
// Every minisign updater
|
|
692
|
-
//
|
|
691
|
+
// Every minisign updater is checked against a trust root, by key id and over the full bytes, before any seal directory or
|
|
692
|
+
// artifact copy is created. A Right Suite app's root is the Right Suite contract's; any other app must declare its own key.
|
|
693
693
|
const updaterPublicKey = config.distribution?.updaterPublicKey ?? config.distribution?.updaterPubkey ?? target?.updater?.pubkey ?? target?.updater?.publicKey;
|
|
694
694
|
if ((target.updater?.artifacts?.length ?? 0) > 0 && target.updater?.kind !== "appinstaller") {
|
|
695
|
-
if (typeof updaterPublicKey !== "string" || !updaterPublicKey.trim()) fail("updater public key is required to verify updater artifacts before sealing");
|
|
695
|
+
if (!isSuiteUpdaterApp(config.app) && (typeof updaterPublicKey !== "string" || !updaterPublicKey.trim())) fail("updater public key is required to verify updater artifacts before sealing");
|
|
696
|
+
let trust;
|
|
697
|
+
try { trust = resolveUpdaterTrust({ app: config.app, configuredPublicKey: updaterPublicKey, configuredKeyId: config.distribution?.updaterKeyId }); }
|
|
698
|
+
catch (error) { fail(`updater trust rejected at seal: ${error.message}`); }
|
|
696
699
|
for (const artifact of target.updater?.artifacts ?? []) {
|
|
697
700
|
const source = resolveArtifactPath(artifact.file);
|
|
698
701
|
const signature = readFileSync(resolveArtifactPath(artifact.signature), "utf8").trim();
|
|
699
702
|
if (!signature) fail(`updater signature is empty, refusing to seal: ${artifact.signature}`);
|
|
700
|
-
|
|
701
|
-
|
|
703
|
+
const label = `signature of ${path.basename(artifact.file)}`;
|
|
704
|
+
// The id comes first: a signature by another key would otherwise fail verification without saying whose it is.
|
|
705
|
+
try {
|
|
706
|
+
assertUpdaterKeyId(signature, trust.keyId, label, { app: config.app });
|
|
707
|
+
assertUpdaterSignature(readFileSync(source), signature, trust.publicKey, label);
|
|
708
|
+
} catch (error) { fail(`updater signature rejected at seal: ${error.message}`); }
|
|
702
709
|
}
|
|
703
710
|
}
|
|
704
711
|
// Android: the build receipt must describe exactly the AAB being sealed (bytes re-hashed) and its signer must be the pinned
|
package/cli/right-release.mjs
CHANGED
|
@@ -57,8 +57,27 @@ const args = process.argv.slice(2);
|
|
|
57
57
|
const first = args[0];
|
|
58
58
|
// The commands that compile, package, sign, ship or inspect a release run the app check (../app-check.mjs, CHECKED_COMMANDS) in this
|
|
59
59
|
// process before they start a child. A red check prints its findings and exits 1; there is no flag that skips it.
|
|
60
|
+
const WORKTREE_COMPILES = new Set(["dev", "test", "stage-sidecar"]);
|
|
60
61
|
async function requireGreenCheck() {
|
|
61
|
-
const { guardCommand } = await import("../app-check.mjs");
|
|
62
|
+
const { guardCommand, checkedCommandOf, checkModeFor, appDirectory } = await import("../app-check.mjs");
|
|
63
|
+
// One door (Adrian, 2026-10-25): a commit-bound command (build, package, release, qa, upload, publish, signing) runs only when
|
|
64
|
+
// rightkit-gate --to launched it for HEAD, after class A and the compile stage passed.
|
|
65
|
+
const checked = checkedCommandOf(args);
|
|
66
|
+
// dev, test and stage-sidecar work on the uncommitted tree but compile, so they too run only under a door run
|
|
67
|
+
// (rightkit-gate --to dev|test, class A on the working tree first).
|
|
68
|
+
// Gate on the mode, not the key: a flag-first call (`right-release --platform mac`) is a build with no command word.
|
|
69
|
+
if (checkModeFor(args) === "commit" || (checked && WORKTREE_COMPILES.has(checked))) {
|
|
70
|
+
const { requireDoorRun } = await import("../gate-stages.mjs");
|
|
71
|
+
const { spawnSync } = await import("../background-process.mjs");
|
|
72
|
+
const cwd = appDirectory(args, process.cwd());
|
|
73
|
+
const gitOut = (a) => { const r = spawnSync("git", a, { cwd, encoding: "utf8" }); return r.status === 0 ? r.stdout.trim() : ""; };
|
|
74
|
+
const common = gitOut(["rev-parse", "--git-common-dir"]);
|
|
75
|
+
// Outside a git repository the app check below refuses (nothing can be bound to a commit); inside one, only a door run admits.
|
|
76
|
+
if (common) {
|
|
77
|
+
const refusal = requireDoorRun({ commonDir: path.resolve(cwd, common), commit: gitOut(["rev-parse", "HEAD"]), command: `right-release ${checked ?? args.join(" ")}`.trim() });
|
|
78
|
+
if (refusal) { console.error(`right-release: ${refusal}`); process.exit(2); }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
62
81
|
const refused = guardCommand({ args });
|
|
63
82
|
if (!refused) return;
|
|
64
83
|
for (const line of refused.lines) console.error(line);
|
|
@@ -103,9 +122,11 @@ if (first === "--version" || first === "-v") {
|
|
|
103
122
|
run("release.mjs", ["--doctor", ...args.slice(1)]);
|
|
104
123
|
}
|
|
105
124
|
} else if (first === "check") {
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
// Exactly `rightkit-gate check` (one door): the app check, then every class A scan over the whole commit. The repository is the git
|
|
126
|
+
// top-level of --config's folder, else the cwd.
|
|
127
|
+
const { appDirectory } = await import("../app-check.mjs");
|
|
128
|
+
const { main } = await import("../bin/rightkit-gate.mjs");
|
|
129
|
+
process.exit(await main(["check", ...args.slice(1)], { cwd: appDirectory(args, process.cwd()) }));
|
|
109
130
|
} else if (first === "hooks" && args[1] === "install") {
|
|
110
131
|
// One installer: `hooks install` is `rightkit-gate install-hook` (pre-push check and gate, ownership pre-commit step) run in the app.
|
|
111
132
|
const app = args.indexOf("--app");
|
|
@@ -373,8 +394,10 @@ Commands:
|
|
|
373
394
|
disk check [--json] Per-writer sizes vs caps and free-space floor (exit 3 on warning)
|
|
374
395
|
disk sweep [--dry-run] [--days N] [--delete] [--json]
|
|
375
396
|
Move stale idle heavy data to the heavy volume (delete only with --delete)
|
|
376
|
-
disk run
|
|
377
|
-
disk
|
|
397
|
+
disk run [--auto|--light] self-check, then the full pass (check, sweep, purge, daily report); --light is the stat-only hourly pass (floors, temp-like writers, scratch; purges only below the agent volume's soft floor); --auto picks full once a day after 03:30, else light (the scheduled job)
|
|
398
|
+
disk run-root <name> [--days N] [--json] Lease a run directory under the scratch root (default 3 days): the sweep skips it until the lease ends
|
|
399
|
+
disk run-root --release <name> End a run-root lease
|
|
400
|
+
disk schedule install|uninstall|status Hourly launchd job (Mac) / hidden Task Scheduler entry (Windows) running "disk run --auto", bound to the stable per-user copy; status fails when no full pass finished in 26 h
|
|
378
401
|
deps --check|--audit|--update Shared dependency lane
|
|
379
402
|
update check|apply|gate|land|discard|run|schedule [--repo <path>]
|
|
380
403
|
Run the pinned @rightkit/outdated updater (right-update) that moves stale pins; reads .rightkit/update.json
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The declared compile inputs a RustRight broker captured for a compile (RIGHTKIT_COMPILE_INPUTS): the gate judges exactly
|
|
2
|
+
// those bytes, where the broker copied them, so what is checked is what the compile reads. The tree's tracked files are
|
|
3
|
+
// judged from the commit as always; the declared inputs are the ignored paths (a built `dist`, generated assets) the
|
|
4
|
+
// commit does not hold, listed in the captured tree's `.rightkit/compile-inputs.json`.
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { spawnSync } from "./background-process.mjs";
|
|
9
|
+
import { SECRET_PATTERNS } from "./publish-cargo.mjs";
|
|
10
|
+
|
|
11
|
+
export const COMPILE_INPUTS_FILE = ".rightkit/compile-inputs.json";
|
|
12
|
+
|
|
13
|
+
/** Files of one declared input in the broker's order (children by name), refusing links; [{ name, abs }]. */
|
|
14
|
+
function inputFiles(root, input) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const walk = (abs, name) => {
|
|
17
|
+
const st = fs.lstatSync(abs);
|
|
18
|
+
if (st.isSymbolicLink()) throw new Error(`declared compile input holds a symbolic link at ${name}`);
|
|
19
|
+
if (st.isDirectory()) {
|
|
20
|
+
for (const child of fs.readdirSync(abs).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))) walk(path.join(abs, child), `${name}/${child}`);
|
|
21
|
+
} else if (st.isFile()) out.push({ name, abs });
|
|
22
|
+
};
|
|
23
|
+
walk(path.join(root, input), input);
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Judges the captured declared inputs under `root`: each declared path must exist without links; every `.js/.mjs/.cjs`
|
|
29
|
+
* file parses (`node --check`), every `.json` file parses, and no file carries a secret. Returns { inputs: [{ path, sha256,
|
|
30
|
+
* files }], findings: [string] }; `sha256` is computed as the broker records it (name, NUL, bytes, NUL per file, in order),
|
|
31
|
+
* so the receipt and the gate name the same bytes.
|
|
32
|
+
*/
|
|
33
|
+
export function judgeCompileInputs(root, { spawn = spawnSync, execPath = process.execPath } = {}) {
|
|
34
|
+
const findings = [];
|
|
35
|
+
let declared = [];
|
|
36
|
+
const file = path.join(root, COMPILE_INPUTS_FILE);
|
|
37
|
+
if (!fs.existsSync(file)) return { inputs: [], findings };
|
|
38
|
+
try {
|
|
39
|
+
declared = JSON.parse(fs.readFileSync(file, "utf8")).inputs;
|
|
40
|
+
if (!Array.isArray(declared) || declared.some((p) => typeof p !== "string")) throw new Error("`inputs` must be a list of paths");
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { inputs: [], findings: [`${COMPILE_INPUTS_FILE}: ${e.message}`] };
|
|
43
|
+
}
|
|
44
|
+
const inputs = [];
|
|
45
|
+
for (const input of declared) {
|
|
46
|
+
if (path.isAbsolute(input) || input.split(/[\\/]/).some((part) => part === ".." || part === "." || part === "")) { findings.push(`${input}: not a plain relative path`); continue; }
|
|
47
|
+
let files;
|
|
48
|
+
try { files = inputFiles(root, input); } catch (e) { findings.push(`${input}: ${e.code === "ENOENT" ? "missing from the captured tree" : e.message}`); continue; }
|
|
49
|
+
const hash = crypto.createHash("sha256");
|
|
50
|
+
for (const f of files) {
|
|
51
|
+
const bytes = fs.readFileSync(f.abs);
|
|
52
|
+
hash.update(f.name); hash.update(Buffer.from([0])); hash.update(bytes); hash.update(Buffer.from([0]));
|
|
53
|
+
const text = bytes.toString("utf8");
|
|
54
|
+
if (SECRET_PATTERNS.some((pattern) => pattern.test(text))) findings.push(`${f.name}: holds a secret (token or private key pattern)`);
|
|
55
|
+
if (/\.(m|c)?js$/.test(f.name)) {
|
|
56
|
+
const r = spawn(execPath, ["--check", f.abs], { encoding: "utf8" });
|
|
57
|
+
if (r.status !== 0) findings.push(`${f.name}: does not parse (${String(r.stderr ?? "").split("\n").find((l) => /Error/.test(l))?.trim() ?? `node --check exit ${r.status}`})`);
|
|
58
|
+
}
|
|
59
|
+
if (/\.json$/.test(f.name)) {
|
|
60
|
+
try { JSON.parse(text); } catch (e) { findings.push(`${f.name}: invalid JSON (${e.message})`); }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
inputs.push({ path: input, sha256: hash.digest("hex"), files: files.length });
|
|
64
|
+
}
|
|
65
|
+
return { inputs, findings };
|
|
66
|
+
}
|