@rightkit/release 0.2.165 → 0.2.172
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -4
- package/addon-command.mjs +6 -13
- package/android-aab-scan.mjs +265 -0
- package/android-dev.mjs +111 -0
- package/android-ndk.mjs +77 -0
- package/android-play-upload.mjs +262 -0
- package/android-release.mjs +344 -0
- package/android-test-fixtures.mjs +79 -0
- package/apple-profile-create.mjs +363 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/build-release.mjs +73 -13
- package/cargo-guard.mjs +2 -0
- package/cargo-profile-contract.mjs +183 -0
- package/cargo-target.mjs +8 -0
- package/cli/right-release.mjs +14 -4
- package/committed-source.mjs +1 -1
- package/dev-artifact-refresh.mjs +176 -0
- package/dev-artifact-support.mjs +61 -0
- package/development-signing.mjs +71 -4
- package/development.mjs +78 -46
- package/direct-bootstrap.mjs +26 -41
- package/disk-command.mjs +298 -5
- package/disk-policy.json +7 -5
- package/disk-policy.mjs +105 -16
- package/disk-stable-root.mjs +19 -16
- package/engine-locator.mjs +116 -0
- package/external-bin.mjs +133 -0
- package/github-release.mjs +2 -2
- package/github-updater-release.mjs +154 -0
- package/hardeningscan.mjs +1 -1
- package/in-place-install.mjs +56 -0
- package/install-app-windows.mjs +63 -0
- package/install-app.mjs +53 -23
- package/local-publish.mjs +11 -1
- package/mac-signing-matrix.mjs +23 -11
- package/managed-cargo.mjs +6 -0
- package/media-sidecar-catalog.mjs +45 -0
- package/media-sidecar.mjs +306 -0
- package/ownership-approval-command.mjs +10 -44
- package/ownership-approval.mjs +3 -42
- package/ownership-catalog-config.json +2 -1
- package/ownership-catalog.mjs +4 -2
- package/ownership-command.mjs +7 -9
- package/ownership-dependency-allowlist.json +25 -1
- package/ownership-evidence.mjs +8 -5
- package/ownership-git-inventory.mjs +41 -7
- package/ownership-lockfiles.mjs +91 -8
- package/ownership-mandatory-set.json +36 -16
- package/ownership-report.mjs +5 -6
- package/ownership-scan-worker.mjs +2 -2
- package/ownership-verdict.mjs +10 -7
- package/ownership.mjs +47 -29
- package/package.json +7 -4
- package/portable-development.mjs +32 -115
- package/preflight.mjs +4 -6
- package/publication-artifacts.mjs +34 -0
- package/publish-update.mjs +72 -11
- package/qa-adoption-exemptions.json +5 -0
- package/qa-build-features.mjs +7 -0
- package/qa-contract.mjs +182 -0
- package/qa-control-scan.mjs +3 -3
- package/release-qa-command.mjs +38 -12
- package/release-qa-installed.mjs +136 -0
- package/release-qa-receipt.mjs +23 -2
- package/release.mjs +281 -13
- package/rightkit-ownership.json +537 -5
- package/rightkit-sync.mjs +26 -0
- package/rightkit-versions.json +109 -62
- package/runtime-budget-classes.json +28 -0
- package/runtime-budgets.mjs +4 -1
- package/sign-candidate.mjs +161 -0
- package/sign-dev-artifact.mjs +198 -0
- package/stage-binary.mjs +60 -0
- package/swap-helper-bundle.mjs +105 -0
- package/templates/cargo-profiles.toml +28 -0
- package/upload-release.mjs +11 -1
|
@@ -12,6 +12,38 @@ const DEFAULT_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
|
12
12
|
const CACHE_LIMIT = 128 * 1024 * 1024;
|
|
13
13
|
const blobCache = new Map();
|
|
14
14
|
let cacheBytes = 0;
|
|
15
|
+
// Dirty inputs kept for reuse. Reused only while size & mtime are unchanged; bounded like the Rust scanner.
|
|
16
|
+
const HASH_CACHE_BYTES = 64 * 1024 * 1024;
|
|
17
|
+
// A file modified more recently than this is never kept: a same-size rewrite inside one timestamp tick is indistinguishable.
|
|
18
|
+
const RACY_WINDOW_MS = 2000;
|
|
19
|
+
const hashCache = new Map();
|
|
20
|
+
let hashCacheBytes = 0;
|
|
21
|
+
|
|
22
|
+
function signatureOf(abs) {
|
|
23
|
+
try {
|
|
24
|
+
const stat = lstatSync(abs, { bigint: true });
|
|
25
|
+
return stat.isFile() ? { key: `${stat.size}:${stat.mtimeNs}`, size: Number(stat.size), mtimeMs: Number(stat.mtimeNs / 1000000n) } : undefined;
|
|
26
|
+
} catch { return undefined; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Read & hash one dirty input. A kept copy is reused only when size & mtime are unchanged; a file whose size or
|
|
30
|
+
// mtime moved while it was read is returned but never kept. This scanner is single-threaded, so one worker at a time.
|
|
31
|
+
function readHashed(abs) {
|
|
32
|
+
const before = signatureOf(abs);
|
|
33
|
+
if (before) {
|
|
34
|
+
const kept = hashCache.get(abs);
|
|
35
|
+
if (kept?.key === before.key) return { bytes: kept.bytes, sum: kept.sum };
|
|
36
|
+
if (kept) { hashCacheBytes -= kept.bytes.length; hashCache.delete(abs); }
|
|
37
|
+
}
|
|
38
|
+
const bytes = readFileSync(abs), sum = sha256(bytes);
|
|
39
|
+
if (before) {
|
|
40
|
+
const after = signatureOf(abs);
|
|
41
|
+
if (after?.key === before.key && bytes.length === before.size && before.mtimeMs + RACY_WINDOW_MS <= Date.now() && hashCacheBytes + before.size <= HASH_CACHE_BYTES) {
|
|
42
|
+
hashCache.set(abs, { key: before.key, bytes, sum }); hashCacheBytes += before.size;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { bytes, sum };
|
|
46
|
+
}
|
|
15
47
|
|
|
16
48
|
function gitEnv() {
|
|
17
49
|
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^GIT_/i.test(key)));
|
|
@@ -19,10 +51,10 @@ function gitEnv() {
|
|
|
19
51
|
return env;
|
|
20
52
|
}
|
|
21
53
|
|
|
22
|
-
function runGit(root, repositoryRoot, args, { input, maxBuffer = MAX_LIST_BYTES } = {}) {
|
|
54
|
+
function runGit(root, repositoryRoot, args, { input, maxBuffer = MAX_LIST_BYTES, timeout = 2500 } = {}) {
|
|
23
55
|
const executable = process.platform === "win32" ? "C:\\Program Files\\Git\\cmd\\git.exe" : "/usr/bin/git";
|
|
24
56
|
const result = spawnSync(executable, ["-C", root, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, "-c", `safe.directory=${repositoryRoot}`, ...args], {
|
|
25
|
-
encoding: "buffer", env: gitEnv(), windowsHide: true, maxBuffer, timeout
|
|
57
|
+
encoding: "buffer", env: gitEnv(), windowsHide: true, maxBuffer, timeout, ...(input ? { input } : {}),
|
|
26
58
|
});
|
|
27
59
|
if (result?.error || result?.status !== 0) throw new Error(`ownership inventory: git ${args[0]} failed${result?.error?.code ? ` (${result.error.code})` : ""}`);
|
|
28
60
|
return result.stdout;
|
|
@@ -95,7 +127,9 @@ export function gitListedInventory(appRoot, { relevant = () => true, protectedPa
|
|
|
95
127
|
catch (error) { return { files: [], coverage: [{ path: ".", status: "unscanned", reason: `repository-root-unavailable:${error.code ?? "error"}` }] }; }
|
|
96
128
|
let listing, dirtyBytes;
|
|
97
129
|
try {
|
|
98
|
-
|
|
130
|
+
// Listing walks the whole worktree (--others), which takes seconds on a CI checkout; the short
|
|
131
|
+
// default stays for the per-object calls.
|
|
132
|
+
listing = runGit(root, safeRoot, ["ls-files", "--stage", "--cached", "--others", "--exclude-standard", "-z"], { timeout: 60_000 });
|
|
99
133
|
dirtyBytes = runGit(root, safeRoot, ["diff-files", "--name-only", "-z", "--no-ext-diff", "--no-textconv", "--relative"]);
|
|
100
134
|
} catch (error) { return { files: [], coverage: [{ path: ".", status: "unscanned", reason: `git-inventory-failed:${error.message}` }] }; }
|
|
101
135
|
const dirty = new Set(dirtyBytes.length ? dirtyBytes.subarray(0, dirtyBytes.length - (dirtyBytes.at(-1) === 0 ? 1 : 0)).toString("utf8").split("\0") : []);
|
|
@@ -193,7 +227,7 @@ export function gitListedInventory(appRoot, { relevant = () => true, protectedPa
|
|
|
193
227
|
if (!stat.isFile()) { coverage.push({ path: rel, status: "unscanned", reason: "not-regular-file" }); continue; }
|
|
194
228
|
}
|
|
195
229
|
const inNodeModules = rel.split("/").includes("node_modules");
|
|
196
|
-
let bytes, oid = indexed?.oid, digest, cacheKey;
|
|
230
|
+
let bytes, oid = indexed?.oid, digest, cacheKey, prehashed;
|
|
197
231
|
if (isRelevant && entry.overLimit) {
|
|
198
232
|
// Size-limit coverage was already recorded before any blob or file read.
|
|
199
233
|
} else if (isRelevant) {
|
|
@@ -203,7 +237,7 @@ export function gitListedInventory(appRoot, { relevant = () => true, protectedPa
|
|
|
203
237
|
else if (isDirty) {
|
|
204
238
|
if (stat.size > entry.maxFileBytes) {
|
|
205
239
|
coverage.push({ path: rel, status: "unscanned", reason: "file-over-size-limit" });
|
|
206
|
-
} else bytes =
|
|
240
|
+
} else ({ bytes, sum: prehashed } = readHashed(abs));
|
|
207
241
|
} else {
|
|
208
242
|
coverage.push({ path: rel, status: "unscanned", reason: "git-blob-cache-miss" });
|
|
209
243
|
}
|
|
@@ -212,7 +246,7 @@ export function gitListedInventory(appRoot, { relevant = () => true, protectedPa
|
|
|
212
246
|
coverage.push({ path: rel, status: "unscanned", reason: "file-over-size-limit" });
|
|
213
247
|
bytes = undefined;
|
|
214
248
|
}
|
|
215
|
-
if (bytes && isDirty) { digest = sha256(bytes); cacheKey = `sha256:${digest}`; }
|
|
249
|
+
if (bytes && isDirty) { digest = prehashed ?? sha256(bytes); cacheKey = `sha256:${digest}`; }
|
|
216
250
|
else if (bytes) { digest = `git:${oid}`; cacheKey = `gitblob:${oid}`; }
|
|
217
251
|
} else if (!isDirty && oid) { digest = `git:${oid}`; cacheKey = `gitblob:${oid}`; }
|
|
218
252
|
files.push({ rel, abs, size: bytes?.length ?? entry.cleanBlobSize ?? stat?.size, bytes, oid, digest, cacheKey, inNodeModules });
|
|
@@ -220,4 +254,4 @@ export function gitListedInventory(appRoot, { relevant = () => true, protectedPa
|
|
|
220
254
|
return { files, coverage };
|
|
221
255
|
}
|
|
222
256
|
|
|
223
|
-
export function clearGitListedInventoryCacheForTests() { blobCache.clear(); cacheBytes = 0; }
|
|
257
|
+
export function clearGitListedInventoryCacheForTests() { blobCache.clear(); cacheBytes = 0; hashCache.clear(); hashCacheBytes = 0; }
|
package/ownership-lockfiles.mjs
CHANGED
|
@@ -89,9 +89,79 @@ function parsePnpmYaml(text) {
|
|
|
89
89
|
return root;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Workspace membership: a manifest is a root of a lock only when the lock's workspace owns it.
|
|
93
|
+
// Membership is decided only from a definition we fully understand; anything else (no
|
|
94
|
+
// definition, unsupported glob syntax, unparsable file) returns null = "every manifest is a
|
|
95
|
+
// member", which keeps the unresolved-root gap rather than hiding it.
|
|
96
|
+
const UNSUPPORTED_GLOB = /[\[\]{}\\]/;
|
|
97
|
+
const globPath = (value) => path.posix.normalize(String(value).trim().replace(/^(?:\.\/)+/, "").replace(/\/+$/, "") || ".").replace(/^\.$/, "");
|
|
98
|
+
function matchSegments(pattern, parts) {
|
|
99
|
+
if (!pattern.length) return !parts.length;
|
|
100
|
+
if (pattern[0] === "**") { for (let i = 0; i <= parts.length; i++) if (matchSegments(pattern.slice(1), parts.slice(i))) return true; return false; }
|
|
101
|
+
if (!parts.length) return false;
|
|
102
|
+
const source = [...pattern[0]].map((c) => c === "*" ? "[^/]*" : c === "?" ? "[^/]" : c.replace(/[.+^$|()]/g, "\\$&")).join("");
|
|
103
|
+
return new RegExp(`^${source}$`).test(parts[0]) && matchSegments(pattern.slice(1), parts.slice(1));
|
|
104
|
+
}
|
|
105
|
+
function globMatches(glob, rel) {
|
|
106
|
+
if (UNSUPPORTED_GLOB.test(glob)) throw new Error("unsupported glob syntax");
|
|
107
|
+
const pattern = globPath(glob), segments = pattern ? pattern.split("/") : [];
|
|
108
|
+
return matchSegments(segments, rel ? rel.split("/") : []);
|
|
109
|
+
}
|
|
110
|
+
/** A directory or any of its ancestors matches the glob. */
|
|
111
|
+
const underGlob = (glob, rel) => { const parts = rel ? rel.split("/") : []; for (let n = parts.length; n >= 1; n--) if (globMatches(glob, parts.slice(0, n).join("/"))) return true; return false; };
|
|
112
|
+
const stringList = (value) => { if (value === undefined) return []; if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new Error("not a string list"); return value; };
|
|
113
|
+
const dependencyTables = ["dependencies", "dev-dependencies", "build-dependencies"];
|
|
114
|
+
function cargoPathDependencies(doc) {
|
|
115
|
+
const out = [], take = (table) => { if (object(table)) for (const dep of Object.values(table)) if (object(dep) && typeof dep.path === "string") out.push(dep.path); };
|
|
116
|
+
for (const name of dependencyTables) take(doc[name]);
|
|
117
|
+
take(doc.workspace?.dependencies);
|
|
118
|
+
if (object(doc.target)) for (const target of Object.values(doc.target)) if (object(target)) for (const name of dependencyTables) take(target[name]);
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/** Predicate over a manifest directory relative to the lock directory ("" is the root), or null when undecidable. */
|
|
122
|
+
function cargoMembership(manifests, lockDir) {
|
|
123
|
+
try {
|
|
124
|
+
const rootRel = path.posix.join(lockDir, "Cargo.toml"), docs = new Map();
|
|
125
|
+
for (const { rel, bytes } of manifests) { try { docs.set(path.posix.relative(lockDir, path.posix.dirname(rel)), parseToml(bytes.toString("utf8"))); } catch { /* a bad manifest fails its own parse later */ } }
|
|
126
|
+
const root = docs.get("");
|
|
127
|
+
if (!manifests.some((m) => m.rel === rootRel) || !root || !object(root.workspace)) return null;
|
|
128
|
+
const patterns = [...stringList(root.workspace.members), ...stringList(root.workspace["default-members"])], excludes = stringList(root.workspace.exclude);
|
|
129
|
+
const excluded = (rel) => excludes.some((glob) => underGlob(glob, rel));
|
|
130
|
+
const members = new Set([""]), queue = [""];
|
|
131
|
+
for (const rel of docs.keys()) if (rel && !excluded(rel) && patterns.some((glob) => globMatches(glob, rel))) { members.add(rel); queue.push(rel); }
|
|
132
|
+
// Cargo adds path dependencies that live inside the workspace directory as members too.
|
|
133
|
+
for (let at = 0; at < queue.length; at++) for (const dep of cargoPathDependencies(docs.get(queue[at]) ?? {})) {
|
|
134
|
+
const target = path.posix.normalize(path.posix.join(queue[at] || ".", dep)).replace(/^\.$/, "");
|
|
135
|
+
if (target.startsWith("..") || path.posix.isAbsolute(target) || members.has(target) || excluded(target)) continue;
|
|
136
|
+
members.add(target); queue.push(target);
|
|
137
|
+
}
|
|
138
|
+
return (rel) => members.has(rel);
|
|
139
|
+
} catch { return null; }
|
|
140
|
+
}
|
|
141
|
+
/** pnpm-workspace.yaml `packages:` globs; the root importer is always a member. */
|
|
142
|
+
function pnpmMembership(bytes) {
|
|
143
|
+
if (!bytes) return null;
|
|
144
|
+
try {
|
|
145
|
+
const lines = bytes.toString("utf8").split(/\r?\n/), globs = [];
|
|
146
|
+
const start = lines.findIndex((line) => /^packages\s*:/.test(line));
|
|
147
|
+
if (start < 0) return null;
|
|
148
|
+
const inline = lines[start].replace(/^packages\s*:/, "").replace(/\s+#.*$/, "").trim();
|
|
149
|
+
if (inline) { const list = scalar(inline); if (!Array.isArray(list)) return null; globs.push(...list); }
|
|
150
|
+
else for (const line of lines.slice(start + 1)) {
|
|
151
|
+
if (!line.trim() || /^\s*#/.test(line)) continue;
|
|
152
|
+
if (!/^\s+-\s/.test(line)) break;
|
|
153
|
+
globs.push(scalar(line.replace(/^\s+-\s+/, "").replace(/\s+#.*$/, "")));
|
|
154
|
+
}
|
|
155
|
+
if (!globs.length || globs.some((glob) => typeof glob !== "string" || !glob.replace(/^!/, ""))) return null;
|
|
156
|
+
const positive = globs.filter((glob) => !glob.startsWith("!")), negative = globs.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1));
|
|
157
|
+
for (const glob of globs) globMatches(glob.replace(/^!/, ""), ""); // throws on unsupported syntax
|
|
158
|
+
return (rel) => !rel || positive.some((glob) => globMatches(glob, rel)) && !negative.some((glob) => underGlob(glob, rel));
|
|
159
|
+
} catch { return null; }
|
|
160
|
+
}
|
|
161
|
+
|
|
92
162
|
function graph() {
|
|
93
163
|
const nodes = new Map(), roots = [];
|
|
94
|
-
return { nodes, roots, diagnostics: [], add(id, name, version, source, local = false) {
|
|
164
|
+
return { nodes, roots, diagnostics: [], info: [], add(id, name, version, source, local = false) {
|
|
95
165
|
if (typeof name !== "string" || !name || typeof version !== "string" || !version) fail("package name/version missing");
|
|
96
166
|
const node = { id, name, version, source, local, edges: [] }; nodes.set(id, node); return node;
|
|
97
167
|
} };
|
|
@@ -107,7 +177,7 @@ function sourceFor(value, fallback = "registry") {
|
|
|
107
177
|
if (typeof value === "string" && /^(git\+|git:|https?:.*\.git)/.test(value)) return value.startsWith("git+") ? value : `git+${value}`;
|
|
108
178
|
return value || fallback;
|
|
109
179
|
}
|
|
110
|
-
function cargoGraph(text, manifests) {
|
|
180
|
+
function cargoGraph(text, manifests, lockDir) {
|
|
111
181
|
const lock = parseToml(text), g = graph();
|
|
112
182
|
if (!Array.isArray(lock.package) || ![1, 2, 3, 4].includes(lock.version ?? 1)) throw new LockError("lockfile-unsupported-format", "Cargo lock version/package list");
|
|
113
183
|
for (const [i, pkg] of lock.package.entries()) g.add(String(i), pkg.name, pkg.version, pkg.source ?? `path:${pkg.name}`, !pkg.source);
|
|
@@ -121,11 +191,16 @@ function cargoGraph(text, manifests) {
|
|
|
121
191
|
connect(g.nodes.get(String(i)), candidates.length === 1 ? candidates[0] : null, ref);
|
|
122
192
|
}
|
|
123
193
|
}
|
|
194
|
+
const membership = cargoMembership(manifests, lockDir);
|
|
124
195
|
for (const { rel, bytes } of manifests) {
|
|
125
196
|
const pkg = parseToml(bytes.toString("utf8")).package;
|
|
126
197
|
if (!pkg) continue; // Virtual workspace: member manifests supply roots.
|
|
127
198
|
const candidates = [...g.nodes.values()].filter((n) => n.name === pkg.name && n.local && (typeof pkg.version !== "string" || n.version === pkg.version));
|
|
128
|
-
if (candidates.length !== 1) {
|
|
199
|
+
if (candidates.length !== 1) {
|
|
200
|
+
// Only a manifest the workspace does not own is informational; a member that fails to resolve is a gap.
|
|
201
|
+
const outside = membership && !membership(path.posix.relative(lockDir, path.posix.dirname(rel)));
|
|
202
|
+
(outside ? g.info : g.diagnostics).push({ reason: outside ? "manifest-outside-workspace" : "lockfile-root-unresolved", detail: rel }); continue;
|
|
203
|
+
}
|
|
129
204
|
g.roots.push(candidates[0].id);
|
|
130
205
|
}
|
|
131
206
|
return g;
|
|
@@ -169,7 +244,7 @@ function packageLockGraph(text, manifests, lockDir) {
|
|
|
169
244
|
}
|
|
170
245
|
return g;
|
|
171
246
|
}
|
|
172
|
-
function pnpmGraph(text, manifests, lockDir) {
|
|
247
|
+
function pnpmGraph(text, manifests, lockDir, workspaceBytes) {
|
|
173
248
|
const lock = parsePnpmYaml(text), version = String(lock.lockfileVersion), g = graph();
|
|
174
249
|
if (!/^(?:9|6)(?:\.\d+)?$/.test(version) || !object(lock.importers) || !object(lock.packages ?? {})) throw new LockError("lockfile-unsupported-format", "pnpm requires v6/v9 importers");
|
|
175
250
|
const v9 = version.startsWith("9"), packages = lock.packages ?? {}, snapshots = v9 ? lock.snapshots ?? {} : packages;
|
|
@@ -211,9 +286,13 @@ function pnpmGraph(text, manifests, lockDir) {
|
|
|
211
286
|
for (const [name, raw] of Object.entries(pkg[section] ?? {})) connect(node, resolve(node, name, raw), name, section === "optionalDependencies");
|
|
212
287
|
}
|
|
213
288
|
}
|
|
289
|
+
const membership = pnpmMembership(workspaceBytes);
|
|
214
290
|
for (const { rel, bytes } of manifests) {
|
|
215
291
|
const id = path.posix.relative(lockDir, path.posix.dirname(rel)) || ".", node = g.nodes.get(`importer:${id}`);
|
|
216
|
-
if (!node) {
|
|
292
|
+
if (!node) {
|
|
293
|
+
const outside = membership && !membership(id === "." ? "" : id);
|
|
294
|
+
(outside ? g.info : g.diagnostics).push({ reason: outside ? "manifest-outside-workspace" : "lockfile-root-unresolved", detail: rel }); continue;
|
|
295
|
+
}
|
|
217
296
|
node.name = JSON.parse(bytes.toString("utf8")).name ?? id;
|
|
218
297
|
const pkg = JSON.parse(bytes.toString("utf8")), importer = lock.importers[id];
|
|
219
298
|
for (const section of sections) for (const name of Object.keys(pkg[section] ?? {})) if (!Object.hasOwn(importer[section] ?? {}, name)) g.diagnostics.push({ reason: "lockfile-root-unresolved", detail: `${rel}: ${section}.${name}` });
|
|
@@ -224,7 +303,7 @@ function pnpmGraph(text, manifests, lockDir) {
|
|
|
224
303
|
|
|
225
304
|
/** Lock bytes stay bound to the scan inventory or committed snapshot; no installed graph reads. */
|
|
226
305
|
export function transitiveDependencies(appRoot, inventory, manifests, catalogOwners, coverage, contentDigests, targets) {
|
|
227
|
-
const result = [], cache = new Map(), groups = new Map(), files = new Map(inventory.files.map((file) => [file.rel, file])), targetNames = new Set(targets);
|
|
306
|
+
const result = [], info = [], cache = new Map(), groups = new Map(), files = new Map(inventory.files.map((file) => [file.rel, file])), targetNames = new Set(targets);
|
|
228
307
|
const diagnostic = (rel, reason, detail) => coverage.push({ path: rel, status: "unscanned", reason, ...(detail ? { detail } : {}) });
|
|
229
308
|
const read = (rel) => {
|
|
230
309
|
if (cache.has(rel)) return cache.get(rel);
|
|
@@ -268,8 +347,10 @@ export function transitiveDependencies(appRoot, inventory, manifests, catalogOwn
|
|
|
268
347
|
try {
|
|
269
348
|
const text = read(rel).toString("utf8"), base = path.posix.basename(rel), dir = path.posix.dirname(rel);
|
|
270
349
|
if (base === "yarn.lock") throw new LockError("lockfile-unsupported-format", "yarn transitive graph");
|
|
271
|
-
const g = group.ecosystem === "cargo" ? cargoGraph(text, group.manifests) : base === "pnpm-lock.yaml" ? pnpmGraph(text, group.manifests, dir) : packageLockGraph(text, group.manifests, dir);
|
|
350
|
+
const g = group.ecosystem === "cargo" ? cargoGraph(text, group.manifests, dir) : base === "pnpm-lock.yaml" ? pnpmGraph(text, group.manifests, dir, read(path.posix.join(dir, "pnpm-workspace.yaml"))) : packageLockGraph(text, group.manifests, dir);
|
|
272
351
|
for (const entry of g.diagnostics) diagnostic(rel, entry.reason, entry.detail);
|
|
352
|
+
// Non-blocking: manifests outside the lock's workspace are reported, never counted as coverage gaps.
|
|
353
|
+
for (const entry of g.info) info.push({ path: rel, status: "info", reason: entry.reason, detail: entry.detail });
|
|
273
354
|
for (const node of g.nodes.values()) for (const detail of node.unresolved ?? []) diagnostic(rel, "lockfile-unresolved-dependency", detail);
|
|
274
355
|
// Retain only nodes leading to block fingerprints. Unrelated lock inventory cannot
|
|
275
356
|
// multiply traversal work; owner boundaries cannot authorize app/workspace nodes.
|
|
@@ -307,5 +388,7 @@ export function transitiveDependencies(appRoot, inventory, manifests, catalogOwn
|
|
|
307
388
|
}
|
|
308
389
|
} catch (error) { diagnostic(rel, error.reason ?? "lockfile-parse-failed", error.message); }
|
|
309
390
|
}
|
|
310
|
-
|
|
391
|
+
result.sort((a, b) => a.dependencyPath.length - b.dependencyPath.length || JSON.stringify(a.dependencyPath).localeCompare(JSON.stringify(b.dependencyPath)));
|
|
392
|
+
Object.defineProperty(result, "info", { value: info, enumerable: false });
|
|
393
|
+
return result;
|
|
311
394
|
}
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"name": "@rightkit/platform-ui",
|
|
67
67
|
"ecosystem": "npm",
|
|
68
68
|
"min": "0.2.8",
|
|
69
|
-
"registryMax": "0.2.
|
|
69
|
+
"registryMax": "0.2.8"
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
"name": "@rightkit/theme",
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"name": "rightkit-fs",
|
|
156
156
|
"ecosystem": "cargo",
|
|
157
157
|
"min": "0.4.0",
|
|
158
|
-
"registryMax": "0.
|
|
158
|
+
"registryMax": "0.4.1"
|
|
159
159
|
}
|
|
160
160
|
]
|
|
161
161
|
},
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"name": "rightkit-db",
|
|
169
169
|
"ecosystem": "cargo",
|
|
170
170
|
"min": "0.4.0",
|
|
171
|
-
"registryMax": "0.
|
|
171
|
+
"registryMax": "0.4.0"
|
|
172
172
|
}
|
|
173
173
|
]
|
|
174
174
|
},
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
"name": "@rightkit/settings-ui",
|
|
187
187
|
"ecosystem": "npm",
|
|
188
188
|
"min": "0.4.4",
|
|
189
|
-
"registryMax": "0.4.
|
|
189
|
+
"registryMax": "0.4.4"
|
|
190
190
|
}
|
|
191
191
|
]
|
|
192
192
|
},
|
|
@@ -211,7 +211,7 @@
|
|
|
211
211
|
"name": "rightkit-license",
|
|
212
212
|
"ecosystem": "cargo",
|
|
213
213
|
"min": "0.2.0",
|
|
214
|
-
"registryMax": "0.
|
|
214
|
+
"registryMax": "0.2.0"
|
|
215
215
|
},
|
|
216
216
|
{
|
|
217
217
|
"name": "rightkit-device",
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
"name": "@rightkit/license",
|
|
223
223
|
"ecosystem": "npm",
|
|
224
224
|
"min": "0.2.0",
|
|
225
|
-
"registryMax": "0.
|
|
225
|
+
"registryMax": "0.2.0"
|
|
226
226
|
}
|
|
227
227
|
]
|
|
228
228
|
},
|
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
"name": "rightkit-license",
|
|
236
236
|
"ecosystem": "cargo",
|
|
237
237
|
"min": "0.2.0",
|
|
238
|
-
"registryMax": "0.
|
|
238
|
+
"registryMax": "0.2.0"
|
|
239
239
|
}
|
|
240
240
|
]
|
|
241
241
|
},
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
"name": "@rightkit/onboarding",
|
|
261
261
|
"ecosystem": "npm",
|
|
262
262
|
"min": "0.3.3",
|
|
263
|
-
"registryMax": "0.3.
|
|
263
|
+
"registryMax": "0.3.3"
|
|
264
264
|
}
|
|
265
265
|
]
|
|
266
266
|
},
|
|
@@ -272,8 +272,8 @@
|
|
|
272
272
|
{
|
|
273
273
|
"name": "rightkit-logs",
|
|
274
274
|
"ecosystem": "cargo",
|
|
275
|
-
"min": "0.
|
|
276
|
-
"registryMax": "0.
|
|
275
|
+
"min": "0.3.0",
|
|
276
|
+
"registryMax": "0.3.0"
|
|
277
277
|
},
|
|
278
278
|
{
|
|
279
279
|
"name": "@rightkit/logs",
|
|
@@ -292,7 +292,7 @@
|
|
|
292
292
|
"name": "rightkit-crash",
|
|
293
293
|
"ecosystem": "cargo",
|
|
294
294
|
"min": "0.2.2",
|
|
295
|
-
"registryMax": "0.
|
|
295
|
+
"registryMax": "0.3.0"
|
|
296
296
|
}
|
|
297
297
|
]
|
|
298
298
|
},
|
|
@@ -305,7 +305,7 @@
|
|
|
305
305
|
"name": "rightkit-http",
|
|
306
306
|
"ecosystem": "cargo",
|
|
307
307
|
"min": "0.3.0",
|
|
308
|
-
"registryMax": "0.
|
|
308
|
+
"registryMax": "0.3.0"
|
|
309
309
|
}
|
|
310
310
|
]
|
|
311
311
|
},
|
|
@@ -330,7 +330,7 @@
|
|
|
330
330
|
"name": "@rightkit/release",
|
|
331
331
|
"ecosystem": "npm",
|
|
332
332
|
"min": "0.2.153",
|
|
333
|
-
"registryMax": "0.2.
|
|
333
|
+
"registryMax": "0.2.165",
|
|
334
334
|
"allowDev": true
|
|
335
335
|
}
|
|
336
336
|
]
|
|
@@ -344,20 +344,21 @@
|
|
|
344
344
|
"name": "rightkit-qa",
|
|
345
345
|
"ecosystem": "cargo",
|
|
346
346
|
"min": "0.2.16",
|
|
347
|
-
"registryMax": "0.
|
|
347
|
+
"registryMax": "0.6.1",
|
|
348
348
|
"allowDev": true
|
|
349
349
|
},
|
|
350
350
|
{
|
|
351
351
|
"name": "rightkit-qa-driver",
|
|
352
352
|
"ecosystem": "cargo",
|
|
353
353
|
"min": "0.3.0",
|
|
354
|
+
"note": "deprecated shim; the QA owner is rightkit-qa with the `control` feature (Ember #280)",
|
|
354
355
|
"allowDev": true
|
|
355
356
|
},
|
|
356
357
|
{
|
|
357
358
|
"name": "@rightkit/qa",
|
|
358
359
|
"ecosystem": "npm",
|
|
359
360
|
"min": "0.4.8",
|
|
360
|
-
"registryMax": "0.4.
|
|
361
|
+
"registryMax": "0.4.8",
|
|
361
362
|
"allowDev": true
|
|
362
363
|
},
|
|
363
364
|
{
|
|
@@ -378,7 +379,7 @@
|
|
|
378
379
|
"name": "rightkit-agent-tools",
|
|
379
380
|
"ecosystem": "cargo",
|
|
380
381
|
"min": "0.1.2",
|
|
381
|
-
"registryMax": "0.1.
|
|
382
|
+
"registryMax": "0.1.4"
|
|
382
383
|
}
|
|
383
384
|
]
|
|
384
385
|
}
|
|
@@ -399,6 +400,25 @@
|
|
|
399
400
|
"ownerPending": "No selected owner (D4 unratified); never fails."
|
|
400
401
|
}
|
|
401
402
|
],
|
|
403
|
+
"appAmendments": {
|
|
404
|
+
"ember": {
|
|
405
|
+
"issue": "Orthic-Labs/rightkit#280",
|
|
406
|
+
"source": "Orthic-Labs/ember#240 port to the mandatory set",
|
|
407
|
+
"profile": "desktop",
|
|
408
|
+
"note": "App-specific not-applicable rulings, ratified for Ember. An app declares them in rightkit-mandatory.json notApplicable with the reason given here. Free and open source apps (MIT OR Apache-2.0, no paid features, activation or signer) and apps with no daemon, no SQLite state and no restart-surviving jobs owe none of these rows.",
|
|
409
|
+
"notApplicable": {
|
|
410
|
+
"M3": "No engine child process and no restart-surviving jobs: exports and imports are in-process jobs on the activity stack. The ffmpeg sidecar already runs through rightkit-ffmpeg.",
|
|
411
|
+
"M4": "No engine child process and no restart-surviving jobs: exports and imports are in-process jobs on the activity stack.",
|
|
412
|
+
"M6": "No local service or daemon-discovered data location. The control channel and MCP are opt-in developer and agent ports.",
|
|
413
|
+
"M7": "No local service or daemon-discovered data location. The control channel and MCP are opt-in developer and agent ports.",
|
|
414
|
+
"M9": "No SQLite app state. Preferences are JSON through rightkit-app-settings; the photo catalog is a JSON-lines journal.",
|
|
415
|
+
"M12": "Free and open source (MIT OR Apache-2.0): no paid features, activation or signer. rightkit-device identity is adopted. Applies to OSS apps.",
|
|
416
|
+
"M13": "Free and open source (MIT OR Apache-2.0): no paid features, activation or signer. Applies to OSS apps.",
|
|
417
|
+
"M14": "@rightkit/legal needs six proprietary roles and imports node:crypto, so it cannot run in a webview; @rightkit/onboarding mandates a legal step first. Ember ships @rightkit/legal-ui for its licences and notices and holds its first-run notice until its text is approved. OSS profile; the legal and onboarding owners need an OSS mode and a mode without a legal step.",
|
|
418
|
+
"M22": "rightkit-agent-tools needs a daemon-backed app control service. Ember's agent surface is its own MCP server over engine command ids, being ported to rightkit-mcp. Applies to non-daemon apps."
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
},
|
|
402
422
|
"amendments": {
|
|
403
423
|
"v2.1": {
|
|
404
424
|
"description": "TABLE1-AMENDMENT-v2.1 (draft): @rightkit/tauri-shell joins M1. Opt-in via --amendment v2.1.",
|
package/ownership-report.mjs
CHANGED
|
@@ -39,16 +39,15 @@ function statusFor(row, report) {
|
|
|
39
39
|
return "not-detected";
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Plain exception entries (Orthic-Labs/rightkit#262) that matched no finding in this scan are stale and listed for cleanup.
|
|
42
43
|
function exceptionRequests(root, app, report) {
|
|
43
44
|
const file = path.join(root, EXCEPTIONS_FILE);
|
|
44
45
|
if (!existsSync(file)) return [];
|
|
45
46
|
let records;
|
|
46
47
|
try { records = JSON.parse(readFileSync(file, "utf8")).exceptions ?? []; } catch { return []; }
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const details = payload(record);
|
|
51
|
-
const request = { app, requestId: details.requestId ?? null, fingerprint: details.fingerprint ?? null, paths: details.paths ?? [], reason: details.reason ?? null, owner: details.owner ?? null, expires: details.expires ?? null, tracking: details.trackingRef ?? details.tracking ?? null, state: "request-pending-verification" };
|
|
48
|
+
const matched = new Set((report.covered ?? []).map((finding) => finding.exception).filter(Boolean).map((entry) => JSON.stringify([entry.capability, entry.package ?? entry.crate, entry.fingerprint ?? null, entry.path ?? null])));
|
|
49
|
+
return records.filter((record) => !matched.has(JSON.stringify([record?.capability, record?.package ?? record?.crate, record?.fingerprint ?? null, record?.path ?? null]))).map((record) => {
|
|
50
|
+
const request = { app, capability: record?.capability ?? null, package: record?.package ?? record?.crate ?? null, reason: record?.reason ?? null, owner: record?.owner ?? null, date: record?.date ?? null, state: "stale-exception-entry" };
|
|
52
51
|
return { ...request, contentHash: sha256(canonicalJson(request)) };
|
|
53
52
|
});
|
|
54
53
|
}
|
|
@@ -148,7 +147,7 @@ export function renderOwnershipMarkdown(report) {
|
|
|
148
147
|
}
|
|
149
148
|
lines.push("", "## Capability status", "", "| App | Capability | Status | Detection | Availability |", "|---|---|---|---|---|");
|
|
150
149
|
for (const app of report.apps) for (const row of app.rows) lines.push(`| ${app.app} | ${row.capability} | ${row.status} | ${row.detection} | ${row.available.map((x) => `${x.package}@${x.version} (${x.state})`).join(", ") || "none recorded"} |`);
|
|
151
|
-
lines.push("", `Blocks: ${report.blocks.length} ·
|
|
150
|
+
lines.push("", `Blocks: ${report.blocks.length} · Stale exception entries: ${report.exceptionRequests.length}`, "", "Exception entries are plain records reviewed through git; an entry that matches no finding is stale and should be removed.", "");
|
|
152
151
|
if (report.warnings?.length) {
|
|
153
152
|
lines.splice(-2, 0, "## Warnings", "", ...report.warnings.map((warning) => `- ${warning.app}: WARNING: ${warning.message}; ${warning.findings.length} finding(s), ${warning.problems.length} problem(s) retained as diagnostics`), "");
|
|
154
153
|
}
|
|
@@ -68,11 +68,11 @@ function scanProtected(appRoot, options, createEvidence) {
|
|
|
68
68
|
const before = scanInputs(appRoot, options);
|
|
69
69
|
const preliminary = { ok: true, enrollmentPending: before.enrollmentPending, violations: [], problems: [], coverage: before.metadata.coverage, metadata: before.metadata };
|
|
70
70
|
const preflight = ownershipVerdict(preliminary, before.trust);
|
|
71
|
-
if (!preflight.admit
|
|
71
|
+
if (!preflight.admit) return { ...before, report: { ...preliminary, ok: false }, verdict: preflight };
|
|
72
72
|
let checked;
|
|
73
73
|
try { checked = createEvidence(); }
|
|
74
74
|
catch (error) {
|
|
75
|
-
if (
|
|
75
|
+
if (!before.enrollmentPending) throw error;
|
|
76
76
|
const report = { ...preliminary, ok: false, problems: [error.message] };
|
|
77
77
|
return { ...before, report, verdict: ownershipVerdict(report, before.trust) };
|
|
78
78
|
}
|
package/ownership-verdict.mjs
CHANGED
|
@@ -40,19 +40,22 @@ const refuse = (code, problems) => ({ admit: false, code, problems });
|
|
|
40
40
|
/** One fail-closed contract for protected scan workers & broker admission. */
|
|
41
41
|
export function ownershipVerdict(report, trust) {
|
|
42
42
|
if (!validReport(report)) return refuse("RK_OWNERSHIP_MALFORMED_REPORT", ["ownership report has missing, unknown, or wrong-typed fields"]);
|
|
43
|
-
|
|
43
|
+
// Pending approval enrollment is a reported warning, never a refusal (#262): every real finding gate below still applies.
|
|
44
|
+
const warnings = report.enrollmentPending ? ["protected ownership approval enrollment is pending (warning only; it existed only for retired signed approvals)"] : [];
|
|
45
|
+
const withWarnings = (verdict) => (warnings.length ? { ...verdict, warnings } : verdict);
|
|
46
|
+
const noRegistry = report.enrollmentPending && !trust;
|
|
44
47
|
let registration;
|
|
45
|
-
try {
|
|
48
|
+
if (!noRegistry) try {
|
|
46
49
|
registration = verifyRepositoryRegistration(trust, report.metadata.repositoryIdentity, { files: [
|
|
47
50
|
...report.violations.map((entry) => entry.file), ...(report.findings ?? []).map((entry) => entry.file),
|
|
48
51
|
...(report.metadata.contentDigests ?? []).map((entry) => entry.path),
|
|
49
52
|
] });
|
|
50
53
|
} catch { return refuse("RK_OWNERSHIP_INVALID_REGISTRATION", ["protected repository registry is missing or malformed"]); }
|
|
51
|
-
if (!registration.ok) return refuse("RK_OWNERSHIP_INVALID_REGISTRATION", registration.problems);
|
|
54
|
+
if (!noRegistry && !registration.ok) return refuse("RK_OWNERSHIP_INVALID_REGISTRATION", registration.problems);
|
|
52
55
|
const scannedRoots = report.metadata.roots?.map((entry) => entry.root) ?? [report.metadata.pathPrefix?.replace(/\/$/, "") || "."];
|
|
53
|
-
if (new Set(scannedRoots).size !== scannedRoots.length || sha256([...scannedRoots].sort()) !== sha256([...registration.registration.roots].sort()))
|
|
56
|
+
if (!noRegistry && (new Set(scannedRoots).size !== scannedRoots.length || sha256([...scannedRoots].sort()) !== sha256([...registration.registration.roots].sort())))
|
|
54
57
|
return refuse("RK_OWNERSHIP_INVALID_REGISTRATION", ["scan roots do not exactly cover protected repository registration"]);
|
|
55
|
-
const floor = validateOwnershipFloor(trust, report.metadata);
|
|
58
|
+
const floor = noRegistry ? { ok: true, problems: [] } : validateOwnershipFloor(trust, report.metadata);
|
|
56
59
|
if (!floor.ok) return refuse("RK_OWNERSHIP_POLICY_DOWNGRADE", floor.problems);
|
|
57
60
|
const incomplete = [...report.coverage, ...report.metadata.coverage].filter((entry) => entry.status !== "complete");
|
|
58
61
|
if (incomplete.length) return refuse("RK_OWNERSHIP_INCOMPLETE_COVERAGE", [...new Set(incomplete.map((entry) => entry.path + ": " + entry.reason))]);
|
|
@@ -61,8 +64,8 @@ export function ownershipVerdict(report, trust) {
|
|
|
61
64
|
if (report.problems.length) return refuse("RK_OWNERSHIP_UNVERIFIED", report.problems);
|
|
62
65
|
if (!report.ok) return refuse("RK_OWNERSHIP_REPORT_FAILED", ["protected ownership scanner reported ok:false"]);
|
|
63
66
|
const advisory = report.violations.filter((entry) => !isBlockingFinding(entry));
|
|
64
|
-
if (advisory.length) return { admit: true, code: "RK_OWNERSHIP_ADVISORY", problems: advisory.map((entry) => entry.capability + " (" + entry.fingerprint + " in " + entry.file + ") is owned by " + entry.package + " [advise]") };
|
|
65
|
-
return { admit: true, code: "RK_OWNERSHIP_CLEAN", problems: [] };
|
|
67
|
+
if (advisory.length) return withWarnings({ admit: true, code: "RK_OWNERSHIP_ADVISORY", problems: advisory.map((entry) => entry.capability + " (" + entry.fingerprint + " in " + entry.file + ") is owned by " + entry.package + " [advise]") });
|
|
68
|
+
return withWarnings({ admit: true, code: "RK_OWNERSHIP_CLEAN", problems: [] });
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
/** SHA-256 over every required authenticated cache binding; never directory or age. */
|