@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.
Files changed (73) hide show
  1. package/README.md +196 -11
  2. package/admission-files.mjs +3 -2
  3. package/advisory-db.json +4 -0
  4. package/advisory-db.mjs +124 -0
  5. package/advisory-exceptions.mjs +13 -10
  6. package/android-play-upload.mjs +4 -4
  7. package/app-check.mjs +702 -26
  8. package/apple-profile-create.mjs +4 -3
  9. package/apple-profile-renew.mjs +5 -4
  10. package/bin/darwin/rightkit-swap +0 -0
  11. package/bin/rightkit-agent-hook.mjs +9 -0
  12. package/bin/rightkit-gate.mjs +671 -133
  13. package/binary-license-notices.mjs +2 -1
  14. package/build-release.mjs +20 -12
  15. package/cache-command.mjs +4 -3
  16. package/cargo-license-notices.mjs +2 -1
  17. package/cargo-manifest-text.mjs +25 -0
  18. package/cargo-profile-contract.mjs +26 -13
  19. package/cli/right-release.mjs +37 -7
  20. package/command-spec.mjs +4 -5
  21. package/compile-inputs.mjs +66 -0
  22. package/consumer-contracts.json +1044 -0
  23. package/consumer-contracts.mjs +627 -0
  24. package/consumer-input-contracts.mjs +422 -0
  25. package/consumer-policy.mjs +588 -0
  26. package/consumer-source-contracts.mjs +469 -0
  27. package/deny-default.toml +42 -0
  28. package/deps.mjs +5 -4
  29. package/deterministic-scans.mjs +513 -54
  30. package/development.mjs +5 -5
  31. package/disk-command.mjs +270 -30
  32. package/disk-policy.json +50 -10
  33. package/disk-policy.mjs +482 -35
  34. package/disk-report.mjs +129 -0
  35. package/door-policy.mjs +228 -0
  36. package/gate-executor.mjs +30 -0
  37. package/gate-stages.mjs +110 -0
  38. package/github-release.mjs +5 -3
  39. package/github-updater-release.mjs +48 -8
  40. package/github-upload-gate.mjs +6 -6
  41. package/init-command.mjs +175 -8
  42. package/ios-upload.mjs +4 -4
  43. package/legion-semantic-contracts.mjs +428 -0
  44. package/mirror-root-artifact.mjs +3 -4
  45. package/npm-license-notices.mjs +2 -1
  46. package/outdated-command.mjs +91 -0
  47. package/ownership-class-rules.mjs +277 -0
  48. package/ownership-key-authority.mjs +3 -1
  49. package/ownership-mandatory-set.mjs +3 -2
  50. package/package.json +46 -3
  51. package/pipeline-admission.mjs +12 -0
  52. package/publish-cargo.mjs +219 -4
  53. package/publish-update.mjs +19 -14
  54. package/release-config.mjs +87 -11
  55. package/release-dependencies-command.mjs +4 -3
  56. package/release-qa-audit.mjs +12 -10
  57. package/release-qa-command.mjs +42 -7
  58. package/release-qa-hardening.mjs +10 -3
  59. package/release-qa-policy.mjs +214 -47
  60. package/release.mjs +4 -4
  61. package/rightkit-registry-resolver.mjs +5 -2
  62. package/rightkit-versions.json +178 -170
  63. package/shell-contract-bridge.json +251 -0
  64. package/shell-contract.mjs +1069 -0
  65. package/sign-updater.mjs +24 -4
  66. package/source-closure-rules.mjs +204 -0
  67. package/stage-sidecar-command.mjs +7 -6
  68. package/stage-sidecar.mjs +437 -114
  69. package/suite-api.generated.mjs +96 -6
  70. package/suite-trust-roots.mjs +198 -0
  71. package/test-command.mjs +3 -2
  72. package/upload-release.mjs +3 -2
  73. package/web-build.mjs +2 -1
