@rightkit/release 0.2.135 → 0.2.137

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 (81) hide show
  1. package/README.md +62 -0
  2. package/addon-command.mjs +53 -34
  3. package/apple-profile-renew.mjs +30 -12
  4. package/artifact-storage.mjs +47 -10
  5. package/background-process.mjs +18 -0
  6. package/build-release.mjs +1 -1
  7. package/cache-command.mjs +4 -3
  8. package/cargo-contract.mjs +1 -1
  9. package/cargo-guard.mjs +4 -3
  10. package/cargo-registry-readback.mjs +51 -0
  11. package/cargo-target.mjs +3 -1
  12. package/cli/right-release.mjs +26 -0
  13. package/codesign-environment.mjs +26 -2
  14. package/codesign-shim.mjs +2 -1
  15. package/committed-source.mjs +3 -3
  16. package/development.mjs +7 -7
  17. package/direct-bootstrap.mjs +19 -9
  18. package/dmg-build.mjs +117 -0
  19. package/dmg-layout.mjs +123 -0
  20. package/github-release.mjs +74 -37
  21. package/heavy-command.mjs +3 -2
  22. package/install-app.mjs +358 -0
  23. package/inventory-check.mjs +980 -0
  24. package/macos-signing-session.mjs +3 -0
  25. package/managed-cargo.mjs +23 -19
  26. package/managed-host-secrets.mjs +30 -0
  27. package/managed-roots.mjs +39 -0
  28. package/model-promote.mjs +31 -37
  29. package/notary-auth.mjs +4 -0
  30. package/osascript-guard.mjs +24 -0
  31. package/ownership-approval-command.mjs +15 -4
  32. package/ownership-approval.mjs +114 -49
  33. package/ownership-availability.mjs +25 -0
  34. package/ownership-catalog-config.json +18 -0
  35. package/ownership-catalog.mjs +94 -0
  36. package/ownership-command.mjs +20 -10
  37. package/ownership-declaration.json +15 -0
  38. package/ownership-default-policy.json +1 -11
  39. package/ownership-evidence.mjs +10 -8
  40. package/ownership-git-inventory.mjs +29 -1
  41. package/ownership-helper-paths.json +2 -2
  42. package/ownership-hooks.mjs +107 -0
  43. package/ownership-internal-owners.json +44 -0
  44. package/ownership-key-authority.mjs +112 -7
  45. package/ownership-key-command.mjs +76 -19
  46. package/ownership-key-trust.mjs +97 -9
  47. package/ownership-lockfiles.mjs +311 -0
  48. package/ownership-report-command.mjs +12 -3
  49. package/ownership-report.mjs +20 -17
  50. package/ownership-repositories.json +19 -0
  51. package/ownership-scan-worker.mjs +96 -11
  52. package/ownership-source.mjs +39 -0
  53. package/ownership-staged.mjs +193 -0
  54. package/ownership-verdict.mjs +74 -0
  55. package/ownership.mjs +215 -107
  56. package/package.json +1 -1
  57. package/protected-credentials.mjs +111 -0
  58. package/prune-r2.mjs +4 -3
  59. package/pub-upload.mjs +3 -85
  60. package/publication-artifacts.mjs +108 -0
  61. package/publication-gate.mjs +28 -7
  62. package/publish-cargo.mjs +47 -16
  63. package/publish-swift.mjs +46 -17
  64. package/publish-update.mjs +61 -39
  65. package/r2-verified-upload.mjs +41 -0
  66. package/registry-parity.mjs +5 -3
  67. package/release-state.mjs +1 -0
  68. package/release-token.mjs +2 -0
  69. package/rightkit-drift.mjs +3 -2
  70. package/rightkit-ownership.json +2028 -139
  71. package/rightkit-signing-agent.mjs +5 -3
  72. package/rightkit-sync.mjs +6 -2
  73. package/rightkit-versions.json +215 -103
  74. package/runtime-artifact-manifest.mjs +20 -6
  75. package/sign-updater.mjs +5 -0
  76. package/signing-agent-client.mjs +2 -1
  77. package/signing-agent-install.mjs +3 -2
  78. package/standalone-clone-verify.mjs +3 -2
  79. package/tauri-bundle-marker.mjs +3 -2
  80. package/upload-large.mjs +36 -82
  81. package/verifier.mjs +5 -1
package/ownership.mjs CHANGED
@@ -1,7 +1,11 @@
1
1
  // RightKit ownership scan. Public exports retain the original findings-array API.
2
+ import { isBlockingFinding } from "./ownership-verdict.mjs";
2
3
  import { spawnSync } from "./background-process.mjs";
3
- import { canonicalRepositoryIdentity, loadTrustedExternal, occurrenceHash, resolveKeyLogTrust, sha256, verifyException, verifyProtectedProvenance } from "./ownership-approval.mjs";
4
+ import { canonicalRepositoryIdentity, loadTrustedExternal, occurrenceHash, policyEntryDigest, resolveKeyLogTrust, sha256, verifyException, verifyProtectedProvenance } from "./ownership-approval.mjs";
5
+ import installedManifest from "./rightkit-ownership.json" with { type: "json" };
4
6
  import { parse as parseToml } from "smol-toml";
7
+ import { sourceNonCode } from "./ownership-source.mjs";
8
+ import { OWNERSHIP_LOCKFILES, transitiveDependencies } from "./ownership-lockfiles.mjs";
5
9
  import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
6
10
  import { createHash } from "node:crypto";
7
11
  import { acquireCommittedSource, committedSourceDataForInternalUse } from "./committed-source.mjs";
