@rightkit/release 0.6.5 → 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 +196 -11
- 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/android-play-upload.mjs +4 -4
- package/app-check.mjs +702 -26
- package/apple-profile-create.mjs +4 -3
- package/apple-profile-renew.mjs +5 -4
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-agent-hook.mjs +9 -0
- package/bin/rightkit-gate.mjs +671 -133
- package/binary-license-notices.mjs +2 -1
- package/build-release.mjs +20 -12
- package/cache-command.mjs +4 -3
- package/cargo-license-notices.mjs +2 -1
- package/cargo-manifest-text.mjs +25 -0
- package/cargo-profile-contract.mjs +26 -13
- package/cli/right-release.mjs +37 -7
- package/command-spec.mjs +4 -5
- 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/deps.mjs +5 -4
- package/deterministic-scans.mjs +513 -54
- package/development.mjs +5 -5
- 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-release.mjs +5 -3
- package/github-updater-release.mjs +48 -8
- package/github-upload-gate.mjs +6 -6
- package/init-command.mjs +175 -8
- package/ios-upload.mjs +4 -4
- package/legion-semantic-contracts.mjs +428 -0
- package/mirror-root-artifact.mjs +3 -4
- package/npm-license-notices.mjs +2 -1
- package/outdated-command.mjs +91 -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 +46 -3
- package/pipeline-admission.mjs +12 -0
- package/publish-cargo.mjs +219 -4
- package/publish-update.mjs +19 -14
- package/release-config.mjs +87 -11
- package/release-dependencies-command.mjs +4 -3
- package/release-qa-audit.mjs +12 -10
- package/release-qa-command.mjs +42 -7
- package/release-qa-hardening.mjs +10 -3
- package/release-qa-policy.mjs +214 -47
- package/release.mjs +4 -4
- package/rightkit-registry-resolver.mjs +5 -2
- package/rightkit-versions.json +178 -170
- 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 +7 -6
- package/stage-sidecar.mjs +437 -114
- package/suite-api.generated.mjs +96 -6
- package/suite-trust-roots.mjs +198 -0
- package/test-command.mjs +3 -2
- package/upload-release.mjs +3 -2
- package/web-build.mjs +2 -1
package/sign-updater.mjs
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, rmSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { signMacUpdaterFiles } from "./macos-signing-session.mjs";
|
|
6
6
|
import { refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
|
|
7
7
|
import { execWithUpdaterCredential } from "./protected-credentials.mjs";
|
|
8
8
|
import { resolvePnpmCli } from "./development.mjs";
|
|
9
|
+
import { UPDATER_KEY_ID_RE, updaterSignatureKeyId } from "./github-updater-release.mjs";
|
|
9
10
|
|
|
10
11
|
export const UPDATER_SIGNING_SECRET = "updater-signing-key";
|
|
11
12
|
|
|
12
13
|
export function runSignUpdater(args, {
|
|
13
14
|
env = process.env, platform = process.platform, cwd = process.cwd(),
|
|
14
15
|
execCredential = execWithUpdaterCredential, resolvePnpm = resolvePnpmCli,
|
|
15
|
-
signMac = signMacUpdaterFiles, exists = existsSync, remove = rmSync, log = console.log,
|
|
16
|
+
signMac = signMacUpdaterFiles, exists = existsSync, remove = rmSync, read = (file) => readFileSync(file, "utf8"), log = console.log,
|
|
16
17
|
} = {}) {
|
|
17
18
|
const files = [];
|
|
18
19
|
let dryRun = false;
|
|
20
|
+
let keyId;
|
|
19
21
|
for (let i = 0; i < args.length; i++) {
|
|
20
22
|
if (args[i] === "--file") {
|
|
21
23
|
if (!args[i + 1] || args[i + 1].startsWith("--")) throw new Error("--file requires an artifact path");
|
|
22
24
|
files.push(path.resolve(cwd, args[++i]));
|
|
25
|
+
} else if (args[i] === "--key-id") {
|
|
26
|
+
if (keyId !== undefined) throw new Error("--key-id may be given only once");
|
|
27
|
+
if (!UPDATER_KEY_ID_RE.test(args[i + 1] ?? "")) throw new Error("--key-id requires the 16 upper-case hex digits of the updater key id");
|
|
28
|
+
keyId = args[++i];
|
|
23
29
|
} else if (args[i] === "--key-file") throw new Error("--key-file is removed: updater keys come from the OS credential provider");
|
|
24
30
|
else if (args[i] === "--dry-run") dryRun = true;
|
|
25
31
|
else throw new Error(`unknown argument: ${args[i]}`);
|
|
@@ -38,9 +44,19 @@ export function runSignUpdater(args, {
|
|
|
38
44
|
for (const key of Object.keys(signingEnv)) {
|
|
39
45
|
if (["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"].includes(key.toUpperCase())) delete signingEnv[key];
|
|
40
46
|
}
|
|
47
|
+
// The credential store holds whichever key was last provisioned, so the signature, not the request, says what signed the file.
|
|
48
|
+
const assertSignedByExpectedKey = (file) => {
|
|
49
|
+
if (keyId === undefined) return;
|
|
50
|
+
let found = null;
|
|
51
|
+
try { found = updaterSignatureKeyId(read(`${file}.sig`)); } catch { /* an unreadable signature is refused below */ }
|
|
52
|
+
if (found !== keyId) throw new Error(`updater signature ${file}.sig was not made by updater key ${keyId} (found ${found ?? "unreadable"})`);
|
|
53
|
+
};
|
|
41
54
|
for (const file of files) {
|
|
42
55
|
log(`${dryRun ? "[dry-run] " : ""}pnpm exec tauri signer sign ${file}`);
|
|
43
|
-
if (dryRun)
|
|
56
|
+
if (dryRun) {
|
|
57
|
+
if (keyId !== undefined) log(`[dry-run] ${file}.sig must be made by updater key ${keyId}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
44
60
|
remove(`${file}.sig`, { force: true });
|
|
45
61
|
if (platform === "darwin") continue;
|
|
46
62
|
const result = execCredential(process.execPath,
|
|
@@ -51,8 +67,12 @@ export function runSignUpdater(args, {
|
|
|
51
67
|
error.exitCode = result.status ?? 1;
|
|
52
68
|
throw error;
|
|
53
69
|
}
|
|
70
|
+
assertSignedByExpectedKey(file);
|
|
71
|
+
}
|
|
72
|
+
if (!dryRun && platform === "darwin") {
|
|
73
|
+
signMac(files, { env: signingEnv, cwd });
|
|
74
|
+
for (const file of files) assertSignedByExpectedKey(file);
|
|
54
75
|
}
|
|
55
|
-
if (!dryRun && platform === "darwin") signMac(files, { env: signingEnv, cwd });
|
|
56
76
|
return { files, dryRun };
|
|
57
77
|
}
|
|
58
78
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Sealed source-closure freshness for the app check. A committed qualification record that claims "these source files, at these
|
|
2
|
+
// digests, produced me" is stale the moment one of those files changes: the claim no longer describes the tree it ships in. Legion's
|
|
3
|
+
// qualification/provider-accuracy/index.json failed exactly so, only after a full Windows build (Orthic-Labs/legion runs 40149636641
|
|
4
|
+
// job 121038474276 and 40158717069 job 121065535787: "AssertionError: source closure stale/incomplete"). This rule reads the same
|
|
5
|
+
// data before any build, as a trusted JSON adapter: no repository code, no network, no clock.
|
|
6
|
+
//
|
|
7
|
+
// source-closure SF-01 in any committed *.json, an object { sourceClosure: { sources: { "<path>": "sha256:<hex>" }, digest } }
|
|
8
|
+
// (a sealed record) where a listed source is missing from the tree, its bytes no longer hash to the recorded
|
|
9
|
+
// digest, or `digest` is not sha256 over the canonical JSON of `sources` (sorted keys, no spaces, UTF-8).
|
|
10
|
+
//
|
|
11
|
+
// What it cannot see: a source the producer would now ADD to the closure. The closure's membership is computed by the producing
|
|
12
|
+
// repository's own code, which a Class A rule never runs; that half stays with the repository's own gate after the build.
|
|
13
|
+
//
|
|
14
|
+
// A digest a producer computes over a projection instead of the raw bytes is declared in .rightkit/source-closures.json by NAME
|
|
15
|
+
// only: { "schema": 1, "normalized": [ { "file": "<sealed json>", "transform": "<identity>", "paths"?: [...] } ] }. The identities
|
|
16
|
+
// are RightKit's (APPROVED_TRANSFORMS below), never the repository's: each binds the exact source path and record families it
|
|
17
|
+
// applies to, the keys it drops, and an input schema of the producer's real semantics, so a field the schema does not know is
|
|
18
|
+
// refused rather than dropped. The rule applies the transform to the CURRENT bytes and compares with the sealed digest: a
|
|
19
|
+
// deleted source fails, a change outside the dropped keys fails, input outside the schema fails. A repository never supplies
|
|
20
|
+
// pointers; an unknown identity, or a declaration that brings its own transform, is a finding and the path is compared raw.
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
|
|
25
|
+
export const SOURCE_CLOSURE_DECLARATION = ".rightkit/source-closures.json";
|
|
26
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
27
|
+
const SKIPPED_TREE = /(^|\/)(node_modules|vendor|target)\//;
|
|
28
|
+
const MAX_BYTES = 16 * 1024 * 1024;
|
|
29
|
+
|
|
30
|
+
const sha256 = (bytes) => `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
31
|
+
const isPlain = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
|
|
33
|
+
/** JSON with every object's keys sorted, no whitespace, non-ASCII kept: Python's json.dumps(sort_keys=True, ensure_ascii=False, separators=(",", ":")). */
|
|
34
|
+
export function canonicalJson(value) {
|
|
35
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
36
|
+
if (isPlain(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A relative, forward-slash, non-escaping path; anything else is never read. */
|
|
41
|
+
export function safeRelative(value) {
|
|
42
|
+
if (typeof value !== "string" || !value || value.length > 240 || value.includes("\\") || value.includes(":") || value.startsWith("/")) return false;
|
|
43
|
+
return value.split("/").every((part) => part && part !== "." && part !== "..");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Every sealed record in a parsed JSON value, depth first: [{ pointer, record }]. */
|
|
47
|
+
export function* sealedRecords(node, pointer = "") {
|
|
48
|
+
if (Array.isArray(node)) { for (let i = 0; i < node.length; i++) yield* sealedRecords(node[i], `${pointer}/${i}`); return; }
|
|
49
|
+
if (!isPlain(node)) return;
|
|
50
|
+
const closure = node.sourceClosure;
|
|
51
|
+
if (isPlain(closure) && isPlain(closure.sources) && "digest" in closure) yield { pointer, record: node };
|
|
52
|
+
for (const [key, value] of Object.entries(node)) if (key !== "sourceClosure") yield* sealedRecords(value, `${pointer}/${key.replace(/~/g, "~0").replace(/\//g, "~1")}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Legion's provider registry, as its producers read it before digesting (Orthic-Labs/legion, scripts/ci). A provider's benchmark
|
|
56
|
+
// holds exactly these keys today (src/registry/providers.json at cfe76b4 and d27a3d4); a new key is refused, not dropped.
|
|
57
|
+
const LEGION_BENCHMARK_KEYS = Object.freeze(["status", "requiredForCleanClaim", "qualificationDigest", "qualification"]);
|
|
58
|
+
const legionRegistryProblems = (value) => {
|
|
59
|
+
if (!isPlain(value) || !Array.isArray(value.providers)) return "the registry is not an object with a providers array";
|
|
60
|
+
for (const [i, provider] of value.providers.entries()) {
|
|
61
|
+
if (!isPlain(provider)) return `providers[${i}] is not an object`;
|
|
62
|
+
if (!isPlain(provider.benchmark)) return `providers[${i}].benchmark is not an object`;
|
|
63
|
+
const unknown = Object.keys(provider.benchmark).filter((k) => !LEGION_BENCHMARK_KEYS.includes(k));
|
|
64
|
+
if (unknown.length) return `providers[${i}].benchmark has key(s) ${unknown.join(", ")} the approved projection does not know`;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The approved transform identities. `path` is the one source each applies to (null: the declaration names text paths, for the
|
|
71
|
+
* lossless line-ending identity only); `families` the record `family` values it applies to (null: any); `schema` refuses input
|
|
72
|
+
* outside the producer's semantics; `project` is the producer's projection.
|
|
73
|
+
*/
|
|
74
|
+
export const APPROVED_TRANSFORMS = Object.freeze({
|
|
75
|
+
// validate-compatibility-report-accuracy.py:104-111, audit-jvm-sdk-accuracy.py:84-92, audit-reasoning-runtime-registration.py:301-308:
|
|
76
|
+
// each pops providers[].benchmark whole and digests the canonical JSON of the rest.
|
|
77
|
+
"legion-provider-registry/without-benchmark/v1": {
|
|
78
|
+
path: "src/registry/providers.json", families: ["compatibility-report", "code-jvm-sdk", "reasoning-runtime"], json: true,
|
|
79
|
+
schema: legionRegistryProblems,
|
|
80
|
+
project: (v) => ({ ...v, providers: v.providers.map(({ benchmark: _dropped, ...rest }) => rest) }),
|
|
81
|
+
},
|
|
82
|
+
// audit-mobile-native-enrollment.py:74-89 (registry_semantics, used by audit-mobile-sdk-accuracy.py:10-13) and
|
|
83
|
+
// audit-supply-chain-native-accuracy.py:68-73,94-96 (semantics): each drops benchmark status, qualificationDigest and qualification
|
|
84
|
+
// and keeps requiredForCleanClaim.
|
|
85
|
+
"legion-provider-registry/without-qualification-metadata/v1": {
|
|
86
|
+
path: "src/registry/providers.json", families: ["code-mobile-sdk", "supply-chain-native"], json: true,
|
|
87
|
+
schema: legionRegistryProblems,
|
|
88
|
+
project: (v) => ({ ...v, providers: v.providers.map((p) => {
|
|
89
|
+
const { status: _s, qualificationDigest: _d, qualification: _q, ...benchmark } = p.benchmark;
|
|
90
|
+
return { ...p, benchmark };
|
|
91
|
+
}) }),
|
|
92
|
+
},
|
|
93
|
+
// CRLF -> LF only: it drops no field, so the declaration names the text paths.
|
|
94
|
+
"text-lf/v1": { path: null, families: null, json: false },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// The canonical JSON form must equal Python's; a non-integer number would print differently, so it is refused.
|
|
98
|
+
const numbersExact = (v) => Array.isArray(v) ? v.every(numbersExact) : isPlain(v) ? Object.values(v).every(numbersExact) : typeof v !== "number" || Number.isSafeInteger(v);
|
|
99
|
+
|
|
100
|
+
/** The digest of `bytes` under an approved identity: { digest } or { problem } when the input is outside the identity's schema. */
|
|
101
|
+
export function transformedDigest(bytes, identity) {
|
|
102
|
+
const transform = APPROVED_TRANSFORMS[identity];
|
|
103
|
+
if (!transform) return { problem: `transform "${identity}" is not an approved RightKit identity` };
|
|
104
|
+
if (!transform.json) return { digest: sha256(Buffer.from(bytes.toString("latin1").replace(/\r\n/g, "\n"), "latin1")) };
|
|
105
|
+
let value;
|
|
106
|
+
try { value = JSON.parse(bytes.toString("utf8")); } catch (error) { return { problem: `is not JSON (${error.message})` }; }
|
|
107
|
+
const problem = transform.schema(value) ?? (numbersExact(value) ? null : "holds a non-integer number, whose canonical form is not exact");
|
|
108
|
+
if (problem) return { problem: `${problem}; ${identity} refuses it` };
|
|
109
|
+
return { digest: sha256(Buffer.from(canonicalJson(transform.project(value)), "utf8")) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Parse and validate the declaration. Returns { normalized: [...], problems: [string] }. */
|
|
113
|
+
export function readDeclaration(text) {
|
|
114
|
+
if (text === null) return { normalized: [], problems: [] };
|
|
115
|
+
let doc;
|
|
116
|
+
try { doc = JSON.parse(text); } catch (error) { return { normalized: [], problems: [`is not valid JSON (${error.message})`] }; }
|
|
117
|
+
const problems = [];
|
|
118
|
+
if (!isPlain(doc) || (doc.schema !== undefined && doc.schema !== 1) || !Array.isArray(doc.normalized ?? [])) return { normalized: [], problems: ['expected { "schema": 1, "normalized": [...] }'] };
|
|
119
|
+
const normalized = [];
|
|
120
|
+
(doc.normalized ?? []).forEach((item, index) => {
|
|
121
|
+
const bad = [];
|
|
122
|
+
if (!isPlain(item)) bad.push("is not an object");
|
|
123
|
+
else {
|
|
124
|
+
if (!safeRelative(item.file)) bad.push("file must be a safe relative path");
|
|
125
|
+
const approved = typeof item.transform === "string" ? APPROVED_TRANSFORMS[item.transform] : undefined;
|
|
126
|
+
if (!approved) bad.push(`transform must name an approved RightKit identity (${Object.keys(APPROVED_TRANSFORMS).join(", ")}); a repository never supplies its own transform or pointers`);
|
|
127
|
+
else if (approved.path !== null && (item.paths !== undefined || item.where !== undefined)) bad.push(`${item.transform} binds its own source and families; drop paths and where`);
|
|
128
|
+
else if (approved.path === null && (!Array.isArray(item.paths) || !item.paths.length || !item.paths.every(safeRelative))) bad.push(`${item.transform} needs paths: a non-empty list of safe relative paths`);
|
|
129
|
+
const unknownKeys = isPlain(item) ? Object.keys(item).filter((k) => !["file", "transform", "paths", "where"].includes(k)) : [];
|
|
130
|
+
if (unknownKeys.length) bad.push(`unknown key(s) ${unknownKeys.join(", ")}`);
|
|
131
|
+
}
|
|
132
|
+
if (bad.length) problems.push(`normalized #${index}: ${bad.join("; ")}`);
|
|
133
|
+
else normalized.push(item);
|
|
134
|
+
});
|
|
135
|
+
return { normalized, problems };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const labelOf = (record, pointer) => {
|
|
139
|
+
for (const key of ["provider", "id", "name"]) if (typeof record[key] === "string" && record[key]) return `${record[key]} (${pointer || "/"})`;
|
|
140
|
+
return pointer || "/";
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** The source-closure rule. */
|
|
144
|
+
export function sourceClosure({ root, files }) {
|
|
145
|
+
const tracked = new Set(files ?? []);
|
|
146
|
+
const out = [];
|
|
147
|
+
const declarationText = tracked.has(SOURCE_CLOSURE_DECLARATION) ? fs.readFileSync(path.join(root, SOURCE_CLOSURE_DECLARATION), "utf8") : null;
|
|
148
|
+
const declaration = readDeclaration(declarationText);
|
|
149
|
+
for (const problem of declaration.problems) out.push({ file: SOURCE_CLOSURE_DECLARATION, subject: "declaration", problem: `${SOURCE_CLOSURE_DECLARATION} ${problem}`, fix: "repair the declaration; an unreadable exemption exempts nothing" });
|
|
150
|
+
const hashes = new Map();
|
|
151
|
+
// { digest } | { problem } | null (not in the tree), per source and identity.
|
|
152
|
+
const hashOf = (rel, identity = null) => {
|
|
153
|
+
const key = `${rel}\0${identity ?? ""}`;
|
|
154
|
+
if (!hashes.has(key)) {
|
|
155
|
+
const bytes = tracked.has(rel) ? fs.readFileSync(path.join(root, rel)) : null;
|
|
156
|
+
hashes.set(key, bytes === null ? null : identity ? transformedDigest(bytes, identity) : { digest: sha256(bytes) });
|
|
157
|
+
}
|
|
158
|
+
return hashes.get(key);
|
|
159
|
+
};
|
|
160
|
+
for (const file of [...tracked].filter((f) => f.endsWith(".json") && !SKIPPED_TREE.test(f)).sort()) {
|
|
161
|
+
const full = path.join(root, file);
|
|
162
|
+
let stat;
|
|
163
|
+
try { stat = fs.statSync(full); } catch { continue; }
|
|
164
|
+
if (!stat.isFile() || stat.size > MAX_BYTES) continue;
|
|
165
|
+
const text = fs.readFileSync(full, "utf8");
|
|
166
|
+
if (!text.includes('"sourceClosure"')) continue;
|
|
167
|
+
let doc;
|
|
168
|
+
try { doc = JSON.parse(text); } catch { continue; } // json-parse rules own syntax
|
|
169
|
+
for (const { pointer, record } of sealedRecords(doc)) {
|
|
170
|
+
const label = labelOf(record, pointer);
|
|
171
|
+
const { sources, digest } = record.sourceClosure;
|
|
172
|
+
const transforms = new Map();
|
|
173
|
+
for (const n of declaration.normalized.filter((d) => d.file === file)) {
|
|
174
|
+
const approved = APPROVED_TRANSFORMS[n.transform];
|
|
175
|
+
if (approved.path === null) { for (const p of n.paths) transforms.set(p, n.transform); continue; }
|
|
176
|
+
if (approved.families.includes(record.family)) transforms.set(approved.path, n.transform);
|
|
177
|
+
}
|
|
178
|
+
const problems = [];
|
|
179
|
+
const malformed = Object.entries(sources).filter(([rel, value]) => !safeRelative(rel) || !DIGEST.test(value)).map(([rel]) => rel);
|
|
180
|
+
if (malformed.length) problems.push(`unsafe path or malformed digest: ${malformed.slice(0, 5).join(", ")}`);
|
|
181
|
+
const missing = [], stale = [], refused = [];
|
|
182
|
+
for (const [rel, value] of Object.entries(sources)) {
|
|
183
|
+
if (malformed.includes(rel)) continue;
|
|
184
|
+
const identity = transforms.get(rel) ?? null;
|
|
185
|
+
const actual = hashOf(rel, identity);
|
|
186
|
+
if (actual === null) missing.push(rel);
|
|
187
|
+
else if (actual.problem) refused.push(`${rel} ${actual.problem}`);
|
|
188
|
+
else if (actual.digest !== value) stale.push(identity ? `${rel} (under ${identity})` : rel);
|
|
189
|
+
}
|
|
190
|
+
if (refused.length) problems.push(`${refused.length} source(s) outside their approved transform: ${refused.slice(0, 3).join("; ")}`);
|
|
191
|
+
if (missing.length) problems.push(`${missing.length} source(s) no longer in the tree: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", ..." : ""}`);
|
|
192
|
+
if (stale.length) problems.push(`${stale.length} source(s) changed since the record was sealed: ${stale.slice(0, 5).join(", ")}${stale.length > 5 ? ", ..." : ""}`);
|
|
193
|
+
if (digest !== sha256(Buffer.from(canonicalJson(sources), "utf8"))) problems.push("sourceClosure.digest is not sha256 over the canonical JSON of sourceClosure.sources");
|
|
194
|
+
if (problems.length) out.push({
|
|
195
|
+
file, subject: label,
|
|
196
|
+
problem: `${file} ${label}: sealed source closure stale/incomplete: ${problems.join("; ")}`,
|
|
197
|
+
fix: "re-run the qualification that produced this record on the exact source and commit its output (never rewrite recorded digests by hand); or revert the source change",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const SOURCE_CLOSURE_RULES = Object.freeze([{ id: "source-closure", gap: "SF-01", run: sourceClosure }]);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `right-release stage-sidecar [--config right-release.config.
|
|
2
|
+
// `right-release stage-sidecar [--config right-release.config.json] [--target <triple>] [--check] [--json]` (bogusyogi/rightkit#345):
|
|
3
3
|
// stages the config's declarative `sidecars` into src-tauri/binaries with Tauri externalBin naming. `--check` validates
|
|
4
4
|
// the manifest and prints the plan without building, downloading, copying or deleting anything (for template gates).
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
7
8
|
import { planSidecarStaging, resolveTargetTriple, stageSidecars } from "./stage-sidecar.mjs";
|
|
8
9
|
|
|
9
|
-
export function parseStageSidecarArgs(argv) {
|
|
10
|
-
const opts = { config:
|
|
10
|
+
export function parseStageSidecarArgs(argv, cwd = process.cwd()) {
|
|
11
|
+
const opts = { config: defaultReleaseConfigFile(cwd), target: undefined, json: false, check: false };
|
|
11
12
|
for (let i = 0; i < argv.length; i += 1) {
|
|
12
13
|
const arg = argv[i];
|
|
13
14
|
if (arg === "--config" || arg === "--target") {
|
|
@@ -49,8 +50,7 @@ async function main() {
|
|
|
49
50
|
const opts = parseStageSidecarArgs(process.argv.slice(2));
|
|
50
51
|
const configPath = path.resolve(opts.config);
|
|
51
52
|
const appRoot = path.dirname(configPath);
|
|
52
|
-
const
|
|
53
|
-
const config = loaded.default ?? loaded.config ?? loaded;
|
|
53
|
+
const config = await importReleaseConfig(configPath, { lenient: true });
|
|
54
54
|
if (config?.sidecars === undefined) throw new Error(`${opts.config} declares no sidecars`);
|
|
55
55
|
if (opts.check) {
|
|
56
56
|
const target = opts.target ?? process.env.TAURI_ENV_TARGET_TRIPLE ?? nodeHostTriple();
|
|
@@ -60,7 +60,8 @@ async function main() {
|
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
const target = resolveTargetTriple({ target: opts.target, cwd: appRoot });
|
|
63
|
-
|
|
63
|
+
// Warnings (a never-run row, a catalog row that is not staged) go to stderr; stdout stays the staged-file list or the --json document.
|
|
64
|
+
const result = await stageSidecars({ appRoot, manifest: config.sidecars, target, deps: { warn: (line) => console.error(line) } });
|
|
64
65
|
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
65
66
|
else {
|
|
66
67
|
for (const file of result.files ?? []) console.log(`right-release stage-sidecar: staged ${typeof file === "string" ? file : file.path ?? JSON.stringify(file)}`);
|