@rightkit/release 0.6.6 → 0.6.8

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.
Files changed (46) hide show
  1. package/README.md +132 -10
  2. package/admission-files.mjs +3 -2
  3. package/advisory-db.json +4 -0
  4. package/advisory-db.mjs +124 -0
  5. package/advisory-exceptions.mjs +13 -10
  6. package/app-check.mjs +257 -38
  7. package/bin/darwin/rightkit-swap +0 -0
  8. package/bin/rightkit-agent-hook.mjs +9 -0
  9. package/bin/rightkit-gate.mjs +524 -345
  10. package/build-release.mjs +13 -6
  11. package/cli/right-release.mjs +29 -6
  12. package/compile-inputs.mjs +66 -0
  13. package/consumer-contracts.json +1044 -0
  14. package/consumer-contracts.mjs +627 -0
  15. package/consumer-input-contracts.mjs +422 -0
  16. package/consumer-policy.mjs +588 -0
  17. package/consumer-source-contracts.mjs +469 -0
  18. package/deny-default.toml +42 -0
  19. package/deterministic-scans.mjs +240 -29
  20. package/disk-command.mjs +270 -30
  21. package/disk-policy.json +50 -10
  22. package/disk-policy.mjs +482 -35
  23. package/disk-report.mjs +129 -0
  24. package/door-policy.mjs +228 -0
  25. package/door.mjs +23 -0
  26. package/gate-executor.mjs +30 -0
  27. package/gate-stages.mjs +110 -0
  28. package/github-updater-release.mjs +48 -8
  29. package/init-command.mjs +41 -0
  30. package/legion-semantic-contracts.mjs +428 -0
  31. package/ownership-class-rules.mjs +277 -0
  32. package/ownership-key-authority.mjs +3 -1
  33. package/ownership-mandatory-set.mjs +3 -2
  34. package/package.json +11 -3
  35. package/publish-cargo.mjs +5 -2
  36. package/publish-update.mjs +14 -11
  37. package/release-config.mjs +51 -6
  38. package/rightkit-versions.json +172 -167
  39. package/shell-contract-bridge.json +251 -0
  40. package/shell-contract.mjs +1069 -0
  41. package/sign-updater.mjs +24 -4
  42. package/source-closure-rules.mjs +204 -0
  43. package/stage-sidecar-command.mjs +2 -1
  44. package/stage-sidecar.mjs +437 -114
  45. package/suite-api.generated.mjs +96 -6
  46. 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 requires an explicit trust root & full-byte signature
692
- // verification before any seal directory or artifact copy is created.
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
- try { assertUpdaterSignature(readFileSync(source), signature, updaterPublicKey, `signature of ${path.basename(artifact.file)}`); }
701
- catch (error) { fail(`updater signature rejected at seal: ${error.message}`); }
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
@@ -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
- // The same offline check as `rightkit-gate check`; the repository is the git top-level of --config's folder, else the cwd.
107
- const { appDirectory, main } = await import("../app-check.mjs");
108
- process.exit(await main(args.slice(1), { label: "check", cwd: appDirectory(args, process.cwd()) }));
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 self-check, check, dry-run sweep, move sweep, warn (the scheduled job)
377
- disk schedule install|uninstall|status Daily launchd job (Mac) / hidden Task Scheduler entry (Windows), bound to the stable per-user copy
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
+ }