@rightkit/release 0.6.13 → 0.6.14
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 +49 -1
- package/adapter-env.mjs +165 -0
- package/android-ndk.mjs +5 -1
- package/android-release.mjs +73 -8
- package/app-check.mjs +22 -8
- package/app-closure.mjs +214 -0
- package/assemble-app-command.mjs +24 -7
- package/atomic-write.mjs +57 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +1190 -337
- package/broker-client.mjs +106 -0
- package/broker-exception.mjs +211 -0
- package/broker-handshake.mjs +160 -0
- package/broker-pins.mjs +27 -8
- package/broker-wire.mjs +262 -0
- package/build-launch.mjs +364 -12
- package/build-ownership.mjs +117 -49
- package/build-release.mjs +3 -2
- package/cache-command.mjs +7 -2
- package/candidate-build-receipt.mjs +96 -0
- package/candidate-receipt-command.mjs +29 -0
- package/cargo-contract.mjs +4 -3
- package/cargo-ledger.mjs +1 -1
- package/cargo-license-notices.mjs +2 -2
- package/cargo-shapes.mjs +332 -0
- package/cargo-target-shim.mjs +44 -5
- package/cargo-target.mjs +5 -0
- package/carried-adapter-run.mjs +31 -0
- package/carried-cargo-run.mjs +9 -0
- package/carried-cargo.mjs +237 -0
- package/carrier-census.mjs +124 -0
- package/class-a-launch-guard.mjs +266 -33
- package/cli/right-release.mjs +2 -0
- package/clippy-route.mjs +23 -11
- package/compile-inputs.mjs +9 -12
- package/control-plane.mjs +18 -0
- package/deps-cargo-lock.mjs +208 -0
- package/deps.mjs +5 -0
- package/deterministic-scans.mjs +273 -44
- package/development-signing.mjs +14 -1
- package/development.mjs +109 -24
- package/direct-origin.mjs +107 -0
- package/disk-command.mjs +1 -0
- package/disk-policy.mjs +18 -3
- package/freshness-census.mjs +206 -58
- package/gate-executor.mjs +30 -12
- package/gate-stages.mjs +93 -16
- package/github-release.mjs +2 -1
- package/github-upload-gate.mjs +2 -1
- package/install-tauri-authenticode.mjs +346 -0
- package/ios-dev.mjs +77 -23
- package/ios-package-recipe.mjs +100 -41
- package/linux-artifacts.mjs +17 -2
- package/linux-qualifier.mjs +83 -0
- package/local-publish.mjs +36 -14
- package/local-runtime.mjs +2 -2
- package/lock-identity.mjs +138 -0
- package/lsclean.sh +0 -0
- package/managed-cargo.mjs +151 -44
- package/managed-roots.mjs +130 -12
- package/media-sidecar-catalog.mjs +1 -1
- package/native-adapters.mjs +411 -0
- package/native-helpers.mjs +26 -18
- package/native-tool-planning.mjs +94 -0
- package/ownership-mark.mjs +182 -0
- package/pack-digest.mjs +122 -0
- package/package.json +12 -12
- package/platform-dir.mjs +14 -0
- package/pnpm-adapter-grammar.mjs +226 -0
- package/progress-control.mjs +7 -2
- package/provenance-real-receipt.pins.json +7 -0
- package/provenance-verify.mjs +262 -1
- package/publication-outcomes.mjs +21 -0
- package/publish-update.mjs +2 -1
- package/qa-journeys-prebuilt-run.mjs +18 -0
- package/qa-journeys-prebuilt.mjs +148 -0
- package/qa-snapshot-run.mjs +53 -0
- package/qa-snapshot.mjs +164 -0
- package/qualification-verify.mjs +144 -0
- package/qualify-reuse.mjs +38 -0
- package/recipe-build.mjs +16 -3
- package/release-qa-audit.mjs +2 -2
- package/release-qa-command.mjs +2 -1
- package/release-qa-receipt.mjs +3 -2
- package/release-state.mjs +3 -2
- package/release.mjs +14 -3
- package/rightkit-drift.mjs +27 -5
- package/rightkit-versions.json +14 -10
- package/rustright-names.mjs +32 -0
- package/scan-tool-enrolment.mjs +167 -0
- package/sign-windows.mjs +212 -112
- package/signing-files.mjs +35 -0
- package/signing-identity.mjs +7 -0
- package/snapshot-broker.mjs +528 -57
- package/snapshot-check.mjs +95 -11
- package/snapshot-metadata.mjs +123 -0
- package/snapshot-request.mjs +389 -96
- package/stage-binary.mjs +22 -4
- package/stage-commands.mjs +56 -13
- package/stage-sidecar.mjs +93 -21
- package/suite-signing-policy.mjs +657 -0
- package/suite-signing.mjs +223 -0
- package/swift-scratch.mjs +25 -0
- package/tauri-locked.mjs +25 -0
- package/test-command.mjs +164 -29
- package/unit-gate.mjs +201 -31
- package/upload-large.mjs +0 -0
- package/web-build.mjs +2 -2
- package/win-package-recipe.mjs +26 -2
package/release-qa-audit.mjs
CHANGED
|
@@ -15,8 +15,8 @@ import { spawnSync } from "./background-process.mjs";
|
|
|
15
15
|
import { hardenQaReceipt } from "./release-qa-hardening.mjs";
|
|
16
16
|
import { admissionModeFor, loadAdmissionPolicy } from "./release-qa-policy.mjs";
|
|
17
17
|
import { assertInstalledQaAdmission, assertSourceQaAdmission, loadQaFloor, QaAdmissionError, qaReceiptPath, readQaReceipt } from "./release-qa-receipt.mjs";
|
|
18
|
+
import { platformDirOf, platformOfDir } from "./platform-dir.mjs";
|
|
18
19
|
|
|
19
|
-
const platformDirOf = (platform) => (platform === "win" ? "windows" : platform);
|
|
20
20
|
|
|
21
21
|
/** "missing" when no receipt exists; "rejected" for anything else the gate refused (bad receipt, no key, wrong commit). */
|
|
22
22
|
function refusalStatus(error) {
|
|
@@ -106,7 +106,7 @@ function installedStatus({ repoRoot, releaseId, platformDir }) {
|
|
|
106
106
|
if (record) return { uploaded: true, status: record.status, ...(record.reason ? { reason: record.reason } : {}), ...(record.checkedAt ? { checkedAt: record.checkedAt } : {}) };
|
|
107
107
|
if (!published) return { uploaded: false, status: "not-uploaded" };
|
|
108
108
|
// Uploaded before outcomes were recorded: fall back to the receipt file the old gate required.
|
|
109
|
-
const receipt = readQaReceipt(qaReceiptPath({ repoRoot, releaseId, platform: platformDir
|
|
109
|
+
const receipt = readQaReceipt(qaReceiptPath({ repoRoot, releaseId, platform: platformOfDir(platformDir), stage: "installed" }));
|
|
110
110
|
if (receipt?.status === "passed") return { uploaded: true, status: "passed", reason: "legacy upload; receipt file present, not re-verified" };
|
|
111
111
|
return { uploaded: true, status: "missing", reason: "uploaded with no recorded installed QA outcome" };
|
|
112
112
|
}
|
package/release-qa-command.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
buildQaReceipt, currentPolicyBundleDigest, dependencyClosure, ensureQaReceiptKey, evaluateRequiredChecks, loadQaFloor,
|
|
37
37
|
qaReceiptPath, QA_STAGES, sealQaReceipt, sourceIdentity, validateAppQaConfig, writeQaReceipt,
|
|
38
38
|
} from "./release-qa-receipt.mjs";
|
|
39
|
+
import { platformDirOf } from "./platform-dir.mjs";
|
|
39
40
|
|
|
40
41
|
const sha256File = (file) => createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
41
42
|
|
|
@@ -126,7 +127,7 @@ export async function runQa({ stage, platform, releaseIdArg, configPath, carryTo
|
|
|
126
127
|
if (stage === "source") releaseId = `${config.app}-${config.version}-${source.commit.slice(0, 8)}`;
|
|
127
128
|
else {
|
|
128
129
|
if (!releaseId) throw new Error("--release <sealed-id> is required for the installed stage");
|
|
129
|
-
sealed = verifySealedRelease(path.join(repoRoot, ".right-release", "sealed", releaseId, platform
|
|
130
|
+
sealed = verifySealedRelease(path.join(repoRoot, ".right-release", "sealed", releaseId, platformDirOf(platform)));
|
|
130
131
|
// R4: a split binds the sealed release to the selected commit (and its tree), never to the checkout's HEAD.
|
|
131
132
|
if (split) assertBindsSource(release, sealed.manifest, "the sealed release");
|
|
132
133
|
else if (sealed.manifest.commit !== source.commit) throw new Error("sealed release was built from a different commit than the current checkout");
|
package/release-qa-receipt.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// runner binary and the evidence bundle it produced, evaluates the protected QA floor, and MACs the result with a
|
|
10
10
|
// key held in the protected credential store (Keychain / protected file / CI secret) — not app-writable JSON.
|
|
11
11
|
// The floor, this verifier, and the receipt schema are pinned in the protected policy bundle.
|
|
12
|
+
import { writeFileAtomic } from "./atomic-write.mjs";
|
|
12
13
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
13
14
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
14
15
|
import path from "node:path";
|
|
@@ -266,8 +267,8 @@ export function readQaReceipt(file) {
|
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
export function writeQaReceipt(file, receipt) {
|
|
269
|
-
|
|
270
|
-
|
|
270
|
+
// GB-26: the authoritative QA receipt is replaced atomically (temp, fsync, rename), never overwritten in place.
|
|
271
|
+
writeFileAtomic(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
271
272
|
}
|
|
272
273
|
|
|
273
274
|
function loadKeyOrRefuse(opts, command) {
|
package/release-state.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
|
7
7
|
import { bufferQaFindings } from "./qa-control-scan.mjs";
|
|
8
8
|
import { sealedManifestAllowsContentDevtools } from "./devtools-policy.mjs";
|
|
9
9
|
import { assertManifestRegistryEvidence } from "./rightkit-registry-resolver.mjs";
|
|
10
|
+
import { platformDirOf } from "./platform-dir.mjs";
|
|
10
11
|
|
|
11
12
|
export function cacheFingerprint({ cargoLockSha256, rustc, target, architecture, features = [] }) {
|
|
12
13
|
const payload = JSON.stringify({
|
|
@@ -157,7 +158,7 @@ export async function runBuildStateMachine({
|
|
|
157
158
|
ops,
|
|
158
159
|
}) {
|
|
159
160
|
const releaseId = `${app}-${version}-${commit.slice(0, 8)}`;
|
|
160
|
-
const platformDir = platform
|
|
161
|
+
const platformDir = platformDirOf(platform);
|
|
161
162
|
const sealedDir = path.join(root, ".right-release", "sealed", releaseId, platformDir);
|
|
162
163
|
const sealedRoot = path.join(root, ".right-release", "sealed");
|
|
163
164
|
if (existsSync(sealedRoot)) {
|
|
@@ -202,7 +203,7 @@ export async function runBuildStateMachine({
|
|
|
202
203
|
export async function runUploadStateMachine({ root, releaseId, platform, tier, ops = {} }) {
|
|
203
204
|
if (tier !== "patch" && tier !== "update") throw new Error("upload tier is required (patch|update)");
|
|
204
205
|
if (!/^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/.test(String(releaseId))) throw new Error(`invalid release id: ${releaseId}`);
|
|
205
|
-
const platformDir = platform
|
|
206
|
+
const platformDir = platformDirOf(platform);
|
|
206
207
|
const sealedDir = path.join(root, ".right-release", "sealed", releaseId, platformDir);
|
|
207
208
|
const sealed = verifySealedRelease(sealedDir);
|
|
208
209
|
if (sealed.manifest.platform !== platform) throw new Error("sealed release platform mismatch");
|
package/release.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { expandPackageRecipeTarget } from "./package-recipe-targets.mjs";
|
|
|
3
3
|
import { access, readFile, readdir } from "node:fs/promises";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { buildLaunch } from "./build-launch.mjs";
|
|
6
|
+
import { buildLaunch, createNativeRunner } from "./build-launch.mjs";
|
|
7
|
+
import { NATIVE_SIGNING_FILE_ENV, nativeToolOf } from "./native-adapters.mjs";
|
|
7
8
|
import { createRequire } from "node:module";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -735,6 +736,9 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
735
736
|
await runAndroidRelease({
|
|
736
737
|
root, app: config.app ?? path.basename(root), target: androidTarget, dryRun: opts.dryRun,
|
|
737
738
|
runGradle: (gradle, env) => runCommand(gradle, root, { env }),
|
|
739
|
+
// RA-07/RA-08: the production runner of the native compile phases (build-launch.mjs createNativeRunner): the carrier or the resolved executor's client, never a spawn of its own, a fixed result shape with the
|
|
740
|
+
// receipt's output mapping, and the signing-properties file as a typed path handoff (`signingFile`), never an environment. An injected executor is not accepted here.
|
|
741
|
+
native: createNativeRunner({ cwd: root, env: process.env }),
|
|
738
742
|
resolveArtifact: (declared) => resolveReleaseArtifactPath(root, declared),
|
|
739
743
|
scanFiles: (label, files, scanOptions) => assertNoQaControlBinaries(label, files, scanOptions),
|
|
740
744
|
receiptDir: path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts")),
|
|
@@ -745,6 +749,8 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
745
749
|
// --dry-run prints the plan anywhere). Upload is a separate command: `right-release upload --platform ios`.
|
|
746
750
|
await runIosPackageRecipe({
|
|
747
751
|
root, app: config.app ?? path.basename(root), spec: iosSpec, dryRun: opts.dryRun,
|
|
752
|
+
// RA-09: the production runner of the native compile phases (xcodegen, xcodebuild archive/export/test), never an injected executor or a spawn: build-launch.mjs createNativeRunner.
|
|
753
|
+
native: createNativeRunner({ cwd: root, env: process.env }),
|
|
748
754
|
receiptDir: path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts")),
|
|
749
755
|
scanFiles: (label, files, scanOptions) => assertNoQaControlBinaries(label, files, scanOptions),
|
|
750
756
|
// The build number counter outlives every release id: it lives in the vault state root that right-release build exports.
|
|
@@ -1198,8 +1204,13 @@ async function runCommands(value, root, options) {
|
|
|
1198
1204
|
// on a broker-managed host (rightkit#1241).
|
|
1199
1205
|
async function runCommand(command, root, { env = {}, captureStdout = false } = {}) {
|
|
1200
1206
|
const cwd = path.resolve(root, command.cwd ?? ".");
|
|
1201
|
-
|
|
1202
|
-
|
|
1207
|
+
// RA-08: the dynamic signing-properties file path of a native tool (Gradle) is a typed FILE handoff of the provisioned signer session, not a step environment: it is taken out of the environment the door
|
|
1208
|
+
// sees (so it never reaches RUSTRIGHT_ADAPTER_ENV or a client's environment) and handed to the door as `signingFile`. A broker route refuses it before launch when no broker can carry it (build-launch.mjs,
|
|
1209
|
+
// nativeSigningFileRefusal); a direct host's launch puts the same path back in its own Gradle's environment (`target.env`). No password is in it: only the file's path.
|
|
1210
|
+
const nativeCommand = nativeToolOf(command.cmd) !== null;
|
|
1211
|
+
const { [NATIVE_SIGNING_FILE_ENV]: signingFile, ...dynamicEnv } = env;
|
|
1212
|
+
const commandEnvironment = { ...(await commandEnv(command, root)), ...(nativeCommand ? dynamicEnv : env) };
|
|
1213
|
+
const target = buildLaunch(command.cmd, command.args ?? [], { env: { ...process.env, ...commandEnvironment }, stepEnv: command.env, cwd, exists: existsSync, ...(nativeCommand && signingFile ? { signingFile } : {}) });
|
|
1203
1214
|
// A direct (Linux) launch carries PATH without RustRight shim directories (build-launch `env`).
|
|
1204
1215
|
return run(target.command, target.args, cwd, { ...commandEnvironment, ...(target.env ?? {}) }, {
|
|
1205
1216
|
...command,
|
package/rightkit-drift.mjs
CHANGED
|
@@ -62,14 +62,36 @@ export function rightkitDrift(appRoot, versions = loadRightkitVersions()) {
|
|
|
62
62
|
return drift;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const SHIM_EXE_LINE = /^"([A-Za-z]:\\[^"%\r\n<>|&^!*?]+\.exe)" %\*$/m;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* How a command starts (GB-08, BR11/B31). `rustright` on Windows is the managed agent shim `rustright.cmd`, which node cannot start without
|
|
69
|
+
* a shell (EINVAL; a shell would re-quote the argv): its target .exe is read from the shim and started directly, argv array, no shell.
|
|
70
|
+
* Anything else keeps the old launch (pnpm on Windows is a .cmd and keeps its shell). A client that cannot be resolved throws before anything runs.
|
|
71
|
+
*/
|
|
72
|
+
export function launchOf(command, args, { platform = process.platform, spawn = spawnSync, readFile = (f) => readFileSync(f, "utf8") } = {}) {
|
|
73
|
+
if (command !== "rustright") return { command, args, shell: platform === "win32" };
|
|
74
|
+
if (platform !== "win32") return { command, args, shell: false };
|
|
75
|
+
const where = spawn("where", ["rustright"], { encoding: "utf8", windowsHide: true, shell: false, timeout: 20_000 });
|
|
76
|
+
const found = String(where?.stdout ?? "").split(/\r?\n/).map((l) => l.trim()).filter((l) => /^[A-Za-z]:\\/.test(l));
|
|
77
|
+
const exe = found.find((l) => /\.exe$/i.test(l));
|
|
78
|
+
if (exe) return { command: exe, args, shell: false };
|
|
79
|
+
const shim = found.find((l) => /\.(cmd|bat)$/i.test(l));
|
|
80
|
+
let target = null;
|
|
81
|
+
if (shim) { try { target = SHIM_EXE_LINE.exec(readFile(shim))?.[1] ?? null; } catch { target = null; } }
|
|
82
|
+
if (!target) throw new Error("RK_BROKER_CLIENT_UNRESOLVED: the RustRight client (rustright.exe) cannot be resolved from PATH; nothing was started");
|
|
83
|
+
return { command: target, args, shell: false };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function run(command, args, cwd, { spawn = spawnSync, platform = process.platform, readFile } = {}) {
|
|
87
|
+
const launch = launchOf(command, args, { platform, spawn, readFile });
|
|
88
|
+
const result = spawn(launch.command, launch.args, { cwd, stdio: "inherit", windowsHide: true, shell: launch.shell });
|
|
67
89
|
if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed in ${cwd} (exit ${result.status ?? result.error?.message})`);
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
/** Move drifted pins to the latest version (keeping their = ^ ~ prefix), then refresh lockfiles.
|
|
71
93
|
* Breaking bumps need code changes, so they are adopted only with includeBreaking. */
|
|
72
|
-
export function applyRightkitAdoption(appRoot, versions = loadRightkitVersions(), { refreshLocks = true, includeBreaking = false } = {}) {
|
|
94
|
+
export function applyRightkitAdoption(appRoot, versions = loadRightkitVersions(), { refreshLocks = true, includeBreaking = false, runOptions = {} } = {}) {
|
|
73
95
|
const drift = rightkitDrift(appRoot, versions).filter((item) => includeBreaking || !item.breaking);
|
|
74
96
|
const byFile = Map.groupBy(drift, (item) => item.file);
|
|
75
97
|
for (const [file, items] of byFile) {
|
|
@@ -90,8 +112,8 @@ export function applyRightkitAdoption(appRoot, versions = loadRightkitVersions()
|
|
|
90
112
|
if (refreshLocks) {
|
|
91
113
|
for (const file of byFile.keys()) {
|
|
92
114
|
const dir = path.join(appRoot, path.dirname(file));
|
|
93
|
-
if (file.endsWith("package.json")) run("pnpm", ["install", "--lockfile-only", "--ignore-scripts"], dir);
|
|
94
|
-
else run("rustright", ["cargo", "metadata", "--format-version", "1", "--manifest-path", path.join(appRoot, file)], dir);
|
|
115
|
+
if (file.endsWith("package.json")) run("pnpm", ["install", "--lockfile-only", "--ignore-scripts"], dir, runOptions);
|
|
116
|
+
else run("rustright", ["cargo", "metadata", "--format-version", "1", "--manifest-path", path.join(appRoot, file)], dir, runOptions);
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
return drift;
|
package/rightkit-versions.json
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@rightkit/qa": "0.6.2",
|
|
33
33
|
"@rightkit/qa-chrome": "0.1.12",
|
|
34
34
|
"@rightkit/react-shell": "0.7.2",
|
|
35
|
-
"@rightkit/release": "0.6.
|
|
35
|
+
"@rightkit/release": "0.6.14",
|
|
36
36
|
"@rightkit/settings-ui": "0.7.0",
|
|
37
37
|
"@rightkit/tauri": "0.3.0",
|
|
38
38
|
"@rightkit/tauri-shell": "0.1.3",
|
|
@@ -380,7 +380,8 @@
|
|
|
380
380
|
"0.6.8",
|
|
381
381
|
"0.6.10",
|
|
382
382
|
"0.6.11",
|
|
383
|
-
"0.6.12"
|
|
383
|
+
"0.6.12",
|
|
384
|
+
"0.6.13"
|
|
384
385
|
],
|
|
385
386
|
"@rightkit/settings-ui": [
|
|
386
387
|
"0.1.0",
|
|
@@ -470,7 +471,7 @@
|
|
|
470
471
|
"rightkit-crash": "0.3.6",
|
|
471
472
|
"rightkit-credentials": "0.4.2",
|
|
472
473
|
"rightkit-crypto": "0.1.0",
|
|
473
|
-
"rightkit-daemon": "0.
|
|
474
|
+
"rightkit-daemon": "0.5.0",
|
|
474
475
|
"rightkit-db": "0.5.3",
|
|
475
476
|
"rightkit-device": "0.4.0",
|
|
476
477
|
"rightkit-docs": "0.1.3",
|
|
@@ -485,7 +486,7 @@
|
|
|
485
486
|
"rightkit-http": "0.3.9",
|
|
486
487
|
"rightkit-identity": "0.1.0",
|
|
487
488
|
"rightkit-job-queue": "0.5.1",
|
|
488
|
-
"rightkit-license": "0.2.
|
|
489
|
+
"rightkit-license": "0.2.6",
|
|
489
490
|
"rightkit-llm": "0.3.1",
|
|
490
491
|
"rightkit-logs": "0.3.6",
|
|
491
492
|
"rightkit-mcp": "0.7.1",
|
|
@@ -500,7 +501,7 @@
|
|
|
500
501
|
"rightkit-pdf": "0.3.4",
|
|
501
502
|
"rightkit-platform-rules": "0.1.0",
|
|
502
503
|
"rightkit-playback": "0.3.0",
|
|
503
|
-
"rightkit-qa": "0.
|
|
504
|
+
"rightkit-qa": "0.10.0",
|
|
504
505
|
"rightkit-qa-driver": "0.7.0",
|
|
505
506
|
"rightkit-record": "0.3.3",
|
|
506
507
|
"rightkit-review": "0.4.0",
|
|
@@ -508,7 +509,7 @@
|
|
|
508
509
|
"rightkit-search": "0.4.1",
|
|
509
510
|
"rightkit-send": "0.3.0",
|
|
510
511
|
"rightkit-sidecar": "0.3.0",
|
|
511
|
-
"rightkit-speech-client": "0.4.
|
|
512
|
+
"rightkit-speech-client": "0.4.1",
|
|
512
513
|
"rightkit-spend": "0.2.0",
|
|
513
514
|
"rightkit-sync": "0.2.2",
|
|
514
515
|
"rightkit-tauri-shell": "0.7.0",
|
|
@@ -642,7 +643,7 @@
|
|
|
642
643
|
"@rightkit/qa": "0.6.1",
|
|
643
644
|
"@rightkit/qa-chrome": "0.1.12",
|
|
644
645
|
"@rightkit/react-shell": "0.7.2",
|
|
645
|
-
"@rightkit/release": "0.6.
|
|
646
|
+
"@rightkit/release": "0.6.13",
|
|
646
647
|
"@rightkit/settings-ui": "0.7.0",
|
|
647
648
|
"@rightkit/shell": "0.1.1",
|
|
648
649
|
"@rightkit/tauri": "0.3.0",
|
|
@@ -891,7 +892,8 @@
|
|
|
891
892
|
"0.3.2",
|
|
892
893
|
"0.3.4",
|
|
893
894
|
"0.3.5",
|
|
894
|
-
"0.3.6"
|
|
895
|
+
"0.3.6",
|
|
896
|
+
"0.4.0"
|
|
895
897
|
],
|
|
896
898
|
"rightkit-crash": [
|
|
897
899
|
"0.1.0",
|
|
@@ -965,7 +967,8 @@
|
|
|
965
967
|
"0.2.2",
|
|
966
968
|
"0.2.3",
|
|
967
969
|
"0.2.7",
|
|
968
|
-
"0.3.0"
|
|
970
|
+
"0.3.0",
|
|
971
|
+
"0.4.0"
|
|
969
972
|
],
|
|
970
973
|
"rightkit-job-queue": [
|
|
971
974
|
"0.3.0",
|
|
@@ -1050,7 +1053,8 @@
|
|
|
1050
1053
|
"0.2.0",
|
|
1051
1054
|
"0.2.2",
|
|
1052
1055
|
"0.2.3",
|
|
1053
|
-
"0.2.4"
|
|
1056
|
+
"0.2.4",
|
|
1057
|
+
"0.2.5"
|
|
1054
1058
|
],
|
|
1055
1059
|
"rightkit-credentials": [
|
|
1056
1060
|
"0.2.0",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// GB-09: the names the gate emits at the RustRight interface. The client reads the current `RUSTRIGHT_*` spellings and, for migration only, the legacy
|
|
2
|
+
// `RIGHTKIT_*` ones (rustright-cli shims.rs LEGACY_ENV_SPELLINGS, removed by RustRight's task L-1; `RUSTRIGHT_LEGACY_ALIASES=off` makes it read none).
|
|
3
|
+
// Every outbound production call carries the current spelling only: a legacy interface name found in a call's environment is translated to its current
|
|
4
|
+
// spelling (the current one wins when both are set) and the legacy name is dropped, so no route depends on an alias the broker may stop reading. Gate-internal
|
|
5
|
+
// RIGHTKIT_* names that are not part of this interface (RIGHTKIT_MANAGED_ROOT, RIGHTKIT_QA_*, ...) are never touched.
|
|
6
|
+
export const LEGACY_INTERFACE_ENV = Object.freeze({
|
|
7
|
+
RIGHTKIT_BUILD_PRIORITY: "RUSTRIGHT_BUILD_PRIORITY", RIGHTKIT_PARENT_REQUEST_ID: "RUSTRIGHT_PARENT_REQUEST_ID", RIGHTKIT_EXCLUSIVE: "RUSTRIGHT_EXCLUSIVE",
|
|
8
|
+
RIGHTKIT_CANCEL_ON_DISCONNECT: "RUSTRIGHT_CANCEL_ON_DISCONNECT", RIGHTKIT_CALLING_GATE: "RUSTRIGHT_CALLING_GATE", RIGHTKIT_SNAPSHOT_REQUEST: "RUSTRIGHT_SNAPSHOT_REQUEST",
|
|
9
|
+
RIGHTKIT_SNAPSHOT_ADMISSION: "RUSTRIGHT_SNAPSHOT_ADMISSION", RIGHTKIT_CHECK_ADMISSION: "RUSTRIGHT_CHECK_ADMISSION", RIGHTKIT_GATE_RUN: "RUSTRIGHT_GATE_RUN",
|
|
10
|
+
RIGHTKIT_ATTEST_NONCE: "RUSTRIGHT_ATTEST_NONCE", RIGHTKIT_REQUEST_ID_FILE: "RUSTRIGHT_REQUEST_ID_FILE", RIGHTKIT_INPUT_MANIFEST: "RUSTRIGHT_INPUT_MANIFEST",
|
|
11
|
+
RIGHTKIT_OUTPUT_ROOTS: "RUSTRIGHT_OUTPUT_ROOTS", RIGHTKIT_ADAPTER_ENV: "RUSTRIGHT_ADAPTER_ENV", RIGHTKIT_FORWARD_ENV: "RUSTRIGHT_FORWARD_ENV",
|
|
12
|
+
RIGHTKIT_PARENT_CAPABILITY_TOKEN: "RUSTRIGHT_PARENT_CAPABILITY_TOKEN", RIGHTKIT_CUTOVER_WAIT_MS: "RUSTRIGHT_CUTOVER_WAIT_MS", RIGHTKIT_RECONNECT_WAIT_MS: "RUSTRIGHT_RECONNECT_WAIT_MS",
|
|
13
|
+
RIGHTKIT_RUN_ROOT: "RUSTRIGHT_RUN_ROOT", RIGHTKIT_MANAGED_AGENT_SHIM: "RUSTRIGHT_MANAGED_AGENT_SHIM",
|
|
14
|
+
RIGHTKIT_BUILD_BROKER_SOCKET: "RUSTRIGHT_BUILD_BROKER_SOCKET",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/** The legacy interface names set in `env` (case-insensitive, as Windows environments are). */
|
|
18
|
+
export const legacyInterfaceNamesIn = (env) => Object.keys(env ?? {}).filter((key) => Object.hasOwn(LEGACY_INTERFACE_ENV, key.toUpperCase()));
|
|
19
|
+
|
|
20
|
+
/** `env` with every legacy interface name translated to the current spelling (the current value wins) and removed; the same object when there is none. */
|
|
21
|
+
export function currentInterfaceEnv(env) {
|
|
22
|
+
const legacy = legacyInterfaceNamesIn(env);
|
|
23
|
+
if (legacy.length === 0) return env;
|
|
24
|
+
const out = { ...env };
|
|
25
|
+
const present = new Set(Object.keys(out).map((key) => key.toUpperCase()));
|
|
26
|
+
for (const key of legacy) {
|
|
27
|
+
const current = LEGACY_INTERFACE_ENV[key.toUpperCase()];
|
|
28
|
+
if (!present.has(current)) { out[current] = out[key]; present.add(current); }
|
|
29
|
+
delete out[key];
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// The gate's own record of the scan tools it trusts on this host (RA-18 / L9-03; the broker-pins.mjs pattern, for analyzers instead of a broker).
|
|
2
|
+
//
|
|
3
|
+
// deterministic-scans.mjs runs shellcheck, actionlint, cargo-deny and cargo-audit only from an ENROLLED executable: an absolute path, the file's
|
|
4
|
+
// sha256 and its version (`options.toolPolicy`, `{ [tool]: { path, sha256, version } }`), never a binary found on PATH. This module is where that
|
|
5
|
+
// policy comes from, and from nowhere else:
|
|
6
|
+
//
|
|
7
|
+
// record `<managed data root>/scan-tools/<platform>.json`, `{schema, platform, tools: {<tool>: {path, sha256, version}}, enrolledAt}`,
|
|
8
|
+
// outside every repository and snapshot, written only by `enrollScanTool` (an explicit operation, one tool per call; a second
|
|
9
|
+
// enrollment of the same tool is a rotation and says so). At enrol time the file named by `path` must be a regular, executable
|
|
10
|
+
// file whose sha256 IS the one enrolled: the operator's claim is checked against the bytes, nothing is computed on their behalf.
|
|
11
|
+
// fallback RightKit's own checkout only: `ci/rk-pipeline.json` `tools.<name>.<platform>`, read from the revision of the RUNNING gate
|
|
12
|
+
// (`git show <gate repository HEAD>:ci/rk-pipeline.json`, readGateRevisionPipeline), never the checked repository, the selected
|
|
13
|
+
// commit, the snapshot or the working tree. The enrolled record decides a tool it names; the fallback only fills the tools it does not.
|
|
14
|
+
//
|
|
15
|
+
// Never read here: an environment variable (apart from the managed data root's own location, which the caller resolves), a flag of the run, the
|
|
16
|
+
// checked repository's files, a receipt. A tool with no entry anywhere is not enrolled: deterministic-scans.mjs answers TOOL_UNTRUSTED.
|
|
17
|
+
// Platforms are those whose host may run the gate's class A: darwin, win32 and linux.
|
|
18
|
+
import crypto from "node:crypto";
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { readGateRevisionPipeline } from "./broker-pins.mjs";
|
|
23
|
+
import { spawnSync } from "./background-process.mjs";
|
|
24
|
+
|
|
25
|
+
export const SCAN_TOOLS_SCHEMA = "rightkit.scan-tools/1";
|
|
26
|
+
export const SCAN_TOOLS_DIR = "scan-tools";
|
|
27
|
+
/** The analyzers class A accepts only from an enrolment (deterministic-scans.mjs TRUSTED_SCAN_TOOLS; kept in step by the tests). */
|
|
28
|
+
export const SCAN_TOOL_NAMES = Object.freeze(["actionlint", "shellcheck", "cargo-deny", "cargo-audit"]);
|
|
29
|
+
export const SCAN_TOOL_PLATFORMS = Object.freeze(["darwin", "win32", "linux"]);
|
|
30
|
+
|
|
31
|
+
export class ScanToolEnrolmentError extends Error {
|
|
32
|
+
constructor(code, message) { super(message); this.name = "ScanToolEnrolmentError"; this.code = code; }
|
|
33
|
+
}
|
|
34
|
+
const fail = (code, message) => { throw new ScanToolEnrolmentError(code, message); };
|
|
35
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
36
|
+
const isLink = (meta) => meta.isSymbolicLink();
|
|
37
|
+
|
|
38
|
+
/** The record of one platform under the managed data root. */
|
|
39
|
+
export const scanToolsFile = ({ dataRoot, platform = process.platform }) => {
|
|
40
|
+
if (!dataRoot || !path.isAbsolute(dataRoot)) fail("RK_SCAN_TOOLS_ROOT", "the managed data root must be an absolute path");
|
|
41
|
+
if (!SCAN_TOOL_PLATFORMS.includes(platform)) fail("RK_SCAN_TOOLS_PLATFORM", `${platform} is not a scan-tool host (${SCAN_TOOL_PLATFORMS.join(", ")} only)`);
|
|
42
|
+
return path.join(dataRoot, SCAN_TOOLS_DIR, `${platform}.json`);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** `{path, sha256, version}` or a throw naming what is wrong: a trusted tool, an absolute path without `..`, a lowercase sha256, a version. */
|
|
46
|
+
export function validateScanTool(tool, entry, { platform = process.platform, label = "the scan-tool enrolment" } = {}) {
|
|
47
|
+
const bad = (why) => fail("RK_SCAN_TOOLS_INVALID", `${label}: ${tool}: ${why}`);
|
|
48
|
+
if (!SCAN_TOOL_NAMES.includes(tool)) fail("RK_SCAN_TOOLS_INVALID", `${label}: ${JSON.stringify(tool)} is not a trusted scan tool (${SCAN_TOOL_NAMES.join(", ")})`);
|
|
49
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) bad("not an object");
|
|
50
|
+
const { path: file, sha256, version } = entry;
|
|
51
|
+
const paths = platform === "win32" ? path.win32 : path.posix;
|
|
52
|
+
if (typeof file !== "string" || !paths.isAbsolute(file) || file.split(/[\\/]/).includes("..") || file.includes("\0")) bad("path is not an absolute path without '..'");
|
|
53
|
+
if (typeof sha256 !== "string" || !SHA256.test(sha256)) bad("sha256 is not a sha256 in lowercase hex");
|
|
54
|
+
if (typeof version !== "string" || !version.trim() || version !== version.trim()) bad("version is missing");
|
|
55
|
+
return { path: file, sha256, version };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const digestOf = (file) => {
|
|
59
|
+
const hash = crypto.createHash("sha256"); const fd = fs.openSync(file, "r");
|
|
60
|
+
try { const buffer = Buffer.alloc(1 << 20); for (let n = fs.readSync(fd, buffer, 0, buffer.length, null); n > 0; n = fs.readSync(fd, buffer, 0, buffer.length, null)) hash.update(buffer.subarray(0, n)); } finally { fs.closeSync(fd); }
|
|
61
|
+
return hash.digest("hex");
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** The enrolled record `{ platform, tools, source: "enrolled", file }`, or null when none exists. A link, a non-file, another platform's record, an unknown tool or a malformed entry is a refusal, never null. */
|
|
65
|
+
export function readEnrolledScanTools({ dataRoot, platform = process.platform }) {
|
|
66
|
+
const file = scanToolsFile({ dataRoot, platform });
|
|
67
|
+
let meta;
|
|
68
|
+
try { meta = fs.lstatSync(file); } catch (e) { if (e.code === "ENOENT") return null; throw e; }
|
|
69
|
+
if (isLink(meta) || !meta.isFile()) fail("RK_SCAN_TOOLS_INVALID", `${file} is not a regular file`);
|
|
70
|
+
const dir = fs.lstatSync(path.dirname(file));
|
|
71
|
+
if (isLink(dir) || !dir.isDirectory()) fail("RK_SCAN_TOOLS_INVALID", `${path.dirname(file)} is not a real directory`);
|
|
72
|
+
if (process.platform !== "win32" && (meta.mode & 0o022 || dir.mode & 0o022)) fail("RK_SCAN_TOOLS_INVALID", `${file} or its directory is writable by group or others`);
|
|
73
|
+
let record;
|
|
74
|
+
try { record = JSON.parse(fs.readFileSync(file, "utf8")); } catch (e) { fail("RK_SCAN_TOOLS_INVALID", `${file} is not JSON (${e.message})`); }
|
|
75
|
+
if (record?.schema !== SCAN_TOOLS_SCHEMA) fail("RK_SCAN_TOOLS_INVALID", `${file} is not a ${SCAN_TOOLS_SCHEMA} record`);
|
|
76
|
+
if (record.platform !== platform) fail("RK_SCAN_TOOLS_INVALID", `${file} is the record of ${record.platform}, not ${platform}`);
|
|
77
|
+
if (record.tools === null || typeof record.tools !== "object" || Array.isArray(record.tools)) fail("RK_SCAN_TOOLS_INVALID", `${file} has no tools object`);
|
|
78
|
+
const tools = {};
|
|
79
|
+
for (const [name, entry] of Object.entries(record.tools)) tools[name] = validateScanTool(name, entry, { platform, label: file });
|
|
80
|
+
return { platform, tools, source: "enrolled", file };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* RightKit's own scan-tool pipeline entries, from the revision of the running gate: the `tools` object of `ci/rk-pipeline.json` there (raw: the
|
|
85
|
+
* caller turns it into a policy with deterministic-scans.mjs scanToolPolicyFromPipeline). Null when the gate is not run from a RightKit checkout
|
|
86
|
+
* or the file has no `tools`.
|
|
87
|
+
*/
|
|
88
|
+
export function readGateRevisionScanTools({ gateFile, checkout, spawn = spawnSync, realpath }) {
|
|
89
|
+
const read = readGateRevisionPipeline({ gateFile, checkout, spawn, ...(realpath ? { realpath } : {}) });
|
|
90
|
+
if (!read || read.pipeline.tools === null || typeof read.pipeline.tools !== "object") return null;
|
|
91
|
+
return { tools: read.pipeline.tools, revision: read.revision };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The scan-tool policy a gate run hands runScans as `options.toolPolicy`: `{ policy, sources, enrolledFile, revision }` where `policy` is
|
|
96
|
+
* `{ [tool]: { path, sha256, version } }`, the enrolled tools over the running gate's own revision's, and `sources[tool]` says which decided
|
|
97
|
+
* (`"enrolled"` | `"rk-pipeline"`). `fromPipeline` is deterministic-scans.mjs scanToolPolicyFromPipeline (passed in so this module adds no
|
|
98
|
+
* import of the scan engine). An empty policy is a valid answer: each tool no one enrolled is then TOOL_UNTRUSTED where a scan needs it.
|
|
99
|
+
*/
|
|
100
|
+
export function loadScanToolPolicy({ dataRoot, platform = process.platform, gateFile, checkout, spawn, realpath, fromPipeline }) {
|
|
101
|
+
if (typeof fromPipeline !== "function") fail("RK_SCAN_TOOLS_INVALID", "the pipeline policy reader is required (scanToolPolicyFromPipeline)");
|
|
102
|
+
// A platform that is not a scan-tool host has no enrolment to read: nothing is trusted there (every analyzer answers TOOL_UNTRUSTED).
|
|
103
|
+
if (!SCAN_TOOL_PLATFORMS.includes(platform)) return { policy: {}, sources: {} };
|
|
104
|
+
const enrolled = readEnrolledScanTools({ dataRoot, platform });
|
|
105
|
+
const fallback = gateFile && checkout ? readGateRevisionScanTools({ gateFile, checkout, spawn, realpath }) : null;
|
|
106
|
+
const policy = {}; const sources = {};
|
|
107
|
+
if (fallback) {
|
|
108
|
+
for (const [name, entry] of Object.entries(fromPipeline(fallback.tools, platform))) { policy[name] = entry; sources[name] = "rk-pipeline"; }
|
|
109
|
+
}
|
|
110
|
+
if (enrolled) for (const [name, entry] of Object.entries(enrolled.tools)) { policy[name] = entry; sources[name] = "enrolled"; }
|
|
111
|
+
return { policy, sources, ...(enrolled ? { enrolledFile: enrolled.file } : {}), ...(fallback ? { revision: fallback.revision } : {}) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The explicit enrol operation, one tool per call. The file at `path` must exist, be a regular executable file and hash to `sha256` now
|
|
116
|
+
* (the operator's value is verified, never filled in); the record under the data root gains the tool. An already enrolled tool is replaced
|
|
117
|
+
* only with `rotate: true`. Returns `{ file, record, rotated }`.
|
|
118
|
+
*/
|
|
119
|
+
export function enrollScanTool({ dataRoot, platform = process.platform, tool, path: toolPath, sha256, version, rotate = false, now = () => new Date() }) {
|
|
120
|
+
const file = scanToolsFile({ dataRoot, platform });
|
|
121
|
+
const entry = validateScanTool(tool, { path: toolPath, sha256, version }, { platform, label: "the enrolment" });
|
|
122
|
+
// The claim is judged against the bytes it names, on the host that enrols (a record for another platform cannot be checked from here).
|
|
123
|
+
if (platform !== process.platform) fail("RK_SCAN_TOOLS_PLATFORM", `${platform} tools are enrolled on a ${platform} host (this is ${process.platform}): the file's bytes cannot be checked from here`);
|
|
124
|
+
let real; let meta;
|
|
125
|
+
try { real = fs.realpathSync.native(entry.path); meta = fs.statSync(real); } catch (e) { fail("RK_SCAN_TOOLS_INVALID", `the ${tool} at ${entry.path} cannot be resolved (${e.code ?? e.message})`); }
|
|
126
|
+
if (!meta.isFile()) fail("RK_SCAN_TOOLS_INVALID", `the ${tool} at ${real} is not a regular file`);
|
|
127
|
+
if (platform !== "win32" && (meta.mode & 0o111) === 0) fail("RK_SCAN_TOOLS_INVALID", `the ${tool} at ${real} is not executable`);
|
|
128
|
+
const actual = digestOf(real);
|
|
129
|
+
if (actual !== entry.sha256) fail("RK_SCAN_TOOLS_DIGEST", `the ${tool} at ${real} has sha256 ${actual}, not the ${entry.sha256} being enrolled; nothing was written`);
|
|
130
|
+
const dir = path.dirname(file);
|
|
131
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
132
|
+
const dirMeta = fs.lstatSync(dir);
|
|
133
|
+
if (isLink(dirMeta) || !dirMeta.isDirectory()) fail("RK_SCAN_TOOLS_INVALID", `${dir} is not a real directory`);
|
|
134
|
+
let existing = null;
|
|
135
|
+
try { existing = readEnrolledScanTools({ dataRoot, platform }); } catch (e) { if (!rotate) throw e; }
|
|
136
|
+
const had = existing?.tools[tool] ?? null;
|
|
137
|
+
if (had && !rotate) fail("RK_SCAN_TOOLS_EXISTS", `${file} already enrolls ${tool} ${had.version} (sha256 ${had.sha256.slice(0, 12)}); replacing it is a rotation (pass rotate: true)`);
|
|
138
|
+
const record = { schema: SCAN_TOOLS_SCHEMA, platform, tools: { ...(existing?.tools ?? {}), [tool]: entry }, enrolledAt: now().toISOString() };
|
|
139
|
+
const tmp = `${file}.${crypto.randomUUID()}.tmp`;
|
|
140
|
+
fs.writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
141
|
+
try { fs.renameSync(tmp, file); } finally { fs.rmSync(tmp, { force: true }); }
|
|
142
|
+
return { file, record, rotated: Boolean(had) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `node scan-tool-enrolment.mjs enroll --platform <darwin|win32|linux> --data-root <dir> --tool <name> --path P --sha256 D --version V [--rotate]` or `show --platform <p> --data-root <dir>`. */
|
|
146
|
+
export function main(argv, { out = (m) => process.stdout.write(`${m}\n`), err = (m) => process.stderr.write(`${m}\n`) } = {}) {
|
|
147
|
+
const [command, ...rest] = argv;
|
|
148
|
+
const opt = (name) => { const i = rest.indexOf(name); return i < 0 ? undefined : rest[i + 1]; };
|
|
149
|
+
try {
|
|
150
|
+
const dataRoot = opt("--data-root"); const platform = opt("--platform") ?? process.platform;
|
|
151
|
+
if (command === "enroll") {
|
|
152
|
+
const r = enrollScanTool({ dataRoot, platform, tool: opt("--tool"), path: opt("--path"), sha256: opt("--sha256"), version: opt("--version"), rotate: rest.includes("--rotate") });
|
|
153
|
+
const tool = opt("--tool");
|
|
154
|
+
out(`enrolled ${tool} ${r.record.tools[tool].version} for ${r.record.platform} (sha256 ${r.record.tools[tool].sha256}) in ${r.file}${r.rotated ? " (rotation)" : ""}`);
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
if (command === "show") {
|
|
158
|
+
const enrolled = readEnrolledScanTools({ dataRoot, platform });
|
|
159
|
+
out(enrolled ? JSON.stringify(enrolled) : "no enrollment");
|
|
160
|
+
return enrolled ? 0 : 1;
|
|
161
|
+
}
|
|
162
|
+
err(`usage: scan-tool-enrolment.mjs enroll|show --platform <darwin|win32|linux> --data-root <dir> [--tool <${SCAN_TOOL_NAMES.join("|")}> --path P --sha256 D --version V [--rotate]]`);
|
|
163
|
+
return 2;
|
|
164
|
+
} catch (e) { err(`${e.code ?? "error"}: ${e.message}`); return 1; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (process.argv[1] && (() => { try { return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })()) process.exit(main(process.argv.slice(2)));
|