@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,469 @@
1
+ // Consumer source contracts: files-only check rules for mechanisms that failed consumer CI minutes into a run, read as data before
2
+ // any build (SellRight's historical Class A rows of the consumer audit). Like the shell contract, every rule is
3
+ // { id, gap, run({ root, files }) } and returns findings [{ file, subject, problem, fix }]: `root` holds the exported tree, `files`
4
+ // is the posix path of every tracked file, `problem` starts with "<file>:<line>", and rule|file|subject (no line number) is the
5
+ // identity an app baseline names. Node builtins only (preflight runs with no node_modules); no repository code, workflow, script,
6
+ // Vite or Caddy config is executed, imported or evaluated: workflow YAML, package.json and Caddyfiles are read as text.
7
+ //
8
+ // workflow-action-pair CA-04 one job runs github/codeql-action init/autobuild/analyze at different refs (SellRight dependabot
9
+ // PRs #263/#264 bumped init or analyze alone to v4.39.0: the job fails at analyze)
10
+ // referenced-input CA-07 a command input a checkout does not have: a package.json script or workflow `run:` that runs
11
+ // `node|sh|bash|python <file>` absent from the commit or outside the repository, a `pnpm --dir`
12
+ // / `npm --prefix` directory, workflow `working-directory` or `cache-dependency-path` that is absent
13
+ // caddy-global-options CA-08 a Caddyfile whose first block is (or, with its {$VAR} address empty, becomes) the global options
14
+ // block while it holds site directives such as `encode` (Caddy refuses "unrecognized global option")
15
+ //
16
+ // Legitimate exceptions stay accepted: build outputs (dist/, build/, out/, target/, node_modules/ ...), anything spelled with a
17
+ // variable, glob or expression, directories a workflow checks out or downloads into (`path:` of actions/checkout or
18
+ // download-artifact), fixtures (a fixtures/ or testdata/ tree), and Caddy global options this rule does not know (plugins add them).
19
+ import path from "node:path";
20
+ import fs from "node:fs";
21
+
22
+ const SKIPPED_TREE = /(^|\/)(node_modules|vendor|target)\//;
23
+ const FIXTURE_TREE = /(^|\/)(fixtures?|__fixtures__|testdata|test-fixtures)\//;
24
+ const scanned = (file) => !SKIPPED_TREE.test(file);
25
+ const readText = (root, rel) => { try { return fs.readFileSync(path.join(root, rel), "utf8"); } catch { return null; } };
26
+ const lineOf = (text, offset) => text.slice(0, offset).split("\n").length;
27
+ const workflowFiles = (files) => files.filter((f) => /^\.github\/workflows\/[^/]+\.ya?ml$/.test(f)).sort();
28
+
29
+ // ---- a line reader for GitHub workflow YAML (block style, the form workflows are written in) ------------------------------------
30
+ const unquote = (value) => {
31
+ const v = value.trim();
32
+ if ((v.startsWith('"') && v.endsWith('"') && v.length > 1) || (v.startsWith("'") && v.endsWith("'") && v.length > 1)) return v.slice(1, -1);
33
+ return v;
34
+ };
35
+ /** `value` without a trailing ` # comment` (outside quotes). */
36
+ const stripComment = (value) => {
37
+ let quote = null;
38
+ for (let i = 0; i < value.length; i += 1) {
39
+ const c = value[i];
40
+ if (quote) { if (c === quote) quote = null; continue; }
41
+ if (c === '"' || c === "'") quote = c;
42
+ else if (c === "#" && (i === 0 || /\s/.test(value[i - 1]))) return value.slice(0, i).trimEnd();
43
+ }
44
+ return value;
45
+ };
46
+
47
+ /**
48
+ * Every `key: value` of a block-style YAML document: { line, key, value, block, path, items }. `path` is the chain of keys above and
49
+ * including this one; `items[i]` is the sequence-item number of path[i] (null outside a sequence item), so two keys of one step share
50
+ * the item number at the step's depth. A block scalar (`|`, `>`) is `block: [{ line, text }]` and its lines are not read as keys.
51
+ * Flow collections and anchors are not followed (workflows rarely use them; a key inside one is simply not seen).
52
+ */
53
+ export function yamlEntries(text) {
54
+ const lines = String(text ?? "").replace(/\r\n?/g, "\n").split("\n");
55
+ const out = [];
56
+ const stack = [];
57
+ let nextItem = 0;
58
+ for (let i = 0; i < lines.length; i += 1) {
59
+ const raw = lines[i];
60
+ if (/^\s*(#.*)?$/.test(raw) || /^\s*(---|\.\.\.)\s*$/.test(raw)) continue;
61
+ let indent = raw.match(/^ */)[0].length;
62
+ let rest = raw.slice(indent);
63
+ let item = null;
64
+ while (/^-(\s|$)/.test(rest)) {
65
+ const dash = indent;
66
+ while (stack.length && stack.at(-1).indent > dash) stack.pop();
67
+ item = (nextItem += 1);
68
+ const after = rest.slice(1);
69
+ const gap = after.match(/^\s*/)[0].length;
70
+ indent += 1 + gap;
71
+ rest = after.slice(gap);
72
+ if (rest === "") break;
73
+ }
74
+ const m = /^([A-Za-z0-9_.$/-]+|"[^"]*"|'[^']*')\s*:(?:\s+(.*)|\s*)$/.exec(rest);
75
+ if (!m) continue;
76
+ const key = unquote(m[1]);
77
+ let value = m[2] === undefined ? "" : stripComment(m[2]);
78
+ while (stack.length && stack.at(-1).indent >= indent) {
79
+ const top = stack.pop();
80
+ if (top.indent === indent && item === null) item = top.item;
81
+ }
82
+ const entry = { line: i + 1, key, value: unquote(value), block: null, path: [...stack.map((s) => s.key), key], items: [...stack.map((s) => s.item), item] };
83
+ if (/^[|>][+-]?\d*$/.test(value.trim())) {
84
+ const block = [];
85
+ let j = i + 1;
86
+ for (; j < lines.length; j += 1) {
87
+ const l = lines[j];
88
+ if (/^\s*$/.test(l)) { block.push({ line: j + 1, text: "" }); continue; }
89
+ if (l.match(/^ */)[0].length <= indent) break;
90
+ block.push({ line: j + 1, text: l.trim() });
91
+ }
92
+ while (block.length && block.at(-1).text === "") block.pop();
93
+ entry.block = block;
94
+ entry.value = "";
95
+ i = j - 1;
96
+ }
97
+ out.push(entry);
98
+ stack.push({ indent, key, item });
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /** The steps of each job: Map(job -> [{ item, entries }]) and the job-level and workflow-level defaults.run.working-directory. */
104
+ function workflowJobs(entries) {
105
+ const jobs = new Map();
106
+ const stepOf = new Map();
107
+ for (const e of entries) {
108
+ if (e.path[0] !== "jobs" || e.path.length < 2) continue;
109
+ const job = e.path[1];
110
+ if (!jobs.has(job)) jobs.set(job, { steps: [], defaultsDir: null, line: e.line });
111
+ const j = jobs.get(job);
112
+ if (e.path.length === 5 && e.path[2] === "defaults" && e.path[3] === "run" && e.path[4] === "working-directory") j.defaultsDir = e;
113
+ if (e.path[2] !== "steps" || e.path.length < 4 || e.items[3] === null) continue;
114
+ const id = `${job}#${e.items[3]}`;
115
+ if (!stepOf.has(id)) { const step = { item: e.items[3], entries: [] }; stepOf.set(id, step); j.steps.push(step); }
116
+ stepOf.get(id).entries.push({ ...e, rel: e.path.slice(3) });
117
+ }
118
+ const topDefaultsDir = entries.find((e) => e.path.join(".") === "defaults.run.working-directory") ?? null;
119
+ return { jobs, topDefaultsDir };
120
+ }
121
+ const stepGet = (step, ...rel) => step.entries.find((e) => e.rel.length === rel.length && e.rel.every((k, i) => k === rel[i])) ?? null;
122
+
123
+ // ---- CA-04: paired actions in one job run one version --------------------------------------------------------------------------
124
+ // CodeQL's init writes the database and configuration that autobuild and analyze read; the action refuses a job whose steps come from
125
+ // different releases. A ref is compared exactly: `@v4` beside `@<sha> # v4.38.3` is also two refs (the tag moves).
126
+ const PAIRED_ACTIONS = [{ repo: "github/codeql-action", parts: new Set(["init", "autobuild", "analyze", "resolve-environment"]) }];
127
+
128
+ function workflowActionPair({ root, files }) {
129
+ const out = [];
130
+ for (const file of workflowFiles(files)) {
131
+ const text = readText(root, file);
132
+ if (text === null || !text.includes("@")) continue;
133
+ const { jobs } = workflowJobs(yamlEntries(text));
134
+ for (const [job, { steps }] of jobs) {
135
+ for (const family of PAIRED_ACTIONS) {
136
+ const uses = [];
137
+ for (const step of steps) {
138
+ const u = stepGet(step, "uses");
139
+ const m = u && /^([^@\s]+)@(\S+)/.exec(u.value);
140
+ if (!m || !m[1].startsWith(`${family.repo}/`)) continue;
141
+ const part = m[1].slice(family.repo.length + 1);
142
+ if (!family.parts.has(part)) continue;
143
+ const comment = /#\s*(\S+)/.exec(text.split("\n")[u.line - 1].slice(text.split("\n")[u.line - 1].indexOf(m[2]) + m[2].length));
144
+ uses.push({ part, ref: m[2], label: comment ? `${m[2].slice(0, 12)} (${comment[1]})` : m[2], line: u.line });
145
+ }
146
+ const refs = [...new Set(uses.map((u) => u.ref))];
147
+ if (refs.length < 2) continue;
148
+ const first = uses[0];
149
+ const odd = uses.find((u) => u.ref !== first.ref);
150
+ out.push({
151
+ file, subject: `${job}:${family.repo}`,
152
+ problem: `${file}:${odd.line} job ${job} runs ${family.repo}/${odd.part}@${odd.label} but ${family.repo}/${first.part}@${first.label} (line ${first.line}): its steps must come from one release, or the job fails at ${odd.part === "init" ? first.part : odd.part}`,
153
+ fix: `pin every ${family.repo} step of job ${job} (${[...new Set(uses.map((u) => u.part))].join(", ")}) to the same commit; bump them together`,
154
+ });
155
+ }
156
+ }
157
+ }
158
+ return out;
159
+ }
160
+
161
+ // ---- CA-07: a command input the checkout does not have -------------------------------------------------------------------------
162
+ // Directories a build or install writes: a path through one of them is produced, not committed.
163
+ const PRODUCED_SEGMENT = /^(dist|dist-[^/]*|build|out|\.output|target|node_modules|coverage|\.next|\.nuxt|\.svelte-kit|\.turbo|\.cache)$/;
164
+ const DYNAMIC = /[$*?[\]{}%`~<>]|^\(|\)$/;
165
+ const SCRIPT_EXT = /\.(?:mjs|cjs|js|mts|cts|ts|tsx|sh|bash|py)$/;
166
+ // Interpreter flags that take the next word as their value.
167
+ const FLAG_VALUE = new Set(["--import", "--require", "-r", "--loader", "--experimental-loader", "--conditions", "-C", "--input-type", "--test-reporter", "--test-reporter-destination", "--env-file", "--title", "--stack-size", "-c", "-o", "-m", "-W", "-X"]);
168
+ const EVAL_FLAG = /^(-e|--eval|-p|--print|-c|-m)$/;
169
+
170
+ /** Shell words of one command line: quotes kept together, `&&`, `||`, `;`, `|` and newlines split commands. */
171
+ export function shellCommands(text) {
172
+ const commands = [];
173
+ let words = [];
174
+ let word = "";
175
+ let quote = null;
176
+ let started = false;
177
+ const endWord = () => { if (started) words.push(word); word = ""; started = false; };
178
+ const endCommand = () => { endWord(); if (words.length) commands.push(words); words = []; };
179
+ for (let i = 0; i < text.length; i += 1) {
180
+ const c = text[i];
181
+ if (quote) { if (c === quote) quote = null; else word += c; continue; }
182
+ if (c === '"' || c === "'") { quote = c; started = true; continue; }
183
+ if (c === "\\" && text[i + 1] === "\n") { i += 1; continue; }
184
+ if (c === "#" && !started) { while (i < text.length && text[i] !== "\n") i += 1; endCommand(); continue; }
185
+ if (c === "\n" || c === ";" || c === "&" || c === "|" || c === "(" || c === ")") { endCommand(); continue; }
186
+ if (/\s/.test(c)) { endWord(); continue; }
187
+ word += c; started = true;
188
+ }
189
+ endCommand();
190
+ return commands;
191
+ }
192
+
193
+ /**
194
+ * The repository inputs one command names, given its words: [{ kind: "file" | "dir", value }]. `cd` is returned as { kind: "cd" }.
195
+ * Only shapes that are certainly paths are read: the script an interpreter runs, and the directory pnpm/npm/yarn is pointed at.
196
+ */
197
+ export function commandInputs(words) {
198
+ const w = words;
199
+ let i = 0;
200
+ while (i < w.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(w[i])) i += 1;
201
+ while (i < w.length && ["env", "exec", "time", "nohup", "sudo", "command"].includes(w[i])) i += 1;
202
+ const cmd = w[i];
203
+ if (cmd === undefined) return [];
204
+ if (cmd === "cd" && w[i + 1] !== undefined) return [{ kind: "cd", value: w[i + 1] }];
205
+ const out = [];
206
+ if (/^(node|bash|sh|python|python3)$/.test(cmd)) {
207
+ for (let k = i + 1; k < w.length; k += 1) {
208
+ const a = w[k];
209
+ if (EVAL_FLAG.test(a) && cmd !== "sh" && cmd !== "bash") return out;
210
+ if (a === "-c" && (cmd === "sh" || cmd === "bash")) return out;
211
+ if (a === "--test" || a === "--") continue;
212
+ if (a.startsWith("-")) { if (!a.includes("=") && FLAG_VALUE.has(a)) k += 1; continue; }
213
+ if (SCRIPT_EXT.test(a) || a.includes("/")) out.push({ kind: "file", value: a });
214
+ return out;
215
+ }
216
+ return out;
217
+ }
218
+ if (/^(pnpm|npm|yarn)$/.test(cmd)) {
219
+ for (let k = i + 1; k < w.length; k += 1) {
220
+ const a = w[k];
221
+ const eq = /^(--dir|--prefix|--cwd)=(.+)$/.exec(a);
222
+ if (eq) out.push({ kind: "dir", value: eq[2] });
223
+ else if (["--dir", "-C", "--prefix", "--cwd"].includes(a) && w[k + 1] !== undefined) { out.push({ kind: "dir", value: w[k + 1] }); k += 1; }
224
+ }
225
+ }
226
+ return out;
227
+ }
228
+
229
+ /** A path matcher for the tracked .gitignore files: a path (or a directory above it) the repository ignores is produced, not committed. */
230
+ export function gitignoreMatcher(root, files) {
231
+ const rules = new Map();
232
+ for (const f of files.filter((x) => /(^|\/)\.gitignore$/.test(x))) {
233
+ const base = path.posix.dirname(f);
234
+ const list = [];
235
+ for (let line of String(readText(root, f) ?? "").replace(/\r\n?/g, "\n").split("\n")) {
236
+ line = line.replace(/(?<!\\)\s+$/, "");
237
+ if (!line || line.startsWith("#")) continue;
238
+ const negate = line.startsWith("!");
239
+ if (negate) line = line.slice(1);
240
+ const dirOnly = line.endsWith("/");
241
+ if (dirOnly) line = line.slice(0, -1);
242
+ const anchored = line.includes("/");
243
+ line = line.replace(/^\//, "");
244
+ const body = line.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\u0000").replace(/\/\*\*/g, "\u0001").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?").replace(/\u0001/g, "/.*");
245
+ list.push({ negate, dirOnly, re: new RegExp(anchored ? `^${body}$` : `(^|/)${body}$`) });
246
+ }
247
+ rules.set(base, list);
248
+ }
249
+ return (rel) => {
250
+ const parts = rel.split("/");
251
+ for (let n = 1; n <= parts.length; n += 1) {
252
+ const candidate = parts.slice(0, n).join("/");
253
+ const isDir = n < parts.length;
254
+ let ignored = false;
255
+ for (const [base, list] of rules) {
256
+ if (base !== "." && !(candidate.startsWith(`${base}/`))) continue;
257
+ const local = base === "." ? candidate : candidate.slice(base.length + 1);
258
+ for (const r of list) if ((!r.dirOnly || isDir || n === parts.length) && r.re.test(local)) ignored = !r.negate;
259
+ }
260
+ if (ignored) return true;
261
+ }
262
+ return false;
263
+ };
264
+ }
265
+
266
+ function trackedIndex(files) {
267
+ const fileSet = new Set(files);
268
+ const dirSet = new Set(["."]);
269
+ for (const f of files) { let d = path.posix.dirname(f); while (d !== "." && !dirSet.has(d)) { dirSet.add(d); d = path.posix.dirname(d); } }
270
+ return { fileSet, dirSet };
271
+ }
272
+ /** What `value` (relative to `cwd`, both posix and repository-relative) is: "outside", "skip" (dynamic or produced) or its normalized path. */
273
+ function resolveInput(cwd, value, produced = [], ignored = () => false) {
274
+ if (DYNAMIC.test(value) || value === "" || value.startsWith("/")) return { state: "skip" };
275
+ const joined = path.posix.normalize(path.posix.join(cwd, value));
276
+ if (joined === ".." || joined.startsWith("../")) return { state: "outside", path: joined };
277
+ const rel = joined.replace(/\/$/, "") || ".";
278
+ if (rel.split("/").some((s) => PRODUCED_SEGMENT.test(s))) return { state: "skip" };
279
+ if (produced.some((p) => rel === p || rel.startsWith(`${p}/`))) return { state: "skip" };
280
+ if (rel !== "." && ignored(rel)) return { state: "skip" };
281
+ return { state: "check", path: rel };
282
+ }
283
+
284
+ function checkInputs({ file, line, cwd, commands, index, produced, ignored, add, where, unsure = () => null }) {
285
+ let dir = cwd;
286
+ for (const words of commands) {
287
+ for (const input of commandInputs(words)) {
288
+ if (input.kind === "cd") {
289
+ const r = resolveInput(dir, input.value, produced, ignored);
290
+ if (r.state === "check" && index.dirSet.has(r.path)) { dir = r.path; continue; }
291
+ const why = r.state === "check" ? unsure(r.path) : null;
292
+ if (why) add(`cd:${r.path}`, `${file}:${line} ${where} runs ${words.join(" ")}: ${r.path} is not in this commit; UNSUPPORTED: ${why}`, "produce the directory in an unconditional checkout or download-artifact step earlier in the same job, or commit it");
293
+ dir = null;
294
+ continue;
295
+ }
296
+ if (dir === null) continue;
297
+ const r = resolveInput(dir, input.value, produced, ignored);
298
+ if (r.state === "skip") continue;
299
+ if (r.state === "outside") {
300
+ add(`${input.kind}:${input.value}`, `${file}:${line} ${where} runs ${words.join(" ")}: ${input.value} resolves to ${r.path}, outside the repository, which no checkout of this commit has`, `commit the ${input.kind === "dir" ? "directory" : "file"} into this repository, or depend on the published package instead of a sibling checkout`);
301
+ continue;
302
+ }
303
+ const present = input.kind === "dir" ? index.dirSet.has(r.path) : index.fileSet.has(r.path) || index.dirSet.has(r.path);
304
+ if (!present) {
305
+ const why = unsure(r.path);
306
+ add(`${input.kind}:${r.path}`, `${file}:${line} ${where} runs ${words.join(" ")}: ${r.path} is not in this commit${why ? `; UNSUPPORTED: ${why}` : ""}`, why ? "produce it in an unconditional checkout or download-artifact step earlier in the same job, or commit it" : `commit ${r.path}, or point the command at the path that exists`);
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ function referencedInput({ root, files }) {
313
+ const out = [];
314
+ const index = trackedIndex(files.filter(scanned));
315
+ const ignored = gitignoreMatcher(root, files);
316
+ for (const file of files.filter((f) => /(^|\/)package\.json$/.test(f) && scanned(f) && !FIXTURE_TREE.test(f)).sort()) {
317
+ const text = readText(root, file);
318
+ let pkg;
319
+ try { pkg = JSON.parse(String(text ?? "").replace(/^/, "")); } catch { continue; }
320
+ const scripts = pkg && typeof pkg.scripts === "object" && !Array.isArray(pkg.scripts) ? pkg.scripts : {};
321
+ const dir = path.posix.dirname(file);
322
+ for (const [name, command] of Object.entries(scripts)) {
323
+ if (typeof command !== "string") continue;
324
+ const offset = Math.max(0, text.indexOf(`"${name}"`));
325
+ const add = (subject, problem, fix) => out.push({ file, subject: `script:${name}:${subject}`, problem, fix });
326
+ checkInputs({ file, line: lineOf(text, offset), cwd: dir, commands: shellCommands(command), index, produced: [], ignored, add, where: `script "${name}"` });
327
+ }
328
+ }
329
+ for (const file of workflowFiles(files)) {
330
+ const text = readText(root, file);
331
+ if (text === null) continue;
332
+ const entries = yamlEntries(text);
333
+ const { jobs, topDefaultsDir } = workflowJobs(entries);
334
+ // Directories a step checks out or downloads into exist only at run time. Only an unconditional actions/checkout or
335
+ // actions/download-artifact step earlier in the same job guarantees one; a step with `if:` or `continue-on-error`, an
336
+ // actions/cache restore (a miss restores nothing), a later step and another job's step do not, and an input under such a
337
+ // path is reported UNSUPPORTED instead of passing.
338
+ const producers = [];
339
+ for (const [job, { steps }] of jobs) steps.forEach((step, i) => {
340
+ const u = stepGet(step, "uses");
341
+ const m = u && /^actions\/(checkout|download-artifact|cache)(?:\/(restore))?@/.exec(u.value);
342
+ const p = stepGet(step, "with", "path");
343
+ if (!m || !p) return;
344
+ const coe = stepGet(step, "continue-on-error");
345
+ const conditional = stepGet(step, "if") ? "it has an if: condition" : coe && coe.value !== "false" ? "it has continue-on-error" : null;
346
+ for (const v of (p.block ? p.block.map((b) => b.text).filter(Boolean) : [p.value])) {
347
+ const r = resolveInput(".", v);
348
+ if (r.state === "check") producers.push({ job, i, line: u.line, kind: m[1], path: r.path, conditional });
349
+ }
350
+ });
351
+ const covers = (p, rel) => rel === p.path || rel.startsWith(`${p.path}/`);
352
+ const seen = new Set();
353
+ const add = (subject, problem, fix) => { if (!seen.has(subject)) { seen.add(subject); out.push({ file, subject, problem, fix }); } };
354
+ for (const [job, { steps, defaultsDir }] of jobs) {
355
+ const jobDir = defaultsDir ?? topDefaultsDir;
356
+ for (const [i, step] of steps.entries()) {
357
+ const guaranteed = (p) => p.job === job && p.i < i && p.kind !== "cache" && !p.conditional;
358
+ const produced = producers.filter(guaranteed).map((p) => p.path);
359
+ const unsure = (rel) => {
360
+ const p = producers.find((x) => !guaranteed(x) && covers(x, rel));
361
+ if (!p) return null;
362
+ const why = p.job !== job ? `it is in job ${p.job}, not job ${job}` : p.i >= i ? "it runs after this step" : p.kind === "cache" ? "a cache restore guarantees nothing (a miss restores no files)" : p.conditional;
363
+ return `only the actions/${p.kind} step at ${file}:${p.line} may produce ${p.path}, and ${why}`;
364
+ };
365
+ const uses = stepGet(step, "uses")?.value ?? "";
366
+ const wd = stepGet(step, "working-directory") ?? jobDir;
367
+ let cwd = ".";
368
+ if (wd) {
369
+ const r = resolveInput(".", wd.value, produced, ignored);
370
+ if (r.state === "outside") { add(`working-directory:${wd.value}`, `${file}:${wd.line} job ${job}: working-directory ${wd.value} is outside the repository`, "use a directory of this repository"); continue; }
371
+ if (r.state === "skip") continue;
372
+ if (!index.dirSet.has(r.path)) { const why = unsure(r.path); add(`working-directory:${r.path}`, `${file}:${wd.line} job ${job}: working-directory ${r.path} is not in this commit (the step fails before its command runs)${why ? `; UNSUPPORTED: ${why}` : ""}`, `commit ${r.path}, or point working-directory at the directory that exists`); continue; }
373
+ cwd = r.path;
374
+ }
375
+ const cache = stepGet(step, "with", "cache-dependency-path");
376
+ if (cache && /^actions\/setup-(node|python|go|java|dotnet)@/.test(uses)) {
377
+ const values = cache.block ? cache.block.filter((b) => b.text).map((b) => ({ v: b.text, line: b.line })) : [{ v: cache.value, line: cache.line }];
378
+ for (const { v, line } of values) {
379
+ if (v.startsWith("!")) continue;
380
+ const r = resolveInput(".", v, produced, ignored);
381
+ if (r.state === "outside") { add(`cache-dependency-path:${v}`, `${file}:${line} job ${job}: cache-dependency-path ${v} is outside the repository`, "name a lock file of this repository"); continue; }
382
+ if (r.state === "skip") {
383
+ if (!/[*?]/.test(v) || /[${}]/.test(v)) continue;
384
+ const glob = new RegExp(`^${v.replace(/[.+^()|[\]\\]/g, "\\$&").replace(/\*\*\/?/g, "\u0000").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?")}$`);
385
+ if (!files.some((f) => glob.test(f))) add(`cache-dependency-path:${v}`, `${file}:${line} job ${job}: cache-dependency-path ${v} matches no file of this commit (setup refuses: the dependency lock is not found)`, "point cache-dependency-path at the lock file that exists");
386
+ continue;
387
+ }
388
+ if (!index.fileSet.has(r.path)) { const why = unsure(r.path); add(`cache-dependency-path:${r.path}`, `${file}:${line} job ${job}: cache-dependency-path ${r.path} is not in this commit (setup refuses: the dependency lock is not found)${why ? `; UNSUPPORTED: ${why}` : ""}`, `commit ${r.path}, or point cache-dependency-path at the lock file that exists`); }
389
+ }
390
+ }
391
+ const run = stepGet(step, "run");
392
+ if (run) {
393
+ const lines = run.block ? run.block : [{ line: run.line, text: run.value }];
394
+ // One shell per step: a `cd` carries to the lines after it.
395
+ let dir = cwd;
396
+ for (const { line, text: l } of lines) {
397
+ const commands = shellCommands(l);
398
+ const cds = commands.flatMap(commandInputs).filter((x) => x.kind === "cd");
399
+ checkInputs({ file, line, cwd: dir, commands, index, produced, ignored, unsure, add: (s, p, f) => add(`run:${s}`, p, f), where: `job ${job} step` });
400
+ for (const cd of cds) { if (dir === null) break; const r = resolveInput(dir, cd.value, produced, ignored); dir = r.state === "check" && index.dirSet.has(r.path) ? r.path : null; }
401
+ if (dir === null) break;
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
407
+ return out;
408
+ }
409
+
410
+ // ---- CA-08: Caddyfile global options ---------------------------------------------------------------------------------------------
411
+ // HTTP handler and site directives: never a global option. Names that are both (log, metrics) and unknown names (a plugin's global
412
+ // option) are not judged.
413
+ const SITE_DIRECTIVES = new Set(["abort", "acme_server", "basic_auth", "basicauth", "bind", "encode", "error", "file_server", "forward_auth", "fs", "handle", "handle_errors", "handle_path", "header", "intercept", "invoke", "log_append", "log_name", "log_skip", "map", "method", "php_fastcgi", "push", "redir", "request_body", "request_header", "respond", "reverse_proxy", "rewrite", "root", "route", "skip_log", "templates", "tls", "tracing", "try_files", "uri", "vars"]);
414
+ const caddyFiles = (files) => files.filter((f) => /(^|\/)(Caddyfile(\.[^/]*)?|[^/]+\.caddyfile)$/i.test(f) && scanned(f) && !FIXTURE_TREE.test(f)).sort();
415
+
416
+ /** Top-level blocks of a Caddyfile: [{ header, line, directives: [{ name, line }] }] (directives at depth 1 only). */
417
+ export function caddyBlocks(text) {
418
+ const blocks = [];
419
+ let depth = 0;
420
+ let current = null;
421
+ String(text ?? "").replace(/\r\n?/g, "\n").split("\n").forEach((raw, i) => {
422
+ const tokens = [];
423
+ for (const m of raw.matchAll(/"(?:[^"\\]|\\.)*"|`[^`]*`|\S+/g)) { if (m[0].startsWith("#")) break; tokens.push(m[0]); }
424
+ if (!tokens.length) return;
425
+ if (depth === 0) {
426
+ if (tokens.at(-1) === "{") {
427
+ current = { header: tokens.slice(0, -1), line: i + 1, directives: [] };
428
+ blocks.push(current);
429
+ depth = 1;
430
+ }
431
+ return;
432
+ }
433
+ if (depth === 1 && tokens[0] !== "}" && tokens[0] !== "{") current.directives.push({ name: tokens[0], line: i + 1 });
434
+ for (const t of tokens) { if (t === "{") depth += 1; else if (t === "}") depth -= 1; }
435
+ if (depth <= 0) { depth = 0; current = null; }
436
+ });
437
+ return blocks;
438
+ }
439
+
440
+ function caddyGlobalOptions({ root, files }) {
441
+ const out = [];
442
+ for (const file of caddyFiles(files)) {
443
+ const text = readText(root, file);
444
+ if (text === null) continue;
445
+ const blocks = caddyBlocks(text);
446
+ const first = blocks[0];
447
+ if (!first) continue;
448
+ const site = first.directives.filter((d) => SITE_DIRECTIVES.has(d.name));
449
+ if (!site.length) continue;
450
+ if (first.header.length === 0) {
451
+ out.push({ file, subject: `global:${site[0].name}`, problem: `${file}:${site[0].line} \`${site[0].name}\` sits in the global options block (the first block, with no address): Caddy refuses it as an unrecognized global option`, fix: `move ${site.map((d) => d.name).join(", ")} into a site block (an address before its {)` });
452
+ continue;
453
+ }
454
+ // A site address spelled only with environment placeholders and no non-empty default: an empty variable leaves the block with
455
+ // no address, and the first block without one is the global options block.
456
+ const fragile = first.header.every((t) => /^\{\$[A-Za-z_][A-Za-z0-9_]*(?::)?\}$/.test(t.replace(/,$/, "")));
457
+ if (fragile) {
458
+ const vars = first.header.map((t) => /\{\$([A-Za-z0-9_]+)/.exec(t)[1]);
459
+ out.push({ file, subject: `address:${vars.join(",")}`, problem: `${file}:${first.line} the first block's address is only ${first.header.join(" ")}: with ${vars.join(", ")} unset or empty Caddy reads the block as global options and refuses \`${site[0].name}\` (line ${site[0].line}) as an unrecognized global option`, fix: `put a global options block ({ ... }, even empty) first, or give the placeholder a non-empty default ({$${vars[0]}:localhost})` });
460
+ }
461
+ }
462
+ return out;
463
+ }
464
+
465
+ export const CONSUMER_SOURCE_RULES = Object.freeze([
466
+ { id: "workflow-action-pair", gap: "CA-04", run: workflowActionPair },
467
+ { id: "referenced-input", gap: "CA-07", run: referencedInput },
468
+ { id: "caddy-global-options", gap: "CA-08", run: caddyGlobalOptions },
469
+ ]);
@@ -0,0 +1,42 @@
1
+ # RightKit's cargo-deny policy for an app that ships no deny.toml of its own (rightkit-gate class A).
2
+ # The licence allow list is RightKit's own (repository deny.toml); the app's own unpublished crates are private.
3
+ [graph]
4
+ all-features = true
5
+
6
+ [advisories]
7
+ version = 2
8
+ unmaintained = "workspace"
9
+ yanked = "deny"
10
+
11
+ [licenses]
12
+ allow = [
13
+ "MIT",
14
+ "MIT-0",
15
+ "Apache-2.0",
16
+ "Apache-2.0 WITH LLVM-exception",
17
+ "BSD-2-Clause",
18
+ "BSD-3-Clause",
19
+ "ISC",
20
+ "Zlib",
21
+ "BSL-1.0",
22
+ "CC0-1.0",
23
+ "Unicode-3.0",
24
+ "Unicode-DFS-2016",
25
+ "OFL-1.1",
26
+ "MPL-2.0",
27
+ "Unlicense",
28
+ "CDLA-Permissive-2.0",
29
+ ]
30
+ confidence-threshold = 0.9
31
+ unused-allowed-license = "allow"
32
+
33
+ [licenses.private]
34
+ ignore = true
35
+
36
+ [bans]
37
+ multiple-versions = "allow"
38
+
39
+ [sources]
40
+ unknown-registry = "deny"
41
+ unknown-git = "deny"
42
+ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
package/deps.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
- import { pathToFileURL } from "node:url";
3
+ import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
4
4
  import { spawnSync } from "./background-process.mjs";
5
5
  import { existsSync } from "node:fs";
6
6
  import { cargoAuditWorkdirs, runCargoAudits } from "./deps-cargo.mjs";
7
7
 
8
8
  const args = process.argv.slice(2);
9
9
  const opts = {
10
- config: "right-release.config.mjs",
10
+ config: defaultReleaseConfigFile(),
11
11
  mode: "check",
12
12
  latest: false,
13
13
  };
@@ -25,7 +25,8 @@ for (let i = 0; i < args.length; i++) {
25
25
  }
26
26
 
27
27
  const configPath = path.resolve(opts.config);
28
- const config = (await import(pathToFileURL(configPath))).default;
28
+ let config;
29
+ try { config = await importReleaseConfig(configPath); } catch (error) { fail(`release config could not be loaded (${configPath}): ${error instanceof Error ? error.message : String(error)}`); }
29
30
  if (config?.schema !== 1) fail(`unsupported config schema: ${config?.schema ?? "<missing>"} (expected 1)`);
30
31
  if (config.packageManager !== "pnpm") fail(`deps tool expects pnpm, got ${config.packageManager}`);
31
32
 
@@ -69,7 +70,7 @@ if (failures.length > 0) {
69
70
 
70
71
  function usage(code, message) {
71
72
  if (message) console.error(`right-deps: ${message}\n`);
72
- console.error("usage: node deps.mjs [--config right-release.config.mjs] [--check|--audit|--update] [--latest]");
73
+ console.error("usage: node deps.mjs [--config right-release.config.json] [--check|--audit|--update] [--latest]");
73
74
  process.exit(code);
74
75
  }
75
76