@rightkit/release 0.2.164 → 0.2.172
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 +121 -4
- package/addon-command.mjs +6 -13
- package/android-aab-scan.mjs +265 -0
- package/android-dev.mjs +111 -0
- package/android-ndk.mjs +77 -0
- package/android-play-upload.mjs +262 -0
- package/android-release.mjs +344 -0
- package/android-test-fixtures.mjs +79 -0
- package/apple-profile-create.mjs +363 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/build-release.mjs +73 -13
- package/cargo-guard.mjs +2 -0
- package/cargo-profile-contract.mjs +183 -0
- package/cargo-target.mjs +8 -0
- package/cli/right-release.mjs +14 -4
- package/committed-source.mjs +1 -1
- package/dev-artifact-refresh.mjs +176 -0
- package/dev-artifact-support.mjs +61 -0
- package/development-signing.mjs +71 -4
- package/development.mjs +78 -46
- package/direct-bootstrap.mjs +26 -41
- package/disk-command.mjs +315 -13
- package/disk-policy.json +7 -5
- package/disk-policy.mjs +141 -22
- package/disk-stable-root.mjs +19 -16
- package/engine-locator.mjs +116 -0
- package/external-bin.mjs +133 -0
- package/github-release.mjs +2 -2
- package/github-updater-release.mjs +154 -0
- package/hardeningscan.mjs +1 -1
- package/in-place-install.mjs +56 -0
- package/install-app-windows.mjs +63 -0
- package/install-app.mjs +53 -23
- package/local-publish.mjs +11 -1
- package/mac-signing-matrix.mjs +23 -11
- package/managed-cargo.mjs +6 -0
- package/media-sidecar-catalog.mjs +45 -0
- package/media-sidecar.mjs +306 -0
- package/ownership-approval-command.mjs +10 -44
- package/ownership-approval.mjs +3 -42
- package/ownership-catalog-config.json +2 -1
- package/ownership-catalog.mjs +4 -2
- package/ownership-command.mjs +7 -9
- package/ownership-dependency-allowlist.json +25 -1
- package/ownership-evidence.mjs +8 -5
- package/ownership-git-inventory.mjs +41 -7
- package/ownership-lockfiles.mjs +91 -8
- package/ownership-mandatory-set.json +36 -16
- package/ownership-report.mjs +5 -6
- package/ownership-scan-worker.mjs +2 -2
- package/ownership-verdict.mjs +10 -7
- package/ownership.mjs +47 -29
- package/package.json +7 -4
- package/portable-development.mjs +32 -115
- package/preflight.mjs +4 -6
- package/publication-artifacts.mjs +34 -0
- package/publish-update.mjs +72 -11
- package/qa-adoption-exemptions.json +5 -0
- package/qa-build-features.mjs +7 -0
- package/qa-contract.mjs +182 -0
- package/qa-control-scan.mjs +3 -3
- package/release-qa-command.mjs +38 -12
- package/release-qa-installed.mjs +136 -0
- package/release-qa-receipt.mjs +23 -2
- package/release.mjs +283 -14
- package/rightkit-ownership.json +537 -5
- package/rightkit-sync.mjs +26 -0
- package/rightkit-versions.json +110 -62
- package/runtime-budget-classes.json +28 -0
- package/runtime-budgets.mjs +4 -1
- package/sign-candidate.mjs +161 -0
- package/sign-dev-artifact.mjs +198 -0
- package/stage-binary.mjs +60 -0
- package/swap-helper-bundle.mjs +105 -0
- package/templates/cargo-profiles.toml +28 -0
- package/upload-release.mjs +11 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Find the rightkit-ffmpeg crate (its catalog and notice files) for a release.
|
|
2
|
+
//
|
|
3
|
+
// Order: `mediaSidecar.catalog` in the release config (the crate's catalog/artifacts.json; the crate directory is the
|
|
4
|
+
// catalog's grandparent), otherwise the crate Cargo resolves for the app's native manifest (`cargo metadata --offline`,
|
|
5
|
+
// so the catalog is exactly the one the app was compiled against, at the version in its Cargo.lock).
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { runCargoSync } from "./managed-cargo.mjs";
|
|
9
|
+
|
|
10
|
+
const invalid = (code, message) => Object.assign(new Error(message), { code });
|
|
11
|
+
|
|
12
|
+
export function cargoFfmpegCrate(manifestPath, { run = runCargoSync } = {}) {
|
|
13
|
+
const result = run(["metadata", "--offline", "--format-version", "1", "--manifest-path", manifestPath], {
|
|
14
|
+
cwd: path.dirname(manifestPath),
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
});
|
|
19
|
+
if (result.error) throw invalid("MEDIA_SIDECAR_CRATE_NOT_FOUND", `could not run cargo metadata: ${result.error.message}`);
|
|
20
|
+
if (result.status !== 0) throw invalid("MEDIA_SIDECAR_CRATE_NOT_FOUND", `cargo metadata failed: ${String(result.stderr ?? "").trim()}`);
|
|
21
|
+
const metadata = JSON.parse(result.stdout);
|
|
22
|
+
const crates = metadata.packages.filter((pkg) => pkg.name === "rightkit-ffmpeg");
|
|
23
|
+
if (crates.length !== 1) throw invalid("MEDIA_SIDECAR_CRATE_NOT_FOUND", `the app's dependency graph has ${crates.length} rightkit-ffmpeg packages (expected exactly one)`);
|
|
24
|
+
return { crateDir: path.dirname(crates[0].manifest_path), crateVersion: crates[0].version };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Returns { catalog, catalogBytes, crateDir, crateVersion }. Injectable: read, crateFor. */
|
|
28
|
+
export function loadSidecarCatalog({ config, root, manifestPath, read = readFileSync, crateFor = cargoFfmpegCrate }) {
|
|
29
|
+
let crateDir;
|
|
30
|
+
let crateVersion;
|
|
31
|
+
let catalogPath;
|
|
32
|
+
if (config.catalog) {
|
|
33
|
+
catalogPath = path.resolve(root, config.catalog);
|
|
34
|
+
crateDir = path.dirname(path.dirname(catalogPath));
|
|
35
|
+
} else {
|
|
36
|
+
if (!manifestPath) throw invalid("MEDIA_SIDECAR_CRATE_NOT_FOUND", "no mediaSidecar.catalog and no native Cargo manifest to resolve rightkit-ffmpeg from");
|
|
37
|
+
({ crateDir, crateVersion } = crateFor(manifestPath));
|
|
38
|
+
catalogPath = path.join(crateDir, "catalog", "artifacts.json");
|
|
39
|
+
}
|
|
40
|
+
let catalogBytes;
|
|
41
|
+
try { catalogBytes = read(catalogPath); } catch (error) { throw invalid("MEDIA_SIDECAR_CATALOG_INVALID", `cannot read ${catalogPath}: ${error.message}`); }
|
|
42
|
+
let catalog;
|
|
43
|
+
try { catalog = JSON.parse(String(catalogBytes)); } catch { throw invalid("MEDIA_SIDECAR_CATALOG_INVALID", `${catalogPath} is not JSON`); }
|
|
44
|
+
return { catalog, catalogBytes, crateDir, crateVersion };
|
|
45
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Bundle the rightkit-ffmpeg decode sidecar (ffmpeg + ffprobe) into a desktop app and seal its identity.
|
|
2
|
+
//
|
|
3
|
+
// targets.mac.mediaSidecar = {
|
|
4
|
+
// arch: "aarch64", // catalog arch (default: build host); one arch per build
|
|
5
|
+
// variant: "decode", // catalog variant (default "decode")
|
|
6
|
+
// tools: ["ffmpeg", "ffprobe"], // default both
|
|
7
|
+
// stageDir: "src-tauri/binaries", // Tauri externalBin directory (default)
|
|
8
|
+
// bundleDir: "Contents/MacOS", // mac only: where Tauri puts externalBin (or "Contents/Resources")
|
|
9
|
+
// notices: { dir: "src-tauri/resources/third-party/ffmpeg", sourceOfferContact: "https://..." },
|
|
10
|
+
// catalog: "path/to/artifacts.json", // optional; default: the rightkit-ffmpeg crate Cargo resolves
|
|
11
|
+
// stage: { cmd: "cargo", args: [...] }, // optional: runs rightkit_ffmpeg::stage_external_bin
|
|
12
|
+
// }
|
|
13
|
+
//
|
|
14
|
+
// This module owns the config shape, catalog row selection, the notice set, the pre-sign checks and the receipt.
|
|
15
|
+
// It never signs and never hashes the catalog's content digest (the crate's stage_external_bin does that when it
|
|
16
|
+
// copies the binary in); release.mjs signs the files with the app and build-release.mjs seals the receipt.
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, copyFileSync } from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
|
|
21
|
+
export const MEDIA_SIDECAR_CONTRACT = "rightkit-ffmpeg-sidecar-v1";
|
|
22
|
+
export const MEDIA_SIDECAR_RECEIPT_NAME = "media-sidecar.json";
|
|
23
|
+
export const MEDIA_SIDECAR_OFFER_NAME = "FFMPEG-SOURCE-OFFER.txt";
|
|
24
|
+
export const MEDIA_SIDECAR_TOOLS = Object.freeze(["ffmpeg", "ffprobe"]);
|
|
25
|
+
|
|
26
|
+
const PLATFORM_OS = Object.freeze({ mac: "macos", win: "windows", linux: "linux" });
|
|
27
|
+
const ARCHES = new Set(["aarch64", "x86_64"]);
|
|
28
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
29
|
+
const CONFIG_KEYS = new Set(["arch", "variant", "tools", "stageDir", "bundleDir", "notices", "catalog", "stage"]);
|
|
30
|
+
const BUNDLE_DIRS = new Set(["Contents/MacOS", "Contents/Resources"]);
|
|
31
|
+
|
|
32
|
+
const invalid = (code, message) => Object.assign(new Error(message), { code });
|
|
33
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
34
|
+
const sha256Bytes = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
35
|
+
const sha256File = (file) => sha256Bytes(readFileSync(file));
|
|
36
|
+
|
|
37
|
+
/** Tauri target triple for a catalog os/arch pair (same table as rightkit-ffmpeg `tauri_target_triple`). */
|
|
38
|
+
export function sidecarTargetTriple(os, arch) {
|
|
39
|
+
const table = {
|
|
40
|
+
"macos/aarch64": "aarch64-apple-darwin",
|
|
41
|
+
"macos/x86_64": "x86_64-apple-darwin",
|
|
42
|
+
"windows/x86_64": "x86_64-pc-windows-msvc",
|
|
43
|
+
"windows/aarch64": "aarch64-pc-windows-msvc",
|
|
44
|
+
"linux/x86_64": "x86_64-unknown-linux-gnu",
|
|
45
|
+
"linux/aarch64": "aarch64-unknown-linux-gnu",
|
|
46
|
+
};
|
|
47
|
+
const triple = table[`${os}/${arch}`];
|
|
48
|
+
if (!triple) throw invalid("MEDIA_SIDECAR_PLATFORM", `no Tauri target triple for ${os}/${arch}`);
|
|
49
|
+
return triple;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `ffmpeg-<triple>[.exe]`: the file Tauri resolves for `externalBin: ["binaries/ffmpeg"]`. */
|
|
53
|
+
export function stagedFileName(tool, os, arch) {
|
|
54
|
+
return `${tool}-${sidecarTargetTriple(os, arch)}${os === "windows" ? ".exe" : ""}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The name Tauri gives the binary inside the bundle (no triple). */
|
|
58
|
+
export function bundledFileName(tool, os) {
|
|
59
|
+
return `${tool}${os === "windows" ? ".exe" : ""}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function hostCatalogArch(nodeArch = process.arch) {
|
|
63
|
+
return nodeArch === "arm64" ? "aarch64" : nodeArch === "x64" ? "x86_64" : nodeArch;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validate and normalise `targets.<platform>.mediaSidecar`. Every problem is reported at once.
|
|
68
|
+
* Returns { os, arch, variant, tools, stageDir, bundleDir, notices: { dir, sourceOfferContact }, catalog?, stage? }.
|
|
69
|
+
*/
|
|
70
|
+
export function validateMediaSidecarConfig(raw, { platform, hostArch = hostCatalogArch() } = {}) {
|
|
71
|
+
const errors = [];
|
|
72
|
+
const os = PLATFORM_OS[platform];
|
|
73
|
+
if (!os) throw invalid("MEDIA_SIDECAR_CONFIG_INVALID", `mediaSidecar is not supported on platform ${JSON.stringify(platform)} (mac, win or linux)`);
|
|
74
|
+
if (!isObject(raw)) throw invalid("MEDIA_SIDECAR_CONFIG_INVALID", "mediaSidecar must be an object");
|
|
75
|
+
for (const key of Object.keys(raw)) if (!CONFIG_KEYS.has(key)) errors.push(`unknown key ${JSON.stringify(key)} (allowed: ${[...CONFIG_KEYS].join(", ")})`);
|
|
76
|
+
const relativePath = (value, label) => {
|
|
77
|
+
if (typeof value !== "string" || !value.trim()) { errors.push(`${label} must be a non-empty relative path`); return undefined; }
|
|
78
|
+
const normalized = value.replaceAll("\\", "/");
|
|
79
|
+
if (path.isAbsolute(value) || /^[A-Za-z]:/.test(value) || normalized.split("/").includes("..")) { errors.push(`${label} must be relative and stay inside the app: ${value}`); return undefined; }
|
|
80
|
+
return normalized.replace(/\/+$/, "");
|
|
81
|
+
};
|
|
82
|
+
const arch = raw.arch ?? hostArch;
|
|
83
|
+
if (!ARCHES.has(arch)) errors.push(`arch must be one of ${[...ARCHES].join(", ")} (one architecture per build): ${JSON.stringify(arch)}`);
|
|
84
|
+
const variant = raw.variant ?? "decode";
|
|
85
|
+
if (typeof variant !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(variant)) errors.push(`variant must be a catalog variant slug: ${JSON.stringify(variant)}`);
|
|
86
|
+
const tools = raw.tools ?? [...MEDIA_SIDECAR_TOOLS];
|
|
87
|
+
if (!Array.isArray(tools) || !tools.length || new Set(tools).size !== tools.length || tools.some((tool) => !MEDIA_SIDECAR_TOOLS.includes(tool))) {
|
|
88
|
+
errors.push(`tools must be a non-empty list without duplicates drawn from ${MEDIA_SIDECAR_TOOLS.join(", ")}`);
|
|
89
|
+
}
|
|
90
|
+
const stageDir = relativePath(raw.stageDir ?? "src-tauri/binaries", "stageDir");
|
|
91
|
+
let bundleDir;
|
|
92
|
+
if (os === "macos") {
|
|
93
|
+
bundleDir = raw.bundleDir ?? "Contents/MacOS";
|
|
94
|
+
if (!BUNDLE_DIRS.has(bundleDir)) errors.push(`bundleDir must be ${[...BUNDLE_DIRS].join(" or ")}`);
|
|
95
|
+
} else if (raw.bundleDir !== undefined) errors.push("bundleDir applies to macOS only");
|
|
96
|
+
let notices;
|
|
97
|
+
if (!isObject(raw.notices)) errors.push("notices must be { dir, sourceOfferContact }: the LGPL text and source offer must ship with the app");
|
|
98
|
+
else {
|
|
99
|
+
for (const key of Object.keys(raw.notices)) if (!["dir", "sourceOfferContact"].includes(key)) errors.push(`notices: unknown key ${JSON.stringify(key)}`);
|
|
100
|
+
const dir = relativePath(raw.notices.dir, "notices.dir");
|
|
101
|
+
const contact = raw.notices.sourceOfferContact;
|
|
102
|
+
if (typeof contact !== "string" || !/^(https:\/\/\S+|[^\s@]+@[^\s@]+\.[^\s@]+)$/.test(contact)) errors.push("notices.sourceOfferContact must be an https URL or an email address where the source can be requested");
|
|
103
|
+
if (dir) notices = { dir, sourceOfferContact: contact };
|
|
104
|
+
}
|
|
105
|
+
const catalog = raw.catalog === undefined ? undefined : relativePath(raw.catalog, "catalog");
|
|
106
|
+
let stage;
|
|
107
|
+
if (raw.stage !== undefined) {
|
|
108
|
+
if (!isObject(raw.stage) || typeof raw.stage.cmd !== "string" || !raw.stage.cmd || (raw.stage.args !== undefined && !Array.isArray(raw.stage.args))) errors.push("stage must be { cmd, args? }");
|
|
109
|
+
else stage = raw.stage;
|
|
110
|
+
}
|
|
111
|
+
if (errors.length) throw invalid("MEDIA_SIDECAR_CONFIG_INVALID", `invalid mediaSidecar configuration:\n${errors.map((line) => `- ${line}`).join("\n")}`);
|
|
112
|
+
return { os, arch, variant, tools: [...tools], stageDir, ...(bundleDir ? { bundleDir } : {}), notices, ...(catalog ? { catalog } : {}), ...(stage ? { stage } : {}) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** `sign.nestedCode` entries for the macOS bundle: the sidecar executables, hardened runtime, no entitlements. */
|
|
116
|
+
export function mediaSidecarNestedCode(config) {
|
|
117
|
+
if (config.os !== "macos") return {};
|
|
118
|
+
return Object.fromEntries(config.tools.map((tool) => [`${config.bundleDir}/${bundledFileName(tool, "macos")}`, { hardenedRuntime: true }]));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Select the one approved catalog row per tool for this variant/os/arch. A missing row, a pending or stale row, or an
|
|
123
|
+
* approved row without a content digest refuses the release: Linux has no approved row yet, so a Linux plan fails
|
|
124
|
+
* here with MEDIA_SIDECAR_NO_ROW instead of shipping an unverified binary.
|
|
125
|
+
*/
|
|
126
|
+
export function selectSidecarRows(catalog, config) {
|
|
127
|
+
if (!isObject(catalog) || !Array.isArray(catalog.artifacts)) throw invalid("MEDIA_SIDECAR_CATALOG_INVALID", "catalog has no artifacts array");
|
|
128
|
+
return config.tools.map((tool) => {
|
|
129
|
+
const matches = catalog.artifacts.filter((row) => row.tool === tool && (row.variant ?? "decode") === config.variant
|
|
130
|
+
&& String(row.os).toLowerCase() === config.os && String(row.arch).toLowerCase() === config.arch);
|
|
131
|
+
const where = `${tool} ${config.variant} ${config.os}/${config.arch}`;
|
|
132
|
+
if (matches.length === 0) throw invalid("MEDIA_SIDECAR_NO_ROW", `the rightkit-ffmpeg catalog has no ${where} row`);
|
|
133
|
+
if (matches.length > 1) throw invalid("MEDIA_SIDECAR_AMBIGUOUS_ROW", `the rightkit-ffmpeg catalog has ${matches.length} ${where} rows`);
|
|
134
|
+
const [row] = matches;
|
|
135
|
+
if (row.status !== "approved") throw invalid("MEDIA_SIDECAR_NOT_APPROVED", `the ${where} row ${row.id} is ${row.status}, not approved`);
|
|
136
|
+
if (!SHA256.test(row.contentSha256 ?? "")) throw invalid("MEDIA_SIDECAR_NOT_APPROVED", `the ${where} row ${row.id} is approved but has no content digest`);
|
|
137
|
+
if (!Array.isArray(row.attribution?.noticeFiles) || !row.attribution.noticeFiles.length || typeof row.attribution.sourceUrl !== "string") {
|
|
138
|
+
throw invalid("MEDIA_SIDECAR_NOTICE_MISSING", `the ${where} row ${row.id} lacks notice files or a source URL`);
|
|
139
|
+
}
|
|
140
|
+
return row;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Plan the sidecar for one build. `catalog` is the parsed artifacts.json, `crateDir` the rightkit-ffmpeg crate
|
|
146
|
+
* directory (notice files are relative to it), `root` the app root.
|
|
147
|
+
*/
|
|
148
|
+
export function planMediaSidecar({ config, catalog, crateDir, crateVersion, catalogBytes, root }) {
|
|
149
|
+
const rows = selectSidecarRows(catalog, config);
|
|
150
|
+
const files = rows.map((row) => ({
|
|
151
|
+
tool: row.tool,
|
|
152
|
+
artifactId: row.id,
|
|
153
|
+
version: row.version,
|
|
154
|
+
contentSha256: row.contentSha256,
|
|
155
|
+
stagedPath: path.resolve(root, config.stageDir, stagedFileName(row.tool, config.os, config.arch)),
|
|
156
|
+
bundledName: bundledFileName(row.tool, config.os),
|
|
157
|
+
...(config.bundleDir ? { bundlePath: `${config.bundleDir}/${bundledFileName(row.tool, config.os)}` } : {}),
|
|
158
|
+
}));
|
|
159
|
+
const noticeSources = new Map();
|
|
160
|
+
for (const row of rows) for (const notice of row.attribution.noticeFiles) {
|
|
161
|
+
const normalized = String(notice).replaceAll("\\", "/");
|
|
162
|
+
if (path.isAbsolute(notice) || normalized.split("/").includes("..")) throw invalid("MEDIA_SIDECAR_NOTICE_MISSING", `notice file path escapes the crate: ${notice}`);
|
|
163
|
+
noticeSources.set(normalized, path.join(crateDir, ...normalized.split("/")));
|
|
164
|
+
}
|
|
165
|
+
const noticeDir = path.resolve(root, config.notices.dir);
|
|
166
|
+
const notices = [...noticeSources].map(([relative, source]) => ({ source, dest: path.join(noticeDir, path.basename(relative)) }));
|
|
167
|
+
const names = notices.map((item) => path.basename(item.dest));
|
|
168
|
+
if (new Set(names).size !== names.length) throw invalid("MEDIA_SIDECAR_NOTICE_MISSING", "two notice files share a basename");
|
|
169
|
+
return {
|
|
170
|
+
config,
|
|
171
|
+
rows,
|
|
172
|
+
files,
|
|
173
|
+
notices,
|
|
174
|
+
offerPath: path.join(noticeDir, MEDIA_SIDECAR_OFFER_NAME),
|
|
175
|
+
offer: sourceOfferText({ rows, contact: config.notices.sourceOfferContact, crateVersion }),
|
|
176
|
+
catalog: { crateVersion: crateVersion ?? null, schemaVersion: catalog.schemaVersion ?? null, ...(catalogBytes ? { sha256: sha256Bytes(catalogBytes) } : {}) },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The written LGPL source offer: where the exact FFmpeg source, the build recipe and the licence text are. */
|
|
181
|
+
export function sourceOfferText({ rows, contact, crateVersion }) {
|
|
182
|
+
const lines = [
|
|
183
|
+
"FFmpeg source offer",
|
|
184
|
+
"",
|
|
185
|
+
"This application includes unmodified FFmpeg programs (ffmpeg and ffprobe) built from the FFmpeg source below. FFmpeg is licensed under the",
|
|
186
|
+
"GNU Lesser General Public License version 2.1 or later (LGPL-2.1-or-later); the build is configured with --disable-gpl and --disable-nonfree.",
|
|
187
|
+
"The licence text and build notes are in this folder. You may replace these programs with your own build of the same source.",
|
|
188
|
+
"",
|
|
189
|
+
`Source code and build recipe: ${rows[0].attribution.sourceUrl}`,
|
|
190
|
+
`Build recipes, pinned sources and notes: https://github.com/Orthic-Labs/rightkit/tree/main/crates/rightkit-ffmpeg${crateVersion ? ` (crate rightkit-ffmpeg ${crateVersion}, published on crates.io)` : ""}`,
|
|
191
|
+
`To request the corresponding source on physical media, or if a link is unavailable, write to: ${contact}`,
|
|
192
|
+
"",
|
|
193
|
+
"Programs shipped",
|
|
194
|
+
];
|
|
195
|
+
for (const row of rows) {
|
|
196
|
+
lines.push(`- ${row.tool} ${row.version} (${row.id})`);
|
|
197
|
+
lines.push(` licence: ${row.attribution.licenseSpdx}`);
|
|
198
|
+
lines.push(` source: ${row.attribution.sourceUrl}`);
|
|
199
|
+
lines.push(` source archive sha256: ${row.sourceSha256}`);
|
|
200
|
+
lines.push(` build notes: ${row.attribution.buildProvenance}`);
|
|
201
|
+
for (const component of row.attribution.components ?? []) lines.push(` component ${component.name} ${component.version}: ${component.licenseSpdx} (${component.sourceUrl})`);
|
|
202
|
+
}
|
|
203
|
+
return `${lines.join("\n")}\n`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Copy the catalog's notice files and write the source offer into the app's notice directory. Idempotent. */
|
|
207
|
+
export function writeMediaSidecarNotices(plan, { exists = existsSync, mkdir = mkdirSync, copy = copyFileSync, write = writeFileSync } = {}) {
|
|
208
|
+
for (const { source } of plan.notices) {
|
|
209
|
+
if (!exists(source)) throw invalid("MEDIA_SIDECAR_NOTICE_MISSING", `notice file not found in the rightkit-ffmpeg crate: ${source}`);
|
|
210
|
+
}
|
|
211
|
+
mkdir(path.dirname(plan.offerPath), { recursive: true });
|
|
212
|
+
for (const { source, dest } of plan.notices) copy(source, dest);
|
|
213
|
+
write(plan.offerPath, plan.offer);
|
|
214
|
+
return [...plan.notices.map((item) => item.dest), plan.offerPath];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Before signing: every staged binary exists, is a non-empty file, and its hash is recorded. */
|
|
218
|
+
export function verifyStagedSidecars(plan, { exists = existsSync, stat = (file) => statSync(file), hashFile = sha256File } = {}) {
|
|
219
|
+
return plan.files.map((file) => {
|
|
220
|
+
if (!exists(file.stagedPath)) throw invalid("MEDIA_SIDECAR_NOT_STAGED", `${file.tool} is not staged: expected ${file.stagedPath} (rightkit_ffmpeg::stage_external_bin writes it; configure mediaSidecar.stage or stage it in prePackage)`);
|
|
221
|
+
const info = stat(file.stagedPath);
|
|
222
|
+
if (!info.isFile() || info.size <= 0) throw invalid("MEDIA_SIDECAR_NOT_STAGED", `${file.stagedPath} is not a non-empty file`);
|
|
223
|
+
return { ...file, preSignSha256: hashFile(file.stagedPath), preSignSizeBytes: info.size };
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** After signing: the receipt that build-release.mjs seals into the release manifest. `signed` maps tool to { sha256, sizeBytes }. */
|
|
228
|
+
export function buildMediaSidecarReceipt(plan, preSign, signed, { notices = [], hashFile = sha256File, signing } = {}) {
|
|
229
|
+
const config = plan.config;
|
|
230
|
+
return {
|
|
231
|
+
schema: 1,
|
|
232
|
+
contract: MEDIA_SIDECAR_CONTRACT,
|
|
233
|
+
variant: config.variant,
|
|
234
|
+
os: config.os,
|
|
235
|
+
arch: config.arch,
|
|
236
|
+
signing: signing ?? (config.os === "macos" ? "developer-id-hardened-runtime" : config.os === "windows" ? "authenticode" : "none"),
|
|
237
|
+
catalog: plan.catalog,
|
|
238
|
+
files: preSign.map((file) => {
|
|
239
|
+
const after = signed[file.tool];
|
|
240
|
+
if (!after || !SHA256.test(after.sha256 ?? "")) throw invalid("MEDIA_SIDECAR_RECEIPT_INVALID", `no signed digest for ${file.tool}`);
|
|
241
|
+
return {
|
|
242
|
+
tool: file.tool,
|
|
243
|
+
artifactId: file.artifactId,
|
|
244
|
+
version: file.version,
|
|
245
|
+
catalogContentSha256: file.contentSha256,
|
|
246
|
+
preSignSha256: file.preSignSha256,
|
|
247
|
+
signedSha256: after.sha256,
|
|
248
|
+
sizeBytes: after.sizeBytes,
|
|
249
|
+
...(file.bundlePath ? { bundlePath: file.bundlePath } : { bundledName: file.bundledName }),
|
|
250
|
+
};
|
|
251
|
+
}),
|
|
252
|
+
notices: [...plan.notices.map((item) => item.dest), plan.offerPath].map((file) => ({ name: path.basename(file), sha256: hashFile(file) })),
|
|
253
|
+
sourceOfferContact: config.notices.sourceOfferContact,
|
|
254
|
+
...(notices.length ? { extra: notices } : {}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Structural check used by the seal and by publication. `expected` optionally pins variant, os and tools. */
|
|
259
|
+
export function validateMediaSidecarReceipt(receipt, expected = {}) {
|
|
260
|
+
const fail = (message) => { throw invalid("MEDIA_SIDECAR_RECEIPT_INVALID", `media sidecar receipt: ${message}`); };
|
|
261
|
+
if (!isObject(receipt) || receipt.schema !== 1 || receipt.contract !== MEDIA_SIDECAR_CONTRACT) fail("unrecognized contract");
|
|
262
|
+
if (!Array.isArray(receipt.files) || !receipt.files.length) fail("no files");
|
|
263
|
+
for (const file of receipt.files) {
|
|
264
|
+
if (!MEDIA_SIDECAR_TOOLS.includes(file.tool) || typeof file.artifactId !== "string" || !file.artifactId) fail(`bad file entry ${JSON.stringify(file.tool)}`);
|
|
265
|
+
for (const key of ["catalogContentSha256", "preSignSha256", "signedSha256"]) if (!SHA256.test(file[key] ?? "")) fail(`${file.tool}.${key} must be a SHA-256`);
|
|
266
|
+
if (!Number.isSafeInteger(file.sizeBytes) || file.sizeBytes <= 0) fail(`${file.tool}.sizeBytes must be positive`);
|
|
267
|
+
if (receipt.signing !== "none" && file.preSignSha256 === file.signedSha256) fail(`${file.tool} was not changed by signing`);
|
|
268
|
+
}
|
|
269
|
+
if (new Set(receipt.files.map((file) => file.tool)).size !== receipt.files.length) fail("duplicate tool");
|
|
270
|
+
if (!["developer-id-hardened-runtime", "authenticode", "none"].includes(receipt.signing)) fail("unknown signing mode");
|
|
271
|
+
if (!Array.isArray(receipt.notices) || !receipt.notices.some((item) => item.name === MEDIA_SIDECAR_OFFER_NAME) || receipt.notices.some((item) => !SHA256.test(item.sha256 ?? ""))) fail("notices must include the source offer with digests");
|
|
272
|
+
if (!receipt.notices.some((item) => /LGPL/i.test(item.name))) fail("notices must include the LGPL text");
|
|
273
|
+
if (expected.variant && receipt.variant !== expected.variant) fail(`variant ${receipt.variant} is not ${expected.variant}`);
|
|
274
|
+
if (expected.os && receipt.os !== expected.os) fail(`os ${receipt.os} is not ${expected.os}`);
|
|
275
|
+
if (expected.tools && JSON.stringify([...expected.tools].sort()) !== JSON.stringify(receipt.files.map((file) => file.tool).sort())) fail("tools differ from the configuration");
|
|
276
|
+
return receipt;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** macOS: the nested signing receipt must show every sidecar signed (strict-valid) with the digest now on disk. */
|
|
280
|
+
export function macSidecarSigningProblems(nestedReceipt, signedFiles) {
|
|
281
|
+
const problems = [];
|
|
282
|
+
const byFile = new Map((nestedReceipt?.files ?? []).map((item) => [path.resolve(item.file), item]));
|
|
283
|
+
for (const { file, sha256 } of signedFiles) {
|
|
284
|
+
const item = byFile.get(path.resolve(file));
|
|
285
|
+
if (!item) problems.push(`${file}: not in the nested signing receipt`);
|
|
286
|
+
else if (item.codesign !== "strict-valid") problems.push(`${file}: codesign is ${item.codesign ?? "missing"}, not strict-valid`);
|
|
287
|
+
else if (item.after?.sha256 !== sha256) problems.push(`${file}: bytes changed after signing`);
|
|
288
|
+
}
|
|
289
|
+
return problems;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function sidecarFileEvidence(file, { hashFile = sha256File, stat = (item) => statSync(item) } = {}) {
|
|
293
|
+
return { sha256: hashFile(file), sizeBytes: stat(file).size };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Windows: each sidecar's signed digest must be one the raw-EXE signing receipt recorded as Valid after signing. */
|
|
297
|
+
export function windowsSidecarSigningProblems(rawReceipt, receipt) {
|
|
298
|
+
const problems = [];
|
|
299
|
+
const signed = new Map((rawReceipt?.files ?? []).map((item) => [item.after?.sha256, item]));
|
|
300
|
+
for (const file of receipt.files) {
|
|
301
|
+
const item = signed.get(file.signedSha256);
|
|
302
|
+
if (!item) problems.push(`${file.tool}: signed digest ${file.signedSha256} is not in the raw-EXE signing receipt`);
|
|
303
|
+
else if (item.authenticode !== "Valid" || item.timestampPresent !== true) problems.push(`${file.tool}: Authenticode evidence is not Valid with a timestamp`);
|
|
304
|
+
}
|
|
305
|
+
return problems;
|
|
306
|
+
}
|
|
@@ -1,50 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
|
|
2
|
+
// `right-release ownership-approval` is retired (Orthic-Labs/rightkit#262), under the proportional-trust rule, like trust-sync.
|
|
3
|
+
// Ownership exceptions are plain entries in the repository's .rightkit/ownership-exceptions.json, reviewed through git:
|
|
4
|
+
// no drafts, signatures, Touch ID or native helper. The command is a no-op that says so and always succeeds.
|
|
5
|
+
import { realpathSync } from "node:fs";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { createDraft, loadTrustedExternal, policyEntryDigest, validateRecord, verifyException } from "./ownership-approval.mjs";
|
|
7
|
-
import { SCANNER_VERSION } from "./ownership.mjs";
|
|
8
|
-
import manifest from "./rightkit-ownership.json" with { type: "json" };
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const duration = Date.parse(draft.payload.expires) - Date.parse(draft.payload.issued);
|
|
16
|
-
if (!(duration > 0 && duration <= 90 * 86400000)) problems.push("draft expiry must be after issued and within 90 days");
|
|
17
|
-
if (problems.length) throw new Error(problems.join("; "));
|
|
18
|
-
return draft;
|
|
19
|
-
}
|
|
20
|
-
export function main(args) {
|
|
21
|
-
if (args.includes("--draft")) {
|
|
22
|
-
const input = json(arg(args, "--draft"));
|
|
23
|
-
const draft = createInstalledDraft(input);
|
|
24
|
-
const out = arg(args, "--out");
|
|
25
|
-
const data = `${JSON.stringify(draft, null, 2)}\n`;
|
|
26
|
-
if (out) writeFileSync(path.resolve(out), data, { flag: "wx" }); else process.stdout.write(data);
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
29
|
-
if (args.includes("--import")) {
|
|
30
|
-
const record = json(arg(args, "--import"));
|
|
31
|
-
const out = arg(args, "--out");
|
|
32
|
-
if (!out) throw new Error("--import requires --out");
|
|
33
|
-
writeFileSync(path.resolve(out), `${JSON.stringify(record, null, 2)}\n`, { flag: "wx" });
|
|
34
|
-
return 0;
|
|
35
|
-
}
|
|
36
|
-
if (args.includes("--verify")) {
|
|
37
|
-
const record = json(arg(args, "--verify"));
|
|
38
|
-
const context = json(arg(args, "--context"));
|
|
39
|
-
const loaded = loadTrustedExternal();
|
|
40
|
-
const result = verifyException(record, { ...context, trust: loaded.trust });
|
|
41
|
-
process.stdout.write(`${JSON.stringify({ ...result, pending: loaded.pending }, null, 2)}\n`);
|
|
42
|
-
return result.ok ? 0 : 1;
|
|
43
|
-
}
|
|
44
|
-
throw new Error("usage: node ownership-approval-command.mjs --draft input.json [--out draft.json] | --import signed.json --out record.json | --verify signed.json --context scan.json");
|
|
9
|
+
export const OWNERSHIP_APPROVAL_RETIRED = "right-release ownership-approval is retired: signed ownership exceptions (draft, sign, Touch ID and native-helper paths) no longer exist. Edit .rightkit/ownership-exceptions.json directly, with plain entries {capability, package, reason, owner, date}, and review the change through git. `right-release ownership` marks a matching finding excepted and warns about stale entries.";
|
|
10
|
+
|
|
11
|
+
export function main(_args = [], { print = (line) => process.stdout.write(`${line}\n`) } = {}) {
|
|
12
|
+
print(`[right-release] ${OWNERSHIP_APPROVAL_RETIRED}`);
|
|
13
|
+
return 0;
|
|
45
14
|
}
|
|
46
15
|
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
47
|
-
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url)))
|
|
48
|
-
try { process.exitCode = main(process.argv.slice(2)); }
|
|
49
|
-
catch (error) { console.error(`[right-release] ownership approval: ${error.message}`); process.exitCode = 1; }
|
|
50
|
-
}
|
|
16
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) process.exitCode = main(process.argv.slice(2));
|
package/ownership-approval.mjs
CHANGED
|
@@ -110,7 +110,7 @@ export function occurrenceHash(finding) {
|
|
|
110
110
|
return sha256({ v: 2, kind, fingerprint: finding.fingerprint, path: finding.file, ecosystem, name: finding.name, alias: finding.alias, section: finding.section, requirement: finding.requirement,
|
|
111
111
|
features: [...new Set(finding.features)].sort(), defaultFeatures: finding.defaultFeatures, source: finding.source, resolvedVersion: finding.resolvedVersion });
|
|
112
112
|
}
|
|
113
|
-
// Schema-v2 envelopes retain source/dependency kinds; lock routes are dependency occurrences.
|
|
113
|
+
// Schema-v2 draft envelopes retain source/dependency kinds; lock routes are dependency occurrences.
|
|
114
114
|
const exceptionOccurrenceKind = (f) => f.kind === "transitive-dependency" ? "dependency" : f.kind ?? (f.ecosystem ? "dependency" : "source");
|
|
115
115
|
function nullableString(value) { return value === null || typeof value === "string" && value.length > 0; }
|
|
116
116
|
function validFingerprint(value) { return typeof value === "string" && /^[a-z0-9][a-z0-9._-]{1,127}$/.test(value) && !HEX.test(value); }
|
|
@@ -415,44 +415,5 @@ function signatureProblems(record, trust) {
|
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const problems = validateRecord(record);
|
|
421
|
-
if (problems.length) return { ok: false, problems };
|
|
422
|
-
const r = record.payload;
|
|
423
|
-
if (!validDate(today)) return { ok: false, problems: ["invalid verification date"] };
|
|
424
|
-
if (r.issued > today) problems.push("approval issued date is in the future");
|
|
425
|
-
const duration = Date.parse(`${r.expires}T00:00:00Z`) - Date.parse(`${r.issued}T00:00:00Z`);
|
|
426
|
-
if (duration <= 0 || duration > 90 * 86400000) problems.push("approval expiry must be after issued and within 90 days");
|
|
427
|
-
if (r.expires < today) problems.push(`approval expired ${r.expires}`);
|
|
428
|
-
if (problems.length) return { ok: false, problems };
|
|
429
|
-
if (!versionAtLeast(scannerVersion, r.minimumScannerVersion)) problems.push("scanner version missing, malformed, or below approval minimum");
|
|
430
|
-
if (!versionAtLeast(catalogVersion, r.minimumCatalogVersion)) problems.push("catalog version missing, malformed, or below approval minimum");
|
|
431
|
-
if (!trust) problems.push("pending: external ownership trust is not enrolled");
|
|
432
|
-
else problems.push(...validateOwnershipFloor(trust, { scannerVersion, catalogVersion, catalogDigest }).problems);
|
|
433
|
-
if (problems.length) return { ok: false, problems };
|
|
434
|
-
if (typeof policyDigest !== "string" || !HEX.test(policyDigest) || r.policyDigest !== policyDigest) return { ok: false, problems: ["ownership fingerprint policy digest missing or changed"] };
|
|
435
|
-
if (!Array.isArray(findings)) return { ok: false, problems: ["current findings missing or malformed"] };
|
|
436
|
-
let current;
|
|
437
|
-
try { current = findings.filter((f) => f.fingerprint === r.fingerprint).map((f) => ({ path: f.file, kind: exceptionOccurrenceKind(f), hash: occurrenceHash(f) })); }
|
|
438
|
-
catch (error) { return { ok: false, problems: [error.message] }; }
|
|
439
|
-
for (const item of r.occurrences) if (!current.some((f) => f.path === item.path && f.hash === item.hash && f.kind === item.kind)) problems.push(`baseline occurrence changed or missing: ${item.path} (${item.hash})`);
|
|
440
|
-
if (problems.length) return { ok: false, problems };
|
|
441
|
-
const trackingEvidence = sha256({ repositoryIdentity: r.repositoryIdentity, requestId: r.requestId, trackingRef: r.trackingRef });
|
|
442
|
-
if (!trust.verifiedTrackingEvidence?.includes(trackingEvidence)) problems.push("tracking reference lacks protected verification evidence");
|
|
443
|
-
let recordIdentity, currentIdentity;
|
|
444
|
-
try { recordIdentity = canonicalRepositoryIdentity(r.repositoryIdentity); currentIdentity = canonicalRepositoryIdentity(repositoryIdentity); }
|
|
445
|
-
catch { problems.push("invalid repository identity"); }
|
|
446
|
-
if (recordIdentity && recordIdentity !== r.repositoryIdentity) problems.push("approval repository identity is not canonical");
|
|
447
|
-
if (recordIdentity && currentIdentity && recordIdentity !== currentIdentity) problems.push("repository identity changed");
|
|
448
|
-
const registration = verifyRepositoryRegistration(trust, r.repositoryIdentity, { files: (findings ?? []).map((f) => f.file) });
|
|
449
|
-
problems.push(...registration.problems);
|
|
450
|
-
if (record.signature === null) problems.push("unsigned approval record");
|
|
451
|
-
else if (!problems.length) problems.push(...signatureProblems(record, trust));
|
|
452
|
-
return { ok: problems.length === 0, problems };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
export function signPayloadForNative(record, { keyId, host, signature }) {
|
|
456
|
-
if (!record?.payload || !signature) throw new Error("native signer must return a signature over displayed approval payload");
|
|
457
|
-
return { schema: APPROVAL_SCHEMA, payload: record.payload, signature: { keyId, host, value: signature } };
|
|
458
|
-
}
|
|
418
|
+
// Exception records are plain, git-reviewed entries (Orthic-Labs/rightkit#262); verifyException and native signing are retired.
|
|
419
|
+
// signatureProblems stays only as the enrolled-key signature check used by key-log tests.
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
{ "capability": "text-to-speech", "packages": ["rightkit-tts-qwen", "rightkit-tts-qwen-sys", "rightkit-tts"], "reason": "Speech facade & its QTS inference, safe-wrapper & FFI layers" },
|
|
9
9
|
{ "capability": "os-credential-store", "packages": ["rightkit-credentials", "RightKitSwift"], "reason": "Cross-platform Rust & Apple-native Swift credential APIs" },
|
|
10
10
|
{ "capability": "desktop-shell", "packages": ["rightkit-tauri-shell", "@rightkit/tauri-shell"], "reason": "Native Tauri shell & typed renderer bridge" },
|
|
11
|
+
{ "capability": "lifecycle-hooks", "packages": ["rightkit-hooks", "@rightkit/hooks"], "reason": "Native Claude Code/Codex hook runtime & its Node pair share one lifecycle-hooks contract" },
|
|
11
12
|
{ "capability": "tauri-host-adapters", "packages": ["rightkit-device", "@rightkit/tauri"], "reason": "Native license/device services & renderer command adapters" }
|
|
12
13
|
],
|
|
13
14
|
"testOwners": [
|
|
14
|
-
{ "path": "crates/rightkit-qa
|
|
15
|
+
{ "path": "crates/rightkit-qa/examples/control-test-app/Cargo.toml", "package": "control-test-app", "reason": "Unpublished desktop-control test fixture" },
|
|
15
16
|
{ "path": "crates/rightkit-tauri-shell/example-app/Cargo.toml", "package": "shell-example-app", "reason": "Unpublished standalone shell demonstration & test host" }
|
|
16
17
|
]
|
|
17
18
|
}
|
package/ownership-catalog.mjs
CHANGED
|
@@ -56,13 +56,15 @@ export function generateOwnershipCatalog(root) {
|
|
|
56
56
|
if (f.type === "dependency") {
|
|
57
57
|
if (!["cargo", "npm", "swiftpm"].includes(f.ecosystem) || !f.name || /^(@rightkit\/|rightkit-)/.test(f.name)) throw new Error(`ownership: imprecise dependency: ${f.id}`);
|
|
58
58
|
} else if (f.type === "source") {
|
|
59
|
-
|
|
59
|
+
// codeOnly: false is explicit opt-in for rules that must see string literals (auth URLs, #279).
|
|
60
|
+
if (typeof f.codeOnly !== "boolean" || !f.extensions?.length) throw new Error(`ownership: source fingerprint must be code & extension scoped: ${f.id}`);
|
|
60
61
|
new RegExp(f.pattern);
|
|
61
62
|
} else throw new Error(`ownership: unknown fingerprint type: ${f.id}`);
|
|
62
63
|
// A banned dependency blocks regardless of owner qualification: the ban stands even
|
|
63
64
|
// when no replacement is used. It names the authority that imposed it.
|
|
64
65
|
if (f.tier === "block" && f.policy === "banned") {
|
|
65
|
-
|
|
66
|
+
// A banned source rule (e.g. in-app webview sign-in, #279) is allowed: the verdict blocks on policy "banned" for any type.
|
|
67
|
+
if (!["dependency", "source"].includes(f.type) || !f.policyAuthority) throw new Error(`ownership: banned fingerprint needs a dependency or source rule & policyAuthority: ${f.id}`);
|
|
66
68
|
} else if (f.tier === "block") {
|
|
67
69
|
const published = ownershipAvailability(owner, versions).find((row) => row.state !== "staged-unverified");
|
|
68
70
|
const q = f.qualification;
|
package/ownership-command.mjs
CHANGED
|
@@ -101,28 +101,27 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
|
|
|
101
101
|
const result = checked.result;
|
|
102
102
|
if (args.includes("--audit-json")) return printAuditJson(result, print);
|
|
103
103
|
// A refusal names every blocking check on stderr and in the JSON `blocked` field. Advise-tier findings never refuse.
|
|
104
|
-
const blocked = checked.
|
|
104
|
+
const blocked = checked.ok ? undefined : blockingChecks(checked);
|
|
105
105
|
if (blocked) reportBlocked(blocked, stderr);
|
|
106
106
|
if (args.includes("--json")) {
|
|
107
|
-
const error =
|
|
107
|
+
const error = !checked.floor.ok
|
|
108
108
|
? { type: "ownership-floor", message: checked.floor.problems.join("; ") }
|
|
109
109
|
: result.coverage.length
|
|
110
110
|
? { type: "ownership-coverage", message: `${result.coverage.length} ownership coverage gap(s)` }
|
|
111
111
|
: undefined;
|
|
112
|
-
print(JSON.stringify({ ok: checked.ok, ...(checked.enrollmentPending ? { enrollmentPending: true, warning: checked.warning } : {}), ...(error ? { error } : {}), ...(blocked ? { blocked } : {}), rows: result.rows, violations: result.violations, covered: result.covered, problems: [...result.problems, ...checked.floor.problems], ...(checked.warnings ? { warnings: checked.warnings } : {}), coverage: result.coverage, metadata: result.metadata }, null, 2));
|
|
112
|
+
print(JSON.stringify({ ok: checked.ok, ...(checked.enrollmentPending ? { enrollmentPending: true, warning: checked.warning } : {}), ...(error ? { error } : {}), ...(blocked ? { blocked } : {}), rows: result.rows, violations: result.violations, covered: result.covered, problems: [...result.problems, ...checked.floor.problems], ...((checked.warnings?.length || result.warnings?.length) ? { warnings: [...(checked.warnings ?? []), ...(result.warnings ?? [])] } : {}), coverage: result.coverage, metadata: result.metadata }, null, 2));
|
|
113
113
|
} else {
|
|
114
114
|
print(`RightKit ownership: ${path.basename(app)}`);
|
|
115
115
|
print(formatTable(result.rows));
|
|
116
116
|
for (const v of result.violations) {
|
|
117
117
|
print(` OWN COPY ${v.fingerprint} (${v.package}) ${v.file}${v.line ? `:${v.line}` : ""} ${v.text}`);
|
|
118
118
|
}
|
|
119
|
-
for (const p of result.problems) print(`
|
|
119
|
+
for (const p of result.problems) print(` EXCEPTION ${p}`);
|
|
120
120
|
print(verdictLine(checked, result, blocked));
|
|
121
|
-
for (const warning of checked.warnings ?? []) print(` WARNING ${warning}`);
|
|
121
|
+
for (const warning of [...(checked.warnings ?? []), ...(result.warnings ?? [])]) print(` WARNING ${warning}`);
|
|
122
|
+
if (checked.enrollmentPending && checked.warning) print(` ${checked.warning}`);
|
|
122
123
|
}
|
|
123
|
-
//
|
|
124
|
-
// nothing, so findings print as diagnostics and the step exits 0. Release lanes never pass it.
|
|
125
|
-
if (checked.enrollmentPending && args.includes("--advisory-when-pending")) return 0;
|
|
124
|
+
// `--advisory-when-pending` is accepted and ignored: pending approval enrollment is a warning and never changes the exit code (#262).
|
|
126
125
|
return checked.ok ? 0 : 1;
|
|
127
126
|
}
|
|
128
127
|
|
|
@@ -193,7 +192,6 @@ function tierCounts(violations) {
|
|
|
193
192
|
|
|
194
193
|
// The human verdict line. Advise-only results pass and say so; a refusal names its checks.
|
|
195
194
|
function verdictLine(checked, result, blocked) {
|
|
196
|
-
if (checked.enrollmentPending) return `[right-release] ${checked.warning}`;
|
|
197
195
|
if (checked.ok) {
|
|
198
196
|
const counts = tierCounts(result.violations ?? []);
|
|
199
197
|
return counts.advise
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"global": [
|
|
4
|
+
"cargo:blake3",
|
|
5
|
+
"cargo:clap",
|
|
6
|
+
"cargo:getrandom",
|
|
4
7
|
"npm:@types/node",
|
|
5
8
|
"npm:typescript",
|
|
6
9
|
"npm:vitest"
|
|
@@ -786,7 +789,28 @@
|
|
|
786
789
|
},
|
|
787
790
|
{
|
|
788
791
|
"repositoryIdentity": "https://github.com/bogusyogi/postright",
|
|
789
|
-
"dependencies": [
|
|
792
|
+
"dependencies": [
|
|
793
|
+
"cargo:jiff",
|
|
794
|
+
"cargo:serde_json",
|
|
795
|
+
"cargo:tauri",
|
|
796
|
+
"cargo:tauri-build",
|
|
797
|
+
"cargo:tauri-plugin-os",
|
|
798
|
+
"cargo:tauri-plugin-process",
|
|
799
|
+
"cargo:tauri-plugin-updater",
|
|
800
|
+
"npm:@tailwindcss/vite",
|
|
801
|
+
"npm:@tauri-apps/api",
|
|
802
|
+
"npm:@tauri-apps/cli",
|
|
803
|
+
"npm:@tauri-apps/plugin-os",
|
|
804
|
+
"npm:@tauri-apps/plugin-process",
|
|
805
|
+
"npm:@tauri-apps/plugin-updater",
|
|
806
|
+
"npm:@types/react",
|
|
807
|
+
"npm:@types/react-dom",
|
|
808
|
+
"npm:@vitejs/plugin-react",
|
|
809
|
+
"npm:react",
|
|
810
|
+
"npm:react-dom",
|
|
811
|
+
"npm:tailwindcss",
|
|
812
|
+
"npm:vite"
|
|
813
|
+
]
|
|
790
814
|
},
|
|
791
815
|
{
|
|
792
816
|
"repositoryIdentity": "https://github.com/orthic-labs/cockpit",
|