@@ -0,0 +1,277 @@
1
+ // Ownership class rules for the app check (plan items 0.5 and 0.6). Code is placed by class, never by consumer count:
2
+ // foundation a row of ownership-mandatory-set.json for the app's profile: the app must use its owner;
3
+ // add-on any other catalogued capability: RightKit-owned from its first consumer;
4
+ // app-specific product domain logic and presentation only.
5
+ // Non-product code stays in an app only under an approved, expiring entry of .rightkit/ownership-exceptions.json that names the
6
+ // RightKit issue that moves it. These two rules run wherever the app check runs (`rightkit-gate check`, `right-release check`, the
7
+ // pre-push hook, the start of every checked right-release command and RightKit's own preflight):
8
+ //
9
+ // mandatory-foundation OW-05 an app whose profile requires a foundation owner the app does not declare as a dependency (missing,
10
+ // or dev-only), or that selects an unknown or unratified profile. An app is a repository whose root
11
+ // holds its declaration, rightkit-mandatory.json, or that ships a desktop app: a root
12
+ // right-release.config.(mjs|json), or one beside a src-tauri/tauri.conf.json; when that directory
13
+ // holds its own declaration (RightKit's desktop-app-template), its rulings are checked here and its
14
+ // owners when the directory is checked as its own root (no TOML parser is needed before install). An app without the
15
+ // declaration is a finding, so a missing declaration is never an exemption. A row the app declares not applicable is
16
+ // excused only by a ratified ruling: appAmendments.<app>.notApplicable.<row> for the `app` the
17
+ // declaration names and its profile, with the reason copied verbatim; any other not-applicable row is
18
+ // a finding and the row still applies (rightkit#1140). Owner-pending rows never fail. Below-required versions
19
+ // and path/git overrides stay with `right-release ownership-mandatory --enforce` (versions move with
20
+ // every publish). Findings carry an identity, so an app may ratchet them in its baseline.
21
+ // ownership-exceptions OW-06 every committed .rightkit/ownership-exceptions.json: { schema?: 1, generated?, exceptions: [...] };
22
+ // each entry holds only the known keys, with capability, package (or crate), reason, owner,
23
+ // approvedBy, a real `date` not later than today, a real `expires` not before `date`, and `issue`, a
24
+ // RightKit issue (`<owner>/rightkit#<n>` or https://github.com/<owner>/rightkit/issues/<n>). Schema
25
+ // findings carry an identity (an adoption ratchet, bounded by the baseline's 90-day sunset); an entry
26
+ // past its `expires` date is a finding with no identity, so no baseline can silence an expiry. Each
27
+ // entry must also match a ratified ruling, appAmendments.<app>.exceptions in ownership-mandatory-set.json
28
+ // for the `app` the root rightkit-mandatory.json names: same capability, package (or crate), path when
29
+ // the ruling names one, and RightKit issue, expiring no later than the ruling. `approvedBy` alone is
30
+ // text the app writes; an entry no ruling covers is a finding.
31
+ import fs from "node:fs";
32
+ import path from "node:path";
33
+
34
+ const readText = (root, rel) => { try { return fs.readFileSync(path.join(root, rel), "utf8"); } catch { return null; } };
35
+ const readJson = (root, rel) => { const text = readText(root, rel); try { return text === null ? null : JSON.parse(text); } catch { return null; } };
36
+
37
+ // The mandatory-set evaluator parses Cargo.toml (smol-toml). It is loaded only when a checked tree is an app, so a copy of the check
38
+ // without it still runs every other rule; an app whose check cannot load it fails the rule ("the rule itself failed").
39
+ let mandatorySet = null;
40
+ let mandatorySetError = null;
41
+ try { mandatorySet = await import("./ownership-mandatory-set.mjs"); } catch (error) { mandatorySetError = error; }
42
+
43
+ export const APP_MARKERS = Object.freeze(["rightkit-mandatory.json"]);
44
+ export const APP_DECLARATION_FILE = "rightkit-mandatory.json";
45
+ const RELEASE_CONFIG = /^(?:(.+)\/)?right-release\.config\.(?:mjs|json)$/;
46
+ const SKIPPED_TREE = /(^|\/)(node_modules|vendor|target)\//;
47
+ const MANDATORY_DATA = new URL("./ownership-mandatory-set.json", import.meta.url);
48
+ /** The mandatory-set data, read as JSON (no TOML parser needed), or null when it cannot be read. */
49
+ export function mandatoryData() { try { return JSON.parse(fs.readFileSync(MANDATORY_DATA, "utf8")); } catch { return null; } }
50
+ export const OWNERSHIP_LEDGER_FILE = ".rightkit/ownership-exceptions.json";
51
+ export const LEDGER_TOP_KEYS = Object.freeze(["schema", "generated", "exceptions"]);
52
+ export const LEDGER_ENTRY_KEYS = Object.freeze(["capability", "package", "crate", "path", "fingerprint", "reason", "owner", "approvedBy", "date", "expires", "issue"]);
53
+ const RIGHTKIT_ISSUE = /^(?:https:\/\/github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/rightkit\/issues\/([1-9]\d*)|([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/rightkit#([1-9]\d*))$/i;
54
+ const todayOf = (now) => (now instanceof Date ? now : new Date()).toISOString().slice(0, 10);
55
+
56
+ /** True when `text` is a real calendar date written YYYY-MM-DD (UTC). */
57
+ export function isRealDate(text) {
58
+ if (typeof text !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(text)) return false;
59
+ const parsed = new Date(`${text}T00:00:00Z`);
60
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === text;
61
+ }
62
+
63
+ /** The RightKit issue an `issue` field names, as `<owner>/rightkit#<n>`, or null when it names no RightKit issue. */
64
+ export function rightkitIssue(text) {
65
+ const m = typeof text === "string" ? RIGHTKIT_ISSUE.exec(text.trim()) : null;
66
+ return m ? `${m[1] ?? m[3]}/rightkit#${m[2] ?? m[4]}` : null;
67
+ }
68
+
69
+ const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
70
+
71
+ /**
72
+ * Validate one ledger entry. Returns { problems: [string], expired: boolean }; `expired` is set only for an otherwise dated entry
73
+ * whose `expires` is before `today` (YYYY-MM-DD, UTC).
74
+ */
75
+ export function validateLedgerEntry(entry, { today = todayOf() } = {}) {
76
+ const problems = [];
77
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return { problems: ["is not an object"], expired: false };
78
+ const unknown = Object.keys(entry).filter((key) => !LEDGER_ENTRY_KEYS.includes(key));
79
+ if (unknown.length) problems.push(`unknown key(s) ${unknown.join(", ")} (allowed: ${LEDGER_ENTRY_KEYS.join(", ")})`);
80
+ for (const key of ["capability", "reason", "owner", "approvedBy"]) if (!nonEmpty(entry[key])) problems.push(`missing ${key}`);
81
+ if (entry.package !== undefined && entry.crate !== undefined) problems.push("names both package and crate; keep one");
82
+ else if (!nonEmpty(entry.package ?? entry.crate)) problems.push("missing package (or crate)");
83
+ for (const key of ["path", "fingerprint"]) if (entry[key] !== undefined && !nonEmpty(entry[key])) problems.push(`${key} must be a non-empty string when present`);
84
+ const dated = isRealDate(entry.date);
85
+ if (!dated) problems.push("date must be a real date, YYYY-MM-DD (UTC)");
86
+ else if (entry.date > today) problems.push(`date ${entry.date} is later than today (${today}, UTC)`);
87
+ const expiring = isRealDate(entry.expires);
88
+ if (!expiring) problems.push("expires must be a real date, YYYY-MM-DD (UTC): every exception expires");
89
+ else if (dated && entry.expires < entry.date) problems.push(`expires ${entry.expires} is before date ${entry.date}`);
90
+ if (entry.issue === undefined) problems.push("missing issue: the RightKit issue that moves this code into RightKit");
91
+ else if (!rightkitIssue(entry.issue)) problems.push(`issue ${JSON.stringify(entry.issue)} is not a RightKit issue (<owner>/rightkit#<n> or https://github.com/<owner>/rightkit/issues/<n>)`);
92
+ return { problems, expired: expiring && entry.expires < today };
93
+ }
94
+
95
+ /**
96
+ * Validate one ledger document (parsed JSON, or null when unparseable). Returns [{ problem, fix, subject?, expired? }]:
97
+ * a schema finding carries a `subject`; an expired entry carries `expired: true` and no subject.
98
+ */
99
+ export function validateLedger(doc, { today = todayOf() } = {}) {
100
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) return [{ problem: "is not a JSON object { exceptions: [...] }", fix: "write { \"exceptions\": [] } and add approved, expiring entries", subject: "document" }];
101
+ const out = [];
102
+ const unknown = Object.keys(doc).filter((key) => !LEDGER_TOP_KEYS.includes(key));
103
+ if (unknown.length) out.push({ problem: `unknown top-level key(s) ${unknown.join(", ")} (allowed: ${LEDGER_TOP_KEYS.join(", ")})`, fix: "remove them", subject: "document" });
104
+ if (doc.schema !== undefined && doc.schema !== 1) out.push({ problem: `schema ${JSON.stringify(doc.schema)} is not 1`, fix: "set \"schema\": 1 or drop it", subject: "document" });
105
+ if (!Array.isArray(doc.exceptions)) { out.push({ problem: "exceptions must be an array", fix: "write \"exceptions\": []", subject: "document" }); return out; }
106
+ doc.exceptions.forEach((entry, index) => {
107
+ const label = entry && typeof entry === "object" && !Array.isArray(entry) ? `${entry.capability ?? "?"}/${entry.package ?? entry.crate ?? "?"}${entry.path ? ` ${entry.path}` : ""}` : "?";
108
+ const { problems, expired } = validateLedgerEntry(entry, { today });
109
+ if (problems.length) out.push({ problem: `entry #${index} (${label}): ${problems.join("; ")}`, fix: "every exception needs capability, package, reason, owner, approvedBy, date, expires and the RightKit issue that moves the code", subject: `#${index} ${label}` });
110
+ if (expired) out.push({ problem: `entry #${index} (${label}) expired on ${entry.expires} (today ${today}, UTC); an expired exception is a failure and no baseline lists it`, fix: `move the code into RightKit (${rightkitIssue(entry.issue) ?? "its RightKit issue"}) and remove the entry, or record a fresh approval with a new expires date`, expired: true });
111
+ });
112
+ return out;
113
+ }
114
+
115
+ /**
116
+ * The ratified ruling that covers a ledger entry, or null. `rulings` is appAmendments.<app>.exceptions: [{ capability, package | crate,
117
+ * path?, issue, expires }]. A ruling with no `path` covers every path of its capability and package.
118
+ */
119
+ export function rulingFor(entry, rulings) {
120
+ if (!entry || typeof entry !== "object" || !Array.isArray(rulings)) return null;
121
+ const issue = rightkitIssue(entry.issue);
122
+ return rulings.find((r) => r && r.capability === entry.capability && (r.package ?? r.crate) === (entry.package ?? entry.crate)
123
+ && (r.path === undefined || r.path === entry.path) && issue !== null && rightkitIssue(r.issue)?.toLowerCase() === issue.toLowerCase()
124
+ && isRealDate(r.expires) && isRealDate(entry.expires) && entry.expires <= r.expires) ?? null;
125
+ }
126
+
127
+ /** The declaration as ownership-mandatory-set.mjs reads it (profile and not-applicable rows of 10+ characters), without its TOML parser. */
128
+ function declarationOf(appRoot, data) {
129
+ const doc = readJson(appRoot, data.appDeclarationFile) ?? {};
130
+ const notApplicable = {};
131
+ for (const [id, reason] of Object.entries(doc.notApplicable ?? {})) if (typeof reason === "string" && reason.trim().length >= 10) notApplicable[id] = reason.trim();
132
+ return { profile: doc.profile ?? data.defaultProfile, notApplicable, records: {}, problems: [] };
133
+ }
134
+
135
+ /** The `app` a root declaration names, or null. */
136
+ export function declaredApp(root) {
137
+ const doc = readJson(root, APP_DECLARATION_FILE);
138
+ return doc && typeof doc === "object" && typeof doc.app === "string" && doc.app.trim() ? doc.app.trim() : null;
139
+ }
140
+
141
+ /** The ownership-exceptions rule: every committed ledger in the tree. */
142
+ export function ownershipExceptions({ root, files, now }, { data = mandatoryData() } = {}) {
143
+ const today = todayOf(now);
144
+ const app = declaredApp(root);
145
+ const amendment = app && data?.appAmendments ? data.appAmendments[app] : undefined;
146
+ const rulings = amendment && Array.isArray(amendment.exceptions) ? amendment.exceptions : [];
147
+ const ledgers = (files ?? []).filter((file) => file === OWNERSHIP_LEDGER_FILE || file.endsWith(`/${OWNERSHIP_LEDGER_FILE}`)).sort();
148
+ const out = [];
149
+ for (const file of ledgers) {
150
+ const text = readText(root, file);
151
+ let doc = null;
152
+ try { doc = JSON.parse(text); } catch (error) {
153
+ out.push({ file, subject: "document", problem: `${file} is not valid JSON (${error.message})`, fix: "repair the JSON" });
154
+ continue;
155
+ }
156
+ for (const finding of validateLedger(doc, { today })) {
157
+ const { subject, expired, ...rest } = finding;
158
+ out.push({ ...rest, problem: `${file}: ${rest.problem}`, ...(expired ? {} : { file, subject }) });
159
+ }
160
+ (Array.isArray(doc?.exceptions) ? doc.exceptions : []).forEach((entry, index) => {
161
+ if (!entry || typeof entry !== "object" || Array.isArray(entry) || rulingFor(entry, rulings)) return;
162
+ const label = `${entry.capability ?? "?"}/${entry.package ?? entry.crate ?? "?"}${entry.path ? ` ${entry.path}` : ""}`;
163
+ out.push({
164
+ file, subject: `#${index} ${label} ruling`,
165
+ problem: `${file}: entry #${index} (${label}) matches no ratified ruling: ${app ? `appAmendments.${app}.exceptions in ownership-mandatory-set.json has none with this capability, ${entry.crate !== undefined ? "crate" : "package"}, path, issue and an expiry no earlier than ${entry.expires}` : `${APP_DECLARATION_FILE} names no "app", so no ruling can apply`}`,
166
+ fix: `ratify the exception in RightKit (appAmendments.<app>.exceptions, with its issue and expiry) and name the app in ${APP_DECLARATION_FILE}, or move the code into RightKit and remove the entry`,
167
+ });
168
+ });
169
+ }
170
+ return out;
171
+ }
172
+
173
+ /** The marker files that make the checked root an app, in APP_MARKERS order. */
174
+ export function appMarkers(root) {
175
+ return APP_MARKERS.filter((name) => readText(root, name) !== null);
176
+ }
177
+
178
+ /**
179
+ * The release configs that make a tree an app even without its declaration: a root right-release.config.(mjs|json), or one whose
180
+ * directory also holds src-tauri/tauri.conf.json (a packaged desktop app).
181
+ */
182
+ export function releaseAppMarkers(files) {
183
+ const tracked = new Set(files ?? []);
184
+ return [...tracked].filter((file) => {
185
+ const m = RELEASE_CONFIG.exec(file);
186
+ if (!m || SKIPPED_TREE.test(file)) return false;
187
+ return m[1] === undefined || tracked.has(`${m[1]}/src-tauri/tauri.conf.json`);
188
+ }).sort();
189
+ }
190
+
191
+ /** Split declared not-applicable rows into those a ratified ruling covers and those it does not. */
192
+ export function ratifiedNotApplicable(declared, { app, profile, data }) {
193
+ const amendment = app && data?.appAmendments ? data.appAmendments[app] : undefined;
194
+ const rulings = amendment && amendment.profile === profile && amendment.notApplicable && typeof amendment.notApplicable === "object" ? amendment.notApplicable : {};
195
+ const ratified = {}, unratified = {};
196
+ for (const [id, reason] of Object.entries(declared ?? {})) {
197
+ if (typeof rulings[id] === "string" && rulings[id].trim() === reason.trim()) ratified[id] = reason;
198
+ else unratified[id] = reason;
199
+ }
200
+ return { ratified, unratified, ruled: Boolean(amendment) };
201
+ }
202
+
203
+ /**
204
+ * The findings of one app root (`dir` relative to the checked root, "" for the root itself). `owners: false` runs only the checks
205
+ * that need no TOML parser (the declaration parses; every not-applicable row is ratified): a nested app's foundation owners are
206
+ * checked when its own directory is checked, so RightKit's preflight, which runs before any install, can still see the rulings.
207
+ */
208
+ function checkAppRoot({ root, dir, evaluator, data, owners = true }) {
209
+ const appRoot = dir ? path.join(root, dir) : root;
210
+ const declarationFile = data.appDeclarationFile;
211
+ const file = dir ? `${dir}/${declarationFile}` : declarationFile;
212
+ const out = [];
213
+ if (readJson(appRoot, declarationFile) === null) {
214
+ out.push({ file, subject: "declaration", problem: `${file} is not valid JSON, so the app's profile and not-applicable rows cannot be read`, fix: `repair ${file}` });
215
+ return out;
216
+ }
217
+ const declaration = (evaluator ?? { loadAppDeclaration: declarationOf }).loadAppDeclaration(appRoot, data);
218
+ const app = declaredApp(appRoot);
219
+ const { ratified, unratified, ruled } = ratifiedNotApplicable(declaration.notApplicable, { app, profile: declaration.profile, data });
220
+ for (const [row, reason] of Object.entries(unratified)) out.push({
221
+ file, subject: `notApplicable:${row}`,
222
+ problem: `notApplicable.${row} ("${reason.slice(0, 80)}${reason.length > 80 ? "..." : ""}") is not a ratified ruling: ${!app ? `${file} names no "app"` : !ruled ? `appAmendments has no entry for "${app}"` : `appAmendments.${app} (profile ${data.appAmendments[app].profile}) has no ruling for ${row} with this reason verbatim for profile ${declaration.profile}`}; the row still applies`,
223
+ fix: `ratify the ruling in RightKit (appAmendments.${app ?? "<app>"}.notApplicable.${row}, rightkit#1140) and copy its reason verbatim, or adopt the row's owner`,
224
+ });
225
+ if (!owners) return out;
226
+ const report = evaluator.checkApp(appRoot, { data, declaration: { ...declaration, notApplicable: ratified } });
227
+ for (const owner of report.owners) {
228
+ if (owner.status === "unknown-profile" || owner.status === "unratified") {
229
+ out.push({ file, subject: owner.owner, problem: `profile "${report.profile}" is ${owner.status === "unknown-profile" ? "not a profile of ownership-mandatory-set.json" : "not ratified (or has no rows)"}, so no foundation set can be checked`, fix: `select a ratified profile (${Object.entries(data.profiles).filter(([, p]) => p.ratified !== false).map(([name]) => name).join(", ")}) in ${file}` });
230
+ continue;
231
+ }
232
+ if (owner.status !== "missing") continue;
233
+ const devOnly = owner.evidence.some((line) => line.includes("dev-dependencies"));
234
+ out.push({
235
+ file, subject: owner.owner,
236
+ problem: `profile "${report.profile}" requires the foundation owner ${owner.owner} (${owner.ecosystem}, row${owner.rows.length > 1 ? "s" : ""} ${owner.rows.join(", ")}) and the app does not use it${devOnly ? " (declared only as a dev dependency)" : ""}`,
237
+ fix: `depend on ${owner.owner} >= ${owner.required} from the registry and use it, or, when the row truly does not apply, declare it under notApplicable in ${file} with the reason RightKit ratified for the app`,
238
+ });
239
+ }
240
+ return out;
241
+ }
242
+
243
+ /**
244
+ * The mandatory-foundation rule. The root declaration makes the whole tree one app. Without it, every release config that ships a
245
+ * desktop app (releaseAppMarkers) is an app: one whose own directory holds a declaration is checked from there; the others are one
246
+ * finding, the missing declaration (no profile is chosen for the app, so none of its rows can be excused or checked).
247
+ */
248
+ export function mandatoryFoundation({ root, files }, { evaluator = mandatorySet } = {}) {
249
+ const markers = appMarkers(root);
250
+ const releaseMarkers = markers.length ? [] : releaseAppMarkers(files);
251
+ if (!markers.length && !releaseMarkers.length) return [];
252
+ const out = [];
253
+ const declared = [], undeclared = [];
254
+ for (const marker of releaseMarkers) {
255
+ const dir = marker.includes("/") ? marker.slice(0, marker.lastIndexOf("/")) : "";
256
+ (dir && readText(root, `${dir}/${APP_DECLARATION_FILE}`) !== null ? declared : undeclared).push(dir || marker);
257
+ }
258
+ if (undeclared.length) out.push({
259
+ file: APP_DECLARATION_FILE, subject: "declaration",
260
+ problem: `${undeclared.join(", ")} ships a desktop app, and the repository has no ${APP_DECLARATION_FILE}: an app without its declaration is never exempt from the mandatory foundation set`,
261
+ fix: `commit ${APP_DECLARATION_FILE} at the root (or beside the app's release config) with the app's "app" name, its "profile" and only the not-applicable rows ratified for it in appAmendments`,
262
+ });
263
+ if (!markers.length && !declared.length) return out;
264
+ if (markers.length) {
265
+ if (!evaluator) throw new Error(`ownership-mandatory-set.mjs could not be loaded: ${mandatorySetError?.message ?? "unknown error"}`);
266
+ return checkAppRoot({ root, dir: "", evaluator, data: evaluator.loadData() });
267
+ }
268
+ const data = mandatoryData();
269
+ if (!data) throw new Error("ownership-mandatory-set.json could not be read");
270
+ for (const dir of declared) out.push(...checkAppRoot({ root, dir, evaluator: null, data, owners: false }));
271
+ return out;
272
+ }
273
+
274
+ export const OWNERSHIP_CLASS_RULES = Object.freeze([
275
+ { id: "mandatory-foundation", gap: "OW-05", run: (ctx) => mandatoryFoundation(ctx) },
276
+ { id: "ownership-exceptions", gap: "OW-06", run: ownershipExceptions },
277
+ ]);
@@ -118,8 +118,10 @@ function persistCheckpoint(trust, bytes, file, audit) {
118
118
  export function readCommittedKeyAuthority(trust) { return readAuthority(trust); }
119
119
  function readAuthority(trust, { file, audit = auditPath } = {}) {
120
120
  try {
121
- file ??= trustedPath();
121
+ // The authority's own shape is judged before anything host-specific: a malformed authority is "malformed" on every
122
+ // platform, not "unsupported platform" on the hosts that have no protected trust location (fixture scan-error-key-authority-malformed).
122
123
  const expected = validateAuthority(trust?.keyAuthority);
124
+ file ??= trustedPath();
123
125
  const top = String(git(expected.checkoutPath, ["rev-parse", "--show-toplevel"])).trim();
124
126
  const normalizedTop = process.platform === "win32" ? path.resolve(top).toLowerCase() : path.resolve(top);
125
127
  const normalizedExpected = process.platform === "win32" ? expected.checkoutPath.toLowerCase() : expected.checkoutPath;
@@ -301,10 +301,11 @@ export function pendingAppliesTo(row, active) {
301
301
  return row.profile === "all" || (Array.isArray(row.profile) ? row.profile.includes(active) : row.profile === active);
302
302
  }
303
303
 
304
- export function checkApp(root, { data = loadData(), profile, amendments = [] } = {}) {
304
+ // `declaration` replaces the one read from root (the app check passes the declaration with its unratified not-applicable rows removed).
305
+ export function checkApp(root, { data = loadData(), profile, amendments = [], declaration: override = null } = {}) {
305
306
  const name = path.basename(root);
306
307
  if (!existsSync(root) || !statSync(root).isDirectory()) return { app: name, root, scanned: false, profile: profile ?? data.defaultProfile, owners: [], pending: [], notApplicable: [], problems: ["app root missing"], violations: 0 };
307
- const declaration = loadAppDeclaration(root, data);
308
+ const declaration = override ?? loadAppDeclaration(root, data);
308
309
  const active = profile ?? declaration.profile;
309
310
  const problems = [...declaration.problems];
310
311
  if (!data.profiles[active]) problems.push(`unknown profile "${active}"`);
package/package.json CHANGED
@@ -1,22 +1,63 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "right-release": "cli/right-release.mjs",
9
- "rightkit-gate": "bin/rightkit-gate.mjs"
9
+ "rightkit-gate": "bin/rightkit-gate.mjs",
10
+ "rightkit-agent-hook": "bin/rightkit-agent-hook.mjs"
11
+ },
12
+ "exports": {
13
+ "./bin/rightkit-agent-hook.mjs": "./bin/rightkit-agent-hook.mjs",
14
+ "./bin/rightkit-gate.mjs": "./bin/rightkit-gate.mjs",
15
+ "./cargo-contract.mjs": "./cargo-contract.mjs",
16
+ "./cargo-target.mjs": "./cargo-target.mjs",
17
+ "./cli/right-release.mjs": "./cli/right-release.mjs",
18
+ "./development.mjs": "./development.mjs",
19
+ "./direct-bootstrap.mjs": "./direct-bootstrap.mjs",
20
+ "./disk-policy.mjs": "./disk-policy.mjs",
21
+ "./dmg-build.mjs": "./dmg-build.mjs",
22
+ "./engine-locator.mjs": "./engine-locator.mjs",
23
+ "./github-release.mjs": "./github-release.mjs",
24
+ "./legal-contract.mjs": "./legal-contract.mjs",
25
+ "./macos-file-evidence.mjs": "./macos-file-evidence.mjs",
26
+ "./macos-notarization.mjs": "./macos-notarization.mjs",
27
+ "./macos-signing-identity.mjs": "./macos-signing-identity.mjs",
28
+ "./macos-signing-session.mjs": "./macos-signing-session.mjs",
29
+ "./managed-cargo.mjs": "./managed-cargo.mjs",
30
+ "./native-release-finalization.mjs": "./native-release-finalization.mjs",
31
+ "./notary-auth.mjs": "./notary-auth.mjs",
32
+ "./package.json": "./package.json",
33
+ "./packaged-app-identity.mjs": "./packaged-app-identity.mjs",
34
+ "./pe-content.mjs": "./pe-content.mjs",
35
+ "./publication-gate.mjs": "./publication-gate.mjs",
36
+ "./qa-build-features.mjs": "./qa-build-features.mjs",
37
+ "./release-dependencies.mjs": "./release-dependencies.mjs",
38
+ "./sign-windows.mjs": "./sign-windows.mjs",
39
+ "./signing-agent-client.mjs": "./signing-agent-client.mjs",
40
+ "./stage-binary.mjs": "./stage-binary.mjs",
41
+ "./stage-sidecar.mjs": "./stage-sidecar.mjs",
42
+ "./supply-chain-evidence.mjs": "./supply-chain-evidence.mjs",
43
+ "./tauri-bundle-marker.mjs": "./tauri-bundle-marker.mjs"
10
44
  },
11
45
  "rightkit": {
12
46
  "publishCheck": {
47
+ "skipImport": [
48
+ "./sign-windows.mjs"
49
+ ],
13
50
  "binSmoke": {
14
51
  "right-release": [
15
52
  "--version"
16
53
  ],
17
54
  "rightkit-gate": [
18
55
  "--version"
19
- ]
56
+ ],
57
+ "rightkit-agent-hook": {
58
+ "args": [],
59
+ "output": "{}"
60
+ }
20
61
  }
21
62
  }
22
63
  },
@@ -26,6 +67,7 @@
26
67
  "templates",
27
68
  "*.mjs",
28
69
  "*.json",
70
+ "deny-default.toml",
29
71
  "*.sh",
30
72
  "msix-repair-guard.ps1",
31
73
  "bin/darwin/rightkit-swap",
@@ -48,6 +90,7 @@
48
90
  "directory": "packages/release"
49
91
  },
50
92
  "dependencies": {
93
+ "@rightkit/outdated": "0.2.0",
51
94
  "smol-toml": "1.9.1"
52
95
  },
53
96
  "scripts": {
@@ -230,6 +230,7 @@ function admitRuntime(bin, rest, options, label) {
230
230
  case "right-release": return admitRightRelease(rest, label);
231
231
  case "tauri": return admitTauri(rest, options, label);
232
232
  case "vite": return rest[0] === "build" ? (VITE_OPTIONS(rest.slice(1), label) ?? admit("vite-build", "runtime")) : rest.length === 1 && rest[0] === "--version" ? admit("vite-version", "runtime") : refuse("R1-option", `${label}: only vite build and vite --version are admitted`);
233
+ case "rightkit-gate": return admitRightKitGate(rest, label);
233
234
  case "tsc": return shellSafeArgs(rest, label) ?? admit("tsc", "runtime");
234
235
  case "vitest": return rest[0] === "run" || (rest.length === 1 && rest[0] === "--version") ? (shellSafeArgs(rest, label) ?? admit("vitest", "runtime")) : refuse("R1-option", `${label}: only vitest run is admitted`);
235
236
  case "eslint": return shellSafeArgs(rest, label) ?? admit("eslint", "runtime");
@@ -237,6 +238,16 @@ function admitRuntime(bin, rest, options, label) {
237
238
  }
238
239
  }
239
240
 
241
+ /**
242
+ * `rightkit-gate install-hook`: writes the committed pre-push hook and sets core.hooksPath, so an app's `prepare` script may run it.
243
+ * It builds, signs, packages and uploads nothing. The gate itself (`rightkit-gate`, `--base`) compiles and is run by the hook, never
244
+ * by a release config; only the installer and the version probe are admitted.
245
+ */
246
+ function admitRightKitGate(args, label) {
247
+ if (args.length === 1 && (args[0] === "install-hook" || args[0] === "--version")) return admit(`rightkit-gate-${args[0].replace(/^--/, "")}`, "owner");
248
+ return refuse("R1-owner-subcommand", `${label}: only rightkit-gate install-hook (and --version) is an admitted pipeline command; the gate itself runs from the pre-push hook`);
249
+ }
250
+
240
251
  function admitRightRelease(args, label) {
241
252
  const sub = args[0];
242
253
  if (!OWNER_SUBCOMMANDS.has(sub)) return refuse("R1-owner-subcommand", `${label}: right-release ${JSON.stringify(sub ?? "")} is not a known RightKit command`);
@@ -350,6 +361,7 @@ export function admitInvocation(step, options = {}) {
350
361
  const tool = classifyTokens([name, ...args]);
351
362
  if (tool) return refuse("R3-pipeline-tool", `${label}: ${tool.tool} is a ${tool.stage} tool; build, sign, notarize, package and upload run only inside RightKit`);
352
363
  if (name === "right-release") return admitRightRelease(args, label);
364
+ if (name === "rightkit-gate") return admitRightKitGate(args, label);
353
365
  if (name === "pnpm") return admitPnpm(args, inner, label, chain);
354
366
  if (name === "node") return admitNode(args, inner, label);
355
367
  if (name === "rightkit") return refuse("R1-retired-client", `${label}: rightkit is the retired name of the RustRight client (rustright#91); use rustright ${args.slice(0, 2).join(" ")}`);