@@ -15,7 +19,7 @@ export const EXCEPTIONS_FILE = path.join(".rightkit", "ownership-exceptions.json
15
19
  const CODE_EXT = new Set([".rs", ".ts", ".tsx", ".js", ".mjs", ".cjs", ".jsx", ".swift", ".m", ".mm", ".c", ".cc", ".cpp", ".h", ".py", ".go", ".kt", ".java", ".cs"]);
16
20
  // Listed installed/build inputs retain explicit coverage diagnostics.
17
21
  const OPAQUE_DIRS = new Set(["target", ".build", "DerivedData", "Pods"]);
18
- export const SCANNER_VERSION = "2.4.0";
22
+ export const SCANNER_VERSION = "2.6.0";
19
23
  export const catalogDigest = (manifest) => sha256(manifest);
20
24
  const byteDigest = (bytes) => createHash("sha256").update(bytes).digest("hex");
21
25
  const inputDigest = (file) => file.digest ?? byteDigest(file.bytes);
@@ -40,57 +44,24 @@ function sourceMatchers(manifest) {
40
44
  return matchers;
41
45
  }
42
46
 
43
- // Code-only patterns may consume string arguments, but must start outside
44
- // literals/comments. Keep offsets/newlines intact for multiline call reporting.
45
- function sourceNonCode(text, rust) {
46
- const nonCode = new Uint8Array(text.length);
47
- for (let i = 0; i < text.length;) {
48
- const start = i;
49
- if (text.startsWith("//", i) || text.startsWith("/*", i)) {
50
- if (text[i + 1] === "/") {
51
- i = text.indexOf("\n", i + 2);
52
- if (i === -1) i = text.length;
53
- } else {
54
- i += 2;
55
- let depth = 1;
56
- while (i < text.length && depth) {
57
- if (rust && text.startsWith("/*", i)) { depth++; i += 2; }
58
- else if (text.startsWith("*/", i)) { depth--; i += 2; }
59
- else i++;
60
- }
61
- }
62
- nonCode.fill(1, start, i);
63
- continue;
64
- }
65
- const raw = rust && (i === 0 || !/[\w]/.test(text[i - 1])) && /^(?:br|cr|r)(#*)"/.exec(text.slice(i));
66
- if (raw) {
67
- const closing = `"${raw[1]}`, end = text.indexOf(closing, i + raw[0].length);
68
- i = end === -1 ? text.length : end + closing.length;
69
- } else if (text[i] === '"' || (!rust && (text[i] === "'" || text[i] === "`")) ||
70
- (rust && text[i] === "'" && /^'(?:\\(?:u\{[\da-fA-F]+\}|x[\da-fA-F]{2}|[^\r\n])|[^'\\\r\n])'/.test(text.slice(i)))) {
71
- const quote = text[i++];
72
- while (i < text.length) {
73
- if (text[i] === "\\") i = Math.min(i + 2, text.length);
74
- else if (text[i++] === quote) break;
75
- }
76
- } else { i++; continue; }
77
- nonCode.fill(1, start, i);
78
- }
79
- return nonCode;
80
- }
81
-
82
47
  function sourceFindings(file, matchers) {
83
48
  const ext = path.posix.extname(file.rel).toLowerCase();
84
49
  const key = `${matchers.digest}:${ext}:${file.cacheKey ?? inputDigest(file)}`;
85
50
  if (sourceCache.has(key)) return sourceCache.get(key);
86
51
  const text = file.bytes.toString("utf8"), findings = [];
52
+ const ordinals = new Map();
53
+ const push = (c, f, line, lineText) => {
54
+ const identity = `${f.id}\0${lineText}`, ordinal = ordinals.get(identity) ?? 0;
55
+ ordinals.set(identity, ordinal + 1);
56
+ findings.push({ fingerprint: f.id, capability: c.capability, package: c.package, ...findingPolicy(c, f), kind: "source", line, lineText, ordinal, text: lineText.trim().slice(0, 160) });
57
+ };
87
58
  if (matchers.prefilter.test(text)) {
88
59
  const lines = text.split(/\r?\n/);
89
60
  const applicable = matchers.sources.filter(({ f }) => !f.extensions || f.extensions.includes(ext));
90
61
  const lineMatchers = applicable.filter(({ f }) => !f.codeOnly);
91
62
  for (const [i, line] of lines.entries()) if (!commentOnly(line)) for (const { c, f, re } of lineMatchers) {
92
63
  re.lastIndex = 0;
93
- if (re.test(line)) findings.push({ fingerprint: f.id, capability: c.capability, package: c.package, line: i + 1, text: line.trim().slice(0, 160) });
64
+ if (re.test(line)) push(c, f, i + 1, line);
94
65
  }
95
66
  const codeMatchers = applicable.filter(({ f }) => f.codeOnly);
96
67
  if (codeMatchers.length) {
@@ -104,7 +75,7 @@ function sourceFindings(file, matchers) {
104
75
  for (; offset < match.index; offset++) if (text[offset] === "\n") line++;
105
76
  if (reported.has(line)) continue;
106
77
  reported.add(line);
107
- findings.push({ fingerprint: f.id, capability: c.capability, package: c.package, line, text: lines[line - 1].trim().slice(0, 160) });
78
+ push(c, f, line, lines[line - 1]);
108
79
  }
109
80
  }
110
81
  }
@@ -121,7 +92,8 @@ export function aggregateOwnership(rightkitRoot) {
121
92
  if (!existsSync(dir)) continue;
122
93
  for (const name of readdirSync(dir).sort()) {
123
94
  const child = path.join(dir, name);
124
- let file = path.join(child, MANIFEST_FILE);
95
+ let file = path.join(child, "ownership-declaration.json");
96
+ if (!existsSync(file)) file = path.join(child, MANIFEST_FILE);
125
97
  if (!existsSync(file) && group === "swift") file = path.join(child, "rightkit-ownership.json");
126
98
  if (!existsSync(file)) continue;
127
99
  const declared = JSON.parse(readFileSync(file, "utf8"));
@@ -129,7 +101,9 @@ export function aggregateOwnership(rightkitRoot) {
129
101
  for (const capability of declared.capabilities ?? []) entries.push({ package: declared.package, ...capability });
130
102
  }
131
103
  }
132
- const manifest = { schema: 1, version: "1", capabilities: entries };
104
+ const internalFile = path.join(rightkitRoot, "packages/release/ownership-internal-owners.json");
105
+ const internalOwners = existsSync(internalFile) ? JSON.parse(readFileSync(internalFile, "utf8")).owners : [];
106
+ const manifest = { schema: 1, version: "2", capabilities: entries, internalOwners };
133
107
  validateManifest(manifest);
134
108
  return manifest;
135
109
  }
@@ -137,7 +111,7 @@ export function aggregateOwnership(rightkitRoot) {
137
111
  export function validateManifest(manifest) {
138
112
  const ids = new Set();
139
113
  for (const c of manifest.capabilities ?? []) {
140
- if (!c.package || !c.capability || !Array.isArray(c.fingerprints) || !c.fingerprints.length) throw new Error(`ownership: incomplete capability ${JSON.stringify(c).slice(0, 120)}`);
114
+ if (!c.package || !c.capability || !Array.isArray(c.fingerprints)) throw new Error(`ownership: incomplete capability ${JSON.stringify(c).slice(0, 120)}`);
141
115
  for (const f of c.fingerprints) {
142
116
  if (!f.id || ids.has(f.id)) throw new Error(`ownership: missing or duplicate fingerprint id ${f.id}`);
143
117
  ids.add(f.id);
@@ -177,18 +151,25 @@ function parseCargo(parsed, file, out, workspace, root, coverage, inventory) {
177
151
  const pathSpec = (pathValue, sourceFile, display) => {
178
152
  resolveLocalPath(root, sourceFile, pathValue, coverage, display, inventory);
179
153
  };
180
- const addTable = (table) => {
154
+ const addTable = (table, prefix = "") => {
181
155
  if (!table || typeof table !== "object") return;
182
156
  for (const section of ["dependencies", "dev-dependencies", "build-dependencies"]) for (const [alias, raw] of Object.entries(table[section] ?? {})) {
183
157
  const spec = raw && typeof raw === "object" ? raw : {};
184
- const inherited = spec.workspace === true ? workspace.deps?.[alias] ?? {} : {};
158
+ const inheritedRaw = spec.workspace === true ? workspace.deps?.[alias] : undefined;
159
+ if (spec.workspace === true && inheritedRaw === undefined) throw new Error(`missing workspace dependency: ${alias}`);
160
+ const inherited = typeof inheritedRaw === "string" ? { version: inheritedRaw } : inheritedRaw ?? {};
185
161
  const name = spec.package ?? inherited.package ?? alias;
186
- const source = spec.path ?? inherited.path ? "path" : spec.git ?? inherited.git ? "git" : undefined;
187
- addDependency(out, "cargo", name, file, { local: source === "path" || source === "git", source });
188
- if (source === "path") pathSpec(spec.path ?? inherited.path, spec.path ? file : path.posix.join(workspace.dir ?? ".", "Cargo.toml"), file);
189
- if (source === "git") coverage.push({ path: file, status: "unscanned", reason: `git-dependency-source:${name}` });
162
+ const pathValue = spec.path ?? inherited.path, git = spec.git ?? inherited.git, selector = spec.rev ?? inherited.rev ?? spec.branch ?? inherited.branch ?? spec.tag ?? inherited.tag;
163
+ const source = pathValue ? `path:${pathValue}` : git ? `git+${git}${selector ? `#${selector}` : ""}` : "registry";
164
+ const requirement = typeof raw === "string" ? raw : spec.version ?? inherited.version ?? null;
165
+ if (spec.features !== undefined && !Array.isArray(spec.features) || inherited.features !== undefined && !Array.isArray(inherited.features)) throw new Error(`malformed dependency features: ${alias}`);
166
+ const features = [...new Set([...(inherited.features ?? []), ...(spec.features ?? [])])].sort();
167
+ if (!raw || typeof raw !== "string" && (typeof raw !== "object" || Array.isArray(raw)) || features.some((x) => typeof x !== "string") || requirement !== null && typeof requirement !== "string") throw new Error(`malformed dependency declaration: ${alias}`);
168
+ addDependency(out, "cargo", name, file, { alias: name === alias ? null : alias, section: `${prefix}${section}`, requirement, features, defaultFeatures: spec["default-features"] ?? inherited["default-features"] ?? null, source, resolvedVersion: null, local: !!pathValue || !!git });
169
+ if (pathValue) pathSpec(pathValue, spec.path ? file : path.posix.join(workspace.dir ?? ".", "Cargo.toml"), file);
170
+ if (git) coverage.push({ path: file, status: "unscanned", reason: `git-dependency-source:${name}` });
190
171
  }
191
- for (const target of Object.values(table.target ?? {})) addTable(target);
172
+ for (const [target, value] of Object.entries(table.target ?? {})) addTable(value, `${prefix}target.'${target}'.`);
192
173
  };
193
174
  addTable(parsed);
194
175
  }
@@ -211,7 +192,7 @@ function fileInventory(appRoot, trust, pathPrefix = "", repositoryRoot = appRoot
211
192
  const inventory = gitListedInventory(appRoot, {
212
193
  repositoryRoot,
213
194
  protectedPaths: [...protectedPaths],
214
- relevant: (rel) => ["Cargo.toml", "package.json", "Package.swift"].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase()),
195
+ relevant: (rel) => ["Cargo.toml", "package.json", "Package.swift", ...OWNERSHIP_LOCKFILES].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase()),
215
196
  maxFileBytes: (rel) => CODE_EXT.has(path.posix.extname(rel).toLowerCase()) || protectedPaths.has(rel) ? 2 * 1024 * 1024 : 64 * 1024 * 1024,
216
197
  });
217
198
  for (const item of inventory.coverage) if (item.reason === "file-over-size-limit") item.reason = protectedPaths.has(item.path) ? "protected-provenance-file-over-2MiB" : CODE_EXT.has(path.posix.extname(item.path).toLowerCase()) ? "source-file-over-2MiB" : "manifest-file-over-64MiB";
@@ -231,7 +212,7 @@ function committedFileInventory(appRoot, trust, pathPrefix, committed) {
231
212
  if (entry.mode === "120000") { coverage.push({ path: rel, status: "unscanned", reason: "committed-symlink" }); continue; }
232
213
  if (entry.mode === "160000" || entry.type === "commit") { coverage.push({ path: rel, status: "unscanned", reason: "committed-gitlink" }); continue; }
233
214
  if (entry.type !== "blob") { coverage.push({ path: rel, status: "unscanned", reason: "committed-unsupported-entry" }); continue; }
234
- const relevant = protectedPaths.has(entry.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json"].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase());
215
+ const relevant = protectedPaths.has(entry.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json", ...OWNERSHIP_LOCKFILES].includes(path.posix.basename(rel)) || CODE_EXT.has(path.posix.extname(rel).toLowerCase());
235
216
  const bytes = relevant ? entry.bytes : undefined;
236
217
  if (relevant && !bytes) coverage.push({ path: rel, status: "unscanned", reason: "committed-blob-unavailable" });
237
218
  const isExceptionFile = path.posix.basename(rel) === "ownership-exceptions.json";
@@ -253,6 +234,113 @@ function committedFileInventory(appRoot, trust, pathPrefix, committed) {
253
234
  return { files, coverage, committed: true, pathPrefix, repositoryFiles, snapshotFiles: new Map(repositoryFiles.map((file) => [file.rel, file])) };
254
235
  }
255
236
 
237
+ function swiftPackageArguments(text) {
238
+ const args = [];
239
+ for (const match of text.matchAll(/\.package\s*\(/g)) {
240
+ let at = match.index + match[0].length, start = at, depth = 1, quote = false;
241
+ for (; at < text.length && depth; at++) {
242
+ if (text[at] === "\\" && quote) { at++; continue; }
243
+ if (text[at] === '"') quote = !quote;
244
+ if (!quote && text[at] === "(") depth++;
245
+ if (!quote && text[at] === ")") depth--;
246
+ }
247
+ if (depth) throw new Error("unterminated Swift package declaration");
248
+ args.push(text.slice(start, at - 1));
249
+ }
250
+ return args;
251
+ }
252
+ function resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDigests) {
253
+ const cache = new Map();
254
+ const adjacent = (file, name) => {
255
+ const rel = path.posix.join(path.posix.dirname(file), name);
256
+ if (cache.has(rel)) return cache.get(rel);
257
+ let bytes;
258
+ if (inventory.committed) {
259
+ const item = inventory.snapshotFiles.get(`${inventory.pathPrefix ?? ""}${rel}`);
260
+ if (item && !item.bytes) throw new Error(`lockfile bytes unavailable: ${rel}`);
261
+ bytes = item?.bytes;
262
+ } else {
263
+ const inventoried = OWNERSHIP_LOCKFILES.includes(name) ? inventory.files.find((item) => item.rel === rel) : undefined;
264
+ if (inventoried && !inventoried.bytes) throw new Error(`lockfile bytes unavailable: ${rel}`);
265
+ const absolute = path.resolve(appRoot, ...rel.split("/"));
266
+ if (inventoried) bytes = inventoried.bytes;
267
+ else if (existsSync(absolute)) {
268
+ const root = realpathSync(appRoot), real = realpathSync(absolute);
269
+ if (!real.startsWith(`${root}${path.sep}`)) throw new Error(`lockfile outside scan root: ${rel}`);
270
+ if (!lstatSync(absolute).isFile() || lstatSync(absolute).size > 64 * 1024 * 1024) throw new Error(`lockfile unsupported or too large: ${rel}`);
271
+ bytes = readFileSync(absolute);
272
+ }
273
+ }
274
+ const text = bytes?.toString("utf8");
275
+ cache.set(rel, text);
276
+ if (bytes) contentDigests.push({ path: rel, digest: byteDigest(bytes) });
277
+ return text;
278
+ };
279
+ for (const d of deps) {
280
+ try {
281
+ if (d.ecosystem === "cargo") {
282
+ const text = adjacent(d.file, "Cargo.lock");
283
+ if (text !== undefined) {
284
+ const lock = parseToml(text);
285
+ if (!Array.isArray(lock.package)) throw new Error("Cargo.lock package list missing");
286
+ let candidates = lock.package.filter((p) => p.name === d.name && (d.source === "registry" ? String(p.source ?? "").startsWith("registry+") : d.source.startsWith("git+") ? String(p.source ?? "").split("#")[0].split("?")[0] === d.source.split("#")[0] : !p.source));
287
+ if (candidates.length > 1) {
288
+ const manifestText = inventory.files.find((f) => f.rel === d.file)?.bytes?.toString("utf8");
289
+ const owner = manifestText && parseToml(manifestText).package?.name;
290
+ const refs = lock.package.filter((p) => p.name === owner).flatMap((p) => p.dependencies ?? []);
291
+ candidates = candidates.filter((p) => refs.some((ref) => ref === `${d.name} ${p.version}` || ref.startsWith(`${d.name} ${p.version} (`)));
292
+ }
293
+ if (candidates.length !== 1 || typeof candidates[0].version !== "string") throw new Error(`Cargo.lock resolution missing or ambiguous: ${d.name}`);
294
+ d.resolvedVersion = candidates[0].version;
295
+ }
296
+ } else if (d.ecosystem === "npm") {
297
+ const text = adjacent(d.file, "npm-shrinkwrap.json") ?? adjacent(d.file, "package-lock.json");
298
+ if (text !== undefined) {
299
+ const lock = JSON.parse(text), name = d.alias ?? d.name;
300
+ const item = lock.packages?.[`node_modules/${name}`] ?? lock.dependencies?.[name];
301
+ if (typeof item?.version !== "string") throw new Error(`npm lock resolution missing: ${name}`);
302
+ d.resolvedVersion = item.version;
303
+ } else {
304
+ const pnpm = adjacent(d.file, "pnpm-lock.yaml"), yarn = pnpm === undefined ? adjacent(d.file, "yarn.lock") : undefined;
305
+ if (pnpm !== undefined) {
306
+ let section, name, dependencyIndent, inRoot = !/^importers:/m.test(pnpm), rootIndent;
307
+ for (const line of pnpm.split(/\r?\n/)) {
308
+ const entry = /^(\s*)(?:'([^']+)'|"([^"]+)"|([^:]+)):\s*(.*?)\s*$/.exec(line);
309
+ if (!entry) continue;
310
+ const indent = entry[1].length, key = entry[2] ?? entry[3] ?? entry[4], value = entry[5];
311
+ if (key === "." && indent === 2) { inRoot = true; rootIndent = 2; continue; }
312
+ if (rootIndent !== undefined && indent <= rootIndent) inRoot = false;
313
+ if (!inRoot) continue;
314
+ if (["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"].includes(key)) { section = key; dependencyIndent = indent + 2; name = undefined; continue; }
315
+ if (indent === dependencyIndent) { name = key; if (name === (d.alias ?? d.name) && section === d.section && value) d.resolvedVersion = value.replace(/^['"]|['"]$/g, "").split("(")[0]; }
316
+ if (key === "version" && name === (d.alias ?? d.name) && section === d.section && indent === dependencyIndent + 2) d.resolvedVersion = value.replace(/^['"]|['"]$/g, "").split("(")[0].replace(/^.*@(?=\d)/, "");
317
+ }
318
+ if (!d.resolvedVersion) throw new Error(`pnpm lock resolution missing: ${d.alias ?? d.name}`);
319
+ } else if (yarn !== undefined) {
320
+ let selected = false;
321
+ for (const line of yarn.split(/\r?\n/)) {
322
+ if (/^\S/.test(line) && /:$/.test(line)) selected = line.includes(`${d.alias ?? d.name}@${d.requirement}`) || line.includes(`${d.alias ?? d.name}@npm:${d.requirement}`);
323
+ const version = /^\s+version(?::|\s)\s*["']?([^\s"']+)/.exec(line);
324
+ if (selected && version) d.resolvedVersion = version[1];
325
+ }
326
+ if (!d.resolvedVersion) throw new Error(`yarn lock resolution missing: ${d.alias ?? d.name}`);
327
+ }
328
+ }
329
+ } else if (d.ecosystem === "swiftpm") {
330
+ const text = adjacent(d.file, "Package.resolved");
331
+ if (text !== undefined && !d.local) {
332
+ const lock = JSON.parse(text), pins = lock.pins ?? lock.object?.pins;
333
+ if (!Array.isArray(pins)) throw new Error("Swift lock pins missing");
334
+ const pin = pins.filter((p) => (p.identity ?? p.package)?.toLowerCase() === d.name.toLowerCase() || p.location === d.source.slice(4).split("#")[0] || p.repositoryURL === d.source.slice(4).split("#")[0]);
335
+ if (pin.length !== 1) throw new Error(`Swift lock resolution missing or ambiguous: ${d.name}`);
336
+ d.resolvedVersion = pin[0].state?.version ?? null;
337
+ if (d.resolvedVersion !== null && typeof d.resolvedVersion !== "string") throw new Error(`invalid Swift resolved version: ${d.name}`);
338
+ }
339
+ }
340
+ } catch (error) { coverage.push({ path: d.file, status: "unscanned", reason: `dependency-lock-resolution:${error.message}` }); }
341
+ }
342
+ }
343
+
256
344
  function dependenciesFromInventory(appRoot, inventory) {
257
345
  const deps = [], coverage = [], contentDigests = [];
258
346
  // Workspace inheritance is loaded before member manifests.
@@ -304,29 +392,35 @@ function dependenciesFromInventory(appRoot, inventory) {
304
392
  else if (base === "package.json") {
305
393
  let pkg; try { pkg = JSON.parse(text); } catch { coverage.push({ path: rel, status: "unscanned", reason: "invalid-json" }); continue; }
306
394
  for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) for (const [name, spec] of Object.entries(pkg[field] ?? {})) {
307
- if (typeof spec !== "string") continue;
395
+ if (typeof spec !== "string") { coverage.push({ path: rel, status: "unscanned", reason: `invalid-npm-dependency:${name}` }); continue; }
308
396
  const alias = spec.match(/^(?:npm:)(@[^/]+\/[^@]+|[^@]+)(?:@|$)/)?.[1];
309
397
  const localPath = spec.match(/^(?:file:|link:)(.+)$/)?.[1];
310
398
  const isGit = /^(git\+|https?:.*\.git)/i.test(spec);
311
399
  const devOnly = field === "devDependencies" && !["dependencies", "optionalDependencies", "peerDependencies"].some((section) => Object.hasOwn(pkg[section] ?? {}, name));
312
- addDependency(deps, "npm", alias ?? name, rel, { alias: alias ? name : undefined, local: !!localPath, source: localPath ? spec.split(":")[0] : isGit ? "git" : spec.startsWith("workspace:") ? "workspace" : undefined, ...(devOnly ? { devOnly: true } : {}) });
400
+ addDependency(deps, "npm", alias ?? name, rel, { alias: alias ? name : null, section: field, requirement: spec, features: [], defaultFeatures: null, resolvedVersion: null, local: !!localPath, source: localPath ? `path:${localPath}` : isGit ? spec.startsWith("git+") ? spec : `git+${spec}` : spec.startsWith("workspace:") ? `path:${spec}` : "registry", ...(devOnly ? { devOnly: true } : {}) });
313
401
  if (localPath) {
314
402
  resolveLocalPath(appRoot, rel, localPath, coverage, rel, inventory, "local-npm-dependency", name);
315
403
  } else if (isGit) coverage.push({ path: rel, status: "unscanned", reason: `git-npm-dependency-source:${name}` });
316
404
  }
317
405
  } else {
318
- for (const m of text.matchAll(/\.package\s*\(([^)]*)\)/g)) {
319
- const arg = m[1], url = arg.match(/url:\s*["']([^"']+)["']/)?.[1], pathDep = arg.match(/path:\s*["']([^"']+)["']/)?.[1];
320
- const id = arg.match(/name:\s*["']([^"']+)["']/)?.[1] ?? url?.replace(/\.git$/i, "").split("/").pop()?.toLowerCase() ?? pathDep?.split(/[\\/]/).pop();
321
- addDependency(deps, "swiftpm", id, rel, { local: !!pathDep, source: pathDep ? "path" : url });
406
+ for (const arg of swiftPackageArguments(text)) {
407
+ const url = arg.match(/url:\s*["']([^"']+)["']/)?.[1], pathDep = arg.match(/path:\s*["']([^"']+)["']/)?.[1], registry = arg.match(/id:\s*["']([^"']+)["']/)?.[1];
408
+ const explicitName = arg.match(/name:\s*["']([^"']+)["']/)?.[1];
409
+ const id = explicitName ?? registry ?? url?.replace(/\.git$/i, "").split("/").pop()?.toLowerCase() ?? pathDep?.split(/[\\/]/).pop();
410
+ if (!id || !url && !pathDep && !registry) throw new Error("unsupported Swift package identity or source");
411
+ const selector = arg.match(/(?:branch|revision):\s*["']([^"']+)["']/)?.[1];
412
+ const requirement = arg.replace(/(?:name|url|path|id):\s*"(?:\\.|[^"\\])*"\s*,?\s*/g, "").trim() || null;
413
+ addDependency(deps, "swiftpm", id, rel, { alias: null, section: "dependencies", requirement, features: [], defaultFeatures: null, resolvedVersion: null, local: !!pathDep, source: pathDep ? `path:${pathDep}` : registry ? "registry" : `git+${url}${selector ? `#${selector}` : ""}` });
322
414
  if (pathDep) {
323
415
  resolveLocalPath(appRoot, rel, pathDep, coverage, rel, inventory, "local-swift-dependency", id);
324
416
  }
325
417
  }
326
418
  }
327
419
  }
420
+ resolveDependencyVersions(appRoot, inventory, deps, coverage, contentDigests);
328
421
  Object.defineProperty(deps, "coverage", { value: coverage, enumerable: false });
329
422
  Object.defineProperty(deps, "contentDigests", { value: contentDigests, enumerable: false });
423
+ Object.defineProperty(deps, "manifests", { value: files, enumerable: false });
330
424
  return deps;
331
425
  }
332
426
 
@@ -419,31 +513,23 @@ function trustedProvenance(appRoot, inventory, trust, repositoryIdentity, covera
419
513
  return { excluded, classifications };
420
514
  }
421
515
 
422
- function npmImportUsage(inventory) {
423
- const usage = new Map(), qaSegments = new Set(["test", "tests", "__tests__", "e2e", "qa", "fixtures"]);
424
- for (const { rel, bytes } of inventory.files) {
425
- if (!CODE_EXT.has(path.posix.extname(rel).toLowerCase()) || !bytes) continue;
426
- const qaPath = rel.split("/").some((segment) => qaSegments.has(segment)) || /(?:\.(?:test|spec)\.|^qa-.*\.)/.test(path.posix.basename(rel));
427
- for (const match of bytes.toString("utf8").matchAll(/\bimport\b\s*(?:[\w$*{},\s]+?\bfrom\s*)?["']([^"']+)["']|\b(?:import|require)\s*\(\s*["']([^"']+)["']/g)) {
428
- const specifier = match[1] ?? match[2];
429
- const name = specifier.split("/").slice(0, specifier.startsWith("@") ? 2 : 1).join("/");
430
- usage.set(name, (usage.get(name) ?? true) && qaPath);
431
- }
432
- }
433
- return usage;
434
- }
435
-
436
516
  function scanInternal(appRoot, manifest, { inventory = fileInventory(appRoot), trust, repositoryIdentity, repositoryRoot = appRoot, pathPrefix = "" } = {}) {
437
517
  const findings = [], coverage = [...inventory.coverage], deps = dependenciesFromInventory(appRoot, inventory), contentDigests = [...(deps.contentDigests ?? [])];
518
+ const catalogOwners = new Set(manifest.capabilities.map((entry) => entry.package));
438
519
  coverage.push(...(deps.coverage ?? []));
439
520
  const provenance = trustedProvenance(appRoot, inventory, trust, repositoryIdentity, coverage, repositoryRoot, pathPrefix);
440
- const npmUsage = npmImportUsage(inventory);
521
+ const pushDependency = (c, f, d, kind = "dependency") => findings.push({ fingerprint: f.id, capability: c.capability, package: c.package, ...findingPolicy(c, f), file: d.file, line: 0, kind, ecosystem: d.ecosystem, name: d.name,
522
+ ...(kind === "dependency" ? { alias: d.alias, section: d.section, requirement: d.requirement, features: d.features, defaultFeatures: d.defaultFeatures, local: d.local } : { lockfile: d.lockfile, dependencyPath: d.dependencyPath }),
523
+ source: d.source, resolvedVersion: d.resolvedVersion, text: `${d.ecosystem} dependency ${d.alias ? `${d.alias} aliases ` : ""}${d.name}`, provenance: d.source });
441
524
  for (const c of manifest.capabilities) for (const f of c.fingerprints.filter((x) => x.type === "dependency")) for (const d of deps) {
442
- if (d.ecosystem === f.ecosystem && (d.name === f.name || d.alias === f.name)) {
443
- const qaOnly = c.capability !== "desktop-qa" && d.ecosystem === "npm" && d.devOnly && npmUsage.get(d.alias ?? d.name) === true;
444
- findings.push({ fingerprint: f.id, capability: c.capability, package: c.package, file: d.file, line: 0, text: `${d.ecosystem} dependency ${d.alias ? `${d.alias} aliases ` : ""}${d.name}`, local: d.local, provenance: d.source, ...(qaOnly ? { qaOnly: true } : {}) });
525
+ if (d.ecosystem === f.ecosystem && (d.name === f.name || d.alias === f.name) && !catalogOwners.has(d.name)) {
526
+ pushDependency(c, f, d);
445
527
  }
446
528
  }
529
+ const blockRules = manifest.capabilities.flatMap((c) => c.fingerprints.filter((f) => f.type === "dependency" && findingPolicy(c, f).tier === "block").map((f) => ({ c, f })));
530
+ const transitiveOwners = new Set([...catalogOwners, ...(manifest.internalOwners ?? []).map((owner) => typeof owner === "string" ? owner : owner.package)]);
531
+ const transitive = transitiveDependencies(appRoot, inventory, deps.manifests, transitiveOwners, coverage, contentDigests, blockRules.map(({ f }) => `${f.ecosystem}:${f.name}`));
532
+ for (const { c, f } of blockRules) for (const d of transitive) if (d.ecosystem === f.ecosystem && d.name === f.name) pushDependency(c, f, d, "transitive-dependency");
447
533
  const matchers = sourceMatchers(manifest);
448
534
  for (const file of inventory.files) {
449
535
  const { rel, size, bytes } = file;
@@ -458,7 +544,7 @@ function scanInternal(appRoot, manifest, { inventory = fileInventory(appRoot), t
458
544
  }
459
545
  if (pathPrefix) {
460
546
  const prefix = (p) => p === "." ? pathPrefix.slice(0, -1) : `${pathPrefix}${p}`;
461
- for (const f of findings) f.file = prefix(f.file);
547
+ for (const f of findings) { f.file = prefix(f.file); if (f.lockfile) f.lockfile = prefix(f.lockfile); }
462
548
  for (const c of coverage) c.path = prefix(c.path);
463
549
  for (const d of deps) d.file = prefix(d.file);
464
550
  for (const d of contentDigests) d.path = prefix(d.path);
@@ -473,26 +559,32 @@ function trustForScan(options = {}) {
473
559
  try { return loadTrustedExternal().trust; } catch { return null; }
474
560
  }
475
561
 
476
- function resolveSharedAuthority(trust) {
562
+ function resolveSharedAuthority(trust, readAuthority = readCommittedKeyAuthority) {
477
563
  if (!trust?.keyAuthority) return { trust };
478
- const authority = readCommittedKeyAuthority(trust);
564
+ const authority = readAuthority(trust);
479
565
  if (!authority.ok) throw new Error(`protected ownership key authority failed: ${authority.problems.join("; ")}`);
480
566
  if (!authority.resolvedTrust.ownershipKeys.length) throw new Error("protected ownership key authority has no active approval keys");
481
567
  return { trust: authority.resolvedTrust, keyLogDigest: authority.digest, keyCheckpoint: authority.keyCheckpoint, rootFingerprint: authority.rootFingerprint, keyAuthority: authority.authority };
482
568
  }
483
569
 
484
- function resolveRepositoryKeyLog(trust, repositoryRoot, repositoryIdentity) {
570
+ function resolveRepositoryKeyLog(trust, repositoryRoot, repositoryIdentity, readAuthority = readCommittedKeyAuthority) {
485
571
  if (!trust?.rootFingerprint || trust.keyAuthority) return { trust };
486
- const authority = readCommittedKeyAuthority({ ...trust, keyAuthority: { repositoryIdentity, checkoutPath: repositoryRoot } });
572
+ const authority = readAuthority({ ...trust, keyAuthority: { repositoryIdentity, checkoutPath: repositoryRoot } });
487
573
  if (!authority.ok) throw new Error(`protected ownership key log failed: ${authority.problems.join("; ")}`);
488
574
  if (!authority.resolvedTrust.ownershipKeys.length) throw new Error("protected ownership key log has no active approval keys");
489
575
  return { trust: authority.resolvedTrust, keyLogDigest: authority.digest, keyCheckpoint: authority.keyCheckpoint, rootFingerprint: authority.rootFingerprint };
490
576
  }
491
577
 
492
578
  export function scanApp(appRoot, manifest, options = {}) {
579
+ return scanAppInternal(appRoot, manifest, options);
580
+ }
581
+ export function scanAppForTest(appRoot, manifest, options = {}) {
582
+ return scanAppInternal(appRoot, manifest, options, options.readAuthorityForTest);
583
+ }
584
+ function scanAppInternal(appRoot, manifest, options, readAuthority) {
493
585
  const initialTrust = trustForScan(options), location = scanRepositoryRoot(appRoot);
494
586
  const repositoryIdentity = repositoryIdentityFor(appRoot);
495
- const authority = initialTrust?.keyAuthority ? resolveSharedAuthority(initialTrust) : resolveRepositoryKeyLog(initialTrust, location.root, repositoryIdentity);
587
+ const authority = initialTrust?.keyAuthority ? resolveSharedAuthority(initialTrust, readAuthority) : resolveRepositoryKeyLog(initialTrust, location.root, repositoryIdentity, readAuthority);
496
588
  const trust = authority.trust;
497
589
  const result = scanInternal(appRoot, manifest, { inventory: fileInventory(appRoot, trust, location.prefix, location.root), trust, repositoryIdentity, repositoryRoot: location.root, pathPrefix: location.prefix });
498
590
  Object.defineProperty(result.findings, "coverage", { value: result.coverage, enumerable: false });
@@ -522,52 +614,68 @@ function enrollmentPending(trust, state) {
522
614
  return !keys.every((key) => key?.keyId && key.publicKeyPem && !trust.revokedKeyIds?.includes(key.keyId)) || keys[0]?.keyId === keys[1]?.keyId || keys[0]?.publicKeyPem === keys[1]?.publicKeyPem;
523
615
  }
524
616
 
617
+ /** Effective enforcement policy; a fingerprint without a tier is treated as `block` (fail closed). */
618
+ function findingPolicy(c, f) {
619
+ return { tier: f.tier ?? c.tier ?? "block", ...(f.policy ? { policy: f.policy } : {}) };
620
+ }
525
621
  export function evaluate(findings, exceptions, options = {}) {
526
622
  const today = options.today ?? new Date().toISOString().slice(0, 10), problems = [], covered = [], violations = [], warnings = [];
527
623
  const metadata = options.metadata ?? findings.metadata ?? {};
528
624
  const catalogDigest = metadata.catalogDigest ?? options.catalogDigest ?? "";
529
625
  const identity = options.repositoryIdentity ?? metadata.repositoryIdentity ?? "";
530
626
  for (const e of exceptions) {
531
- const p = e.payload ?? e;
532
- const where = `exception #${e.index} (${p.fingerprint ?? "?"})`;
533
- const missing = ["fingerprint", "paths", "reason", "owner", "expires", "requestId", "occurrenceHashes"].filter((k) => !p[k] || (Array.isArray(p[k]) && !p[k].length));
627
+ const p = e?.payload ?? e ?? {};
628
+ const where = `exception #${e?.index} (${p.fingerprint ?? "?"})`;
629
+ const missing = ["fingerprint", "occurrences", "policyDigest", "minimumScannerVersion", "minimumCatalogVersion", "issued", "reason", "owner", "expires", "requestId"].filter((k) => !p[k] || (Array.isArray(p[k]) && !p[k].length));
534
630
  if (missing.length) { problems.push(`${where}: missing ${missing.join(", ")}`); continue; }
535
631
  const approvedBy = e.approvedBy ?? p.approvedBy;
536
632
  if (approvedBy !== undefined && approvedBy !== "Adrian") { problems.push(`${where}: approvedBy must be Adrian`); continue; }
537
633
  if (!/^\d{4}-\d{2}-\d{2}$/.test(p.expires) || Number.isNaN(Date.parse(`${p.expires}T00:00:00Z`)) || p.expires < today) { problems.push(`${where}: invalid or expired expires date`); continue; }
538
634
  if (!p.trackingRef || typeof p.trackingRef !== "object" || !p.trackingRef.verified || !/^https:\/\//.test(p.trackingRef.url ?? "") || !p.trackingRef.id) { problems.push(`${where}: tracking must be a verified RightKit request reference`); continue; }
539
- if (p.paths.some((item) => !item || item.includes("\\") || item.startsWith("/") || item.split("/").includes("..") || /[*?\[\]{}]/.test(item))) { problems.push(`${where}: exception paths must be exact repository-relative paths`); continue; }
540
- const verification = verifyException(e, { repositoryIdentity: identity, catalogDigest, findings, trust: options.trust, today, scannerVersion: options.scannerVersion ?? metadata.scannerVersion ?? SCANNER_VERSION, catalogVersion: options.catalogVersion ?? metadata.catalogVersion });
635
+ let policyDigest;
636
+ try { policyDigest = policyEntryDigest(options.manifest ?? installedManifest, p.fingerprint); } catch (error) { problems.push(`${where}: ${error.message}`); continue; }
637
+ const { index, ...record } = e;
638
+ const verification = verifyException(record, { repositoryIdentity: identity, catalogDigest, policyDigest, findings, trust: options.trust, today, scannerVersion: options.scannerVersion ?? metadata.scannerVersion ?? SCANNER_VERSION, catalogVersion: options.catalogVersion ?? metadata.catalogVersion });
541
639
  if (!verification?.ok) { problems.push(`${where}: signature verification failed${verification?.problems?.length ? ` (${verification.problems.join("; ")})` : ""}`); continue; }
542
- const matching = findings.filter((f) => f.fingerprint === p.fingerprint && p.paths.includes(f.file) && p.occurrenceHashes.includes(occurrenceHash(f)));
640
+ const matching = findings.filter((f) => f.fingerprint === p.fingerprint && p.occurrences.some((item) => item.path === f.file && item.hash === occurrenceHash(f)));
543
641
  if (!matching.length) { problems.push(`${where}: stale, matches no signed occurrence`); continue; }
544
642
  for (const f of matching) covered.push({ ...f, occurrenceHash: occurrenceHash(f), exception: e });
545
643
  }
546
644
  const pending = enrollmentPending(options.trust, options.trustEnrollment);
547
645
  if (pending) warnings.push("approval enrollment pending; detected findings remain open backlog requests until distinct Windows & macOS keys are enrolled");
548
- const qaOnly = findings.filter((f) => f.qaOnly === true && f.capability !== "desktop-qa");
549
- for (const f of findings) if (!qaOnly.includes(f) && !covered.some((x) => x.occurrenceHash === occurrenceHash(f))) violations.push({ ...f, occurrenceHash: occurrenceHash(f) });
550
- return { violations, covered, qaOnly, problems, warnings, enrollmentPending: pending };
646
+ for (const f of findings) if (!covered.some((x) => x.occurrenceHash === occurrenceHash(f))) violations.push({ ...f, occurrenceHash: occurrenceHash(f) });
647
+ return { violations, covered, problems, warnings, enrollmentPending: pending };
551
648
  }
552
649
 
553
650
  export function capabilityTable(manifest, deps, result) {
554
- return [...new Map(manifest.capabilities.map((c) => [c.capability, c.package])).entries()].map(([capability, owner]) => {
555
- const declared = deps.some((d) => d.name === owner || d.name === owner.replace(/^rightkit-/, "@rightkit/"));
651
+ const groups = new Map();
652
+ for (const c of manifest.capabilities) {
653
+ if (!groups.has(c.capability)) groups.set(c.capability, []);
654
+ groups.get(c.capability).push(c.package);
655
+ }
656
+ return [...groups].map(([capability, owners]) => {
657
+ const owner = owners[0];
658
+ const declared = deps.some((d) => owners.includes(d.name) || owners.some((name) => d.name === name.replace(/^rightkit-/, "@rightkit/")));
556
659
  const own = result.violations.filter((x) => x.capability === capability), covered = result.covered.filter((x) => x.capability === capability);
557
- const qaOnly = (result.qaOnly ?? []).filter((x) => x.capability === capability && !covered.some((item) => item.occurrenceHash === x.occurrenceHash));
558
- const status = own.length ? "blocked" : covered.length ? "excepted" : qaOnly.length ? "qa-only" : declared ? "declared-dependency" : result.coverage?.length ? "unscanned" : "not-detected";
559
- return { capability, owner, uses: declared, status, count: own.length + covered.length + qaOnly.length };
660
+ const status = own.length ? "blocked" : covered.length ? "excepted" : declared ? "declared-dependency" : result.coverage?.length ? "unscanned" : "not-detected";
661
+ return { capability, owner, ...(owners.length > 1 ? { owners } : {}), uses: declared, status, count: own.length + covered.length };
560
662
  });
561
663
  }
562
664
  export function formatTable(rows) { return ["capability owner status", ...rows.map((x) => `${x.capability} ${x.owner} ${x.status}`)].join("\n"); }
563
665
 
564
666
  export function checkApp(appRoot, manifest, options = {}) {
667
+ return checkAppInternal(appRoot, manifest, options);
668
+ }
669
+ export function checkAppForTest(appRoot, manifest, options = {}) {
670
+ return checkAppInternal(appRoot, manifest, options, options.readAuthorityForTest);
671
+ }
672
+ function checkAppInternal(appRoot, manifest, options, readAuthority) {
565
673
  let trustState = options.trust;
566
674
  if (!trustState) trustState = loadTrustedExternal();
567
675
  const initialTrust = trustState && Object.prototype.hasOwnProperty.call(trustState, "trust") ? trustState.trust : trustState;
568
676
  const repositoryLocation = scanRepositoryRoot(appRoot);
569
677
  const repositoryIdentity = repositoryIdentityFor(appRoot);
570
- const resolved = initialTrust?.keyAuthority ? resolveSharedAuthority(initialTrust) : resolveRepositoryKeyLog(initialTrust, repositoryLocation.root, repositoryIdentity);
678
+ const resolved = initialTrust?.keyAuthority ? resolveSharedAuthority(initialTrust, readAuthority) : resolveRepositoryKeyLog(initialTrust, repositoryLocation.root, repositoryIdentity, readAuthority);
571
679
  const trust = resolved.trust;
572
680
  const location = repositoryLocation;
573
681
  const scanned = scanInternal(appRoot, manifest, { inventory: fileInventory(appRoot, trust, location.prefix, location.root), trust, repositoryIdentity, repositoryRoot: location.root, pathPrefix: location.prefix });
@@ -576,13 +684,13 @@ export function checkApp(appRoot, manifest, options = {}) {
576
684
  const enrollment = trust?.rootFingerprint
577
685
  ? { enrolled: trust.enrollmentComplete === true, pending: trust.enrollmentComplete !== true }
578
686
  : trustState?.pending !== undefined || trustState?.enrolled !== undefined ? trustState : undefined;
579
- const result = evaluate(scanned.findings, loadExceptions(appRoot), { ...options, trust, trustEnrollment: enrollment, metadata });
687
+ const result = evaluate(scanned.findings, loadExceptions(appRoot), { ...options, manifest, trust, trustEnrollment: enrollment, metadata });
580
688
  const coverage = [...new Map(metadata.coverage.map((x) => [`${x.path}\0${x.reason}`, x])).values()];
581
689
  const rows = capabilityTable(manifest, scanned.deps, { ...result, coverage });
582
- return { ok: result.enrollmentPending || (!result.violations.length && !result.problems.length && !coverage.length), rows, ...result, findings: scanned.findings, coverage, metadata };
690
+ return { ok: !result.enrollmentPending && !result.violations.some(isBlockingFinding) && !result.problems.length && !coverage.length, rows, ...result, findings: scanned.findings, coverage, metadata };
583
691
  }
584
692
 
585
- function checkCommitted(appRoot, manifest, options, acquire) {
693
+ function checkCommitted(appRoot, manifest, options, acquire, readAuthority = readCommittedKeyAuthority) {
586
694
  let trustState = options.trust;
587
695
  if (!trustState) trustState = loadTrustedExternal();
588
696
  let trust = trustState && Object.prototype.hasOwnProperty.call(trustState, "trust") ? trustState.trust : trustState;
@@ -601,7 +709,7 @@ function checkCommitted(appRoot, manifest, options, acquire) {
601
709
  if (!repositoryIdentity) throw new Error("committed ownership: repository origin is missing or invalid");
602
710
  let keyLogDigest, keyCheckpoint, keyAuthorityBinding;
603
711
  if (trust?.keyAuthority) {
604
- const authority = readCommittedKeyAuthority(trust);
712
+ const authority = readAuthority(trust);
605
713
  if (!authority.ok) throw new Error(`committed ownership key authority failed: ${authority.problems.join("; ")}`);
606
714
  trust = authority.resolvedTrust;
607
715
  if (!trust.ownershipKeys.length) throw new Error("protected ownership key authority has no active approval keys");
@@ -639,12 +747,12 @@ function checkCommitted(appRoot, manifest, options, acquire) {
639
747
  pathBase: location.pathBase, pathPrefix: location.prefix, provenance: scanned.provenance, exceptionDigest,
640
748
  source: "committed-git-tree", ...(keyLogDigest ? { keyLogDigest, keyCheckpoint, rootFingerprint: trust.rootFingerprint, ...(keyAuthorityBinding ? { keyAuthority: keyAuthorityBinding } : {}) } : {}),
641
749
  };
642
- const result = evaluate(scanned.findings, exceptions, { ...options, trust, trustEnrollment: trust?.rootFingerprint
750
+ const result = evaluate(scanned.findings, exceptions, { ...options, manifest, trust, trustEnrollment: trust?.rootFingerprint
643
751
  ? { enrolled: trust.enrollmentComplete === true, pending: trust.enrollmentComplete !== true }
644
752
  : trustState?.pending !== undefined || trustState?.enrolled !== undefined ? trustState : undefined, metadata });
645
753
  const coverage = [...new Map(metadata.coverage.map((x) => [`${x.path}\0${x.reason}`, x])).values()];
646
754
  const rows = capabilityTable(manifest, scanned.deps, { ...result, coverage });
647
- return { ok: !result.enrollmentPending && !result.violations.length && !result.problems.length && !coverage.length, rows, ...result, findings: scanned.findings, coverage, metadata };
755
+ return { ok: !result.enrollmentPending && !result.violations.some(isBlockingFinding) && !result.problems.length && !coverage.length, rows, ...result, findings: scanned.findings, coverage, metadata };
648
756
  }
649
757
 
650
758
  /** Protected production scan: acquire and inspect exact HEAD blobs without reading checkout contents. */
@@ -654,5 +762,5 @@ export function checkCommittedApp(appRoot, manifest, options = {}) {
654
762
 
655
763
  /** Explicit test-only seam for bounded Git process failures and fixtures. */
656
764
  export function checkCommittedAppForTest(appRoot, manifest, options = {}) {
657
- return checkCommitted(appRoot, manifest, options, options.acquireCommittedSourceForTest ?? acquireCommittedSource);
765
+ return checkCommitted(appRoot, manifest, options, options.acquireCommittedSourceForTest ?? acquireCommittedSource, options.readAuthorityForTest);
658
766
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.135",
3
+ "version": "0.2.137",
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",