@rightkit/release 0.2.174 → 0.4.0
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 +200 -2
- package/addon-command.mjs +3 -4
- package/android-play-upload.mjs +2 -1
- package/apple-profile-renew.mjs +11 -3
- package/artifact-storage.mjs +35 -2
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/build-release.mjs +138 -47
- package/cli/right-release.mjs +40 -5
- package/codesign-environment.mjs +2 -2
- package/command-spec.mjs +80 -0
- package/content-digest.mjs +209 -0
- package/deps-cargo.mjs +55 -0
- package/deps.mjs +7 -0
- package/development.mjs +17 -6
- package/direct-bootstrap.mjs +5 -6
- package/disk-command.mjs +1 -0
- package/disk-policy.json +6 -2
- package/disk-stable-root.mjs +13 -9
- package/dist-scan.mjs +102 -0
- package/dmg-build.mjs +53 -4
- package/github-updater-release.mjs +70 -7
- package/icon-pack.mjs +117 -0
- package/in-place-install.mjs +17 -14
- package/init-command.mjs +116 -0
- package/install-app-windows.mjs +24 -7
- package/install-app.mjs +56 -42
- package/ios-asc-credentials.mjs +163 -0
- package/ios-build-context.mjs +129 -0
- package/ios-build-upload.mjs +282 -0
- package/ios-credentials-command.mjs +70 -0
- package/ios-dev.mjs +248 -0
- package/ios-host.mjs +76 -0
- package/ios-lock.mjs +75 -0
- package/ios-package-recipe.mjs +707 -0
- package/ios-testflight.mjs +266 -0
- package/ios-upload.mjs +313 -0
- package/local-publish.mjs +390 -47
- package/macho-approval-hash.mjs +181 -0
- package/managed-cargo.mjs +57 -1
- package/media-sidecar.mjs +20 -4
- package/mirror-root-artifact.mjs +74 -22
- package/native-cargo-layout.mjs +47 -4
- package/npm-license-notices.mjs +159 -0
- package/ownership-command.mjs +2 -1
- package/ownership-dependency-allowlist.json +2 -1
- package/ownership-evidence.mjs +30 -18
- package/ownership-key-command.mjs +69 -2
- package/ownership-mandatory-set.mjs +66 -16
- package/ownership-repositories.json +1 -1
- package/ownership-scope.mjs +94 -0
- package/ownership.mjs +86 -11
- package/package-recipe-targets.mjs +56 -2
- package/package.json +1 -1
- package/pe-content.mjs +133 -0
- package/preflight.mjs +14 -7
- package/project-type.mjs +3 -1
- package/protected-credentials.mjs +28 -1
- package/publication-artifacts.mjs +30 -5
- package/publication-gate.mjs +83 -5
- package/publish-cargo.mjs +124 -18
- package/publish-update.mjs +15 -2
- package/qa-build-features.mjs +16 -13
- package/qa-contract.mjs +30 -2
- package/qa-control-scan.mjs +4 -0
- package/release-dependencies-command.mjs +45 -0
- package/release-dependencies.mjs +60 -0
- package/release-invocation.mjs +118 -8
- package/release-switch-registry.json +126 -27
- package/release-switch-scan.mjs +62 -24
- package/release.mjs +144 -21
- package/rightkit-ownership.json +46 -2
- package/rightkit-versions.json +380 -189
- package/runtime-budgets.mjs +134 -23
- package/sign-updater.mjs +56 -59
- package/source-gate.mjs +8 -1
- package/stage-sidecar.mjs +634 -69
- package/target-distribution.mjs +5 -0
- package/test-command.mjs +190 -0
- package/toolchain-swift.mjs +239 -0
- package/upload-release.mjs +7 -2
- package/web-build.mjs +67 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `right-release license-notices npm [--config <file>] [--output <file>] [--product-name <name>] [--check]`: writes the
|
|
3
|
+
// third-party npm license appendix (one HTML file) for an app's production dependency closure, from
|
|
4
|
+
// `pnpm licenses list --prod --json`, the installed packages' own LICENSE / COPYING / NOTICE files, and the lockfile hash.
|
|
5
|
+
//
|
|
6
|
+
// legal: { npmNotices: { output: "legal/THIRD_PARTY_NPM_LICENSES.html", productName: "ViewRight", exclude: ["@rightkit/"] } }
|
|
7
|
+
//
|
|
8
|
+
// `output` and `productName` may also come from `--output` / `--product-name`. `exclude` is a list of package-name prefixes left
|
|
9
|
+
// out (default `["@rightkit/"]`). A package that ships no license file gets the standard published text for its declared SPDX
|
|
10
|
+
// license (MIT or ISC), labelled as a fallback; any other license without a file is an error, never invented text.
|
|
11
|
+
// The page records the app version, the sha256 of `pnpm-lock.yaml` and `generated-by=right-release license-notices npm`, so the
|
|
12
|
+
// same inputs always produce the same bytes. `--check` writes nothing and exits 1 when the file on disk differs.
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { spawnSync } from "./background-process.mjs";
|
|
18
|
+
import { CommandSpecError, loadAppConfig, relativeInside } from "./command-spec.mjs";
|
|
19
|
+
|
|
20
|
+
const fail = (message) => { throw new CommandSpecError(message); };
|
|
21
|
+
|
|
22
|
+
// Generic published SPDX text, used ONLY when an installed package ships no license file of its own.
|
|
23
|
+
export const SPDX_FALLBACK_TEXT = {
|
|
24
|
+
MIT: `Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
25
|
+
|
|
26
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
27
|
+
|
|
28
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`,
|
|
29
|
+
ISC: `Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
30
|
+
|
|
31
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.`,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const escapeHtml = (value) => String(value)
|
|
35
|
+
.replaceAll("&", "&")
|
|
36
|
+
.replaceAll("<", "<")
|
|
37
|
+
.replaceAll(">", ">")
|
|
38
|
+
.replaceAll('"', """);
|
|
39
|
+
|
|
40
|
+
/** Flatten `pnpm licenses list --json` (license -> entries) into a sorted list, minus excluded name prefixes. */
|
|
41
|
+
export function collectNpmPackages(groups, exclude = ["@rightkit/"]) {
|
|
42
|
+
return Object.entries(groups)
|
|
43
|
+
.flatMap(([license, entries]) => entries.map((entry) => ({ ...entry, license })))
|
|
44
|
+
.filter((entry) => !exclude.some((prefix) => entry.name.startsWith(prefix)))
|
|
45
|
+
.sort((a, b) => `${a.name}@${a.versions.join(",")}`.localeCompare(`${b.name}@${b.versions.join(",")}`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Render the appendix. `readNames(dir)` lists a package directory and `readText(file)` reads one file (both injectable).
|
|
50
|
+
* Returns `{ html, packageCount, fallbackCount }`.
|
|
51
|
+
*/
|
|
52
|
+
export function renderNpmNotices({ productName, version, lockHash, packages, readNames = (dir) => readdirSync(dir), readText = (file) => readFileSync(file, "utf8") }) {
|
|
53
|
+
let fallbackCount = 0;
|
|
54
|
+
const sections = packages.map((entry) => {
|
|
55
|
+
const packageRoot = entry.paths[0];
|
|
56
|
+
const files = readNames(packageRoot).filter((name) => /^(?:license|copying|notice)(?:[._-]|$)/i.test(name)).sort();
|
|
57
|
+
let texts;
|
|
58
|
+
if (files.length) {
|
|
59
|
+
texts = files.map((name) => `<h3>${escapeHtml(name)}</h3><pre>${escapeHtml(readText(path.join(packageRoot, name)))}</pre>`).join("\n");
|
|
60
|
+
} else {
|
|
61
|
+
const fallback = SPDX_FALLBACK_TEXT[entry.license];
|
|
62
|
+
if (!fallback) {
|
|
63
|
+
fail(`${entry.name} ${entry.versions.join(", ")} has no packaged license/notice file and no SPDX fallback text for declared license "${entry.license}"; bundle the file or extend SPDX_FALLBACK_TEXT`);
|
|
64
|
+
}
|
|
65
|
+
fallbackCount += 1;
|
|
66
|
+
texts = `<h3>No packaged license file found</h3><p><em>This package's installed npm distribution does not include a LICENSE/COPYING/NOTICE file. The text below is the standard published license text for its declared SPDX license (${escapeHtml(entry.license)}), not package-specific content.</em></p><pre>${escapeHtml(fallback)}</pre>`;
|
|
67
|
+
}
|
|
68
|
+
return `<section><h2>${escapeHtml(entry.name)} ${escapeHtml(entry.versions.join(", "))}</h2><p>Declared license: ${escapeHtml(entry.license)}</p>${texts}</section>`;
|
|
69
|
+
});
|
|
70
|
+
const html = `<!doctype html>
|
|
71
|
+
<html lang="en"><head><meta charset="utf-8"><title>${escapeHtml(productName)} npm third-party licenses</title></head><body>
|
|
72
|
+
<h1>${escapeHtml(productName)} npm third-party licenses</h1>
|
|
73
|
+
<p>Build provenance: ${escapeHtml(productName)} ${escapeHtml(version)}; inputs: pnpm-lock.yaml sha256=${lockHash}; generated-by=right-release license-notices npm</p>
|
|
74
|
+
${sections.join("\n")}
|
|
75
|
+
</body></html>\n`;
|
|
76
|
+
return { html, packageCount: packages.length, fallbackCount };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Resolve options from config + flags. */
|
|
80
|
+
export function resolveNoticeOptions(config, flags = {}) {
|
|
81
|
+
const declared = config?.legal?.npmNotices ?? {};
|
|
82
|
+
if (typeof declared !== "object" || declared === null || Array.isArray(declared)) fail("legal.npmNotices must be an object");
|
|
83
|
+
const unknown = Object.keys(declared).filter((key) => !["output", "productName", "exclude"].includes(key));
|
|
84
|
+
if (unknown.length) fail(`legal.npmNotices has unknown key(s): ${unknown.join(", ")} (allowed: output, productName, exclude)`);
|
|
85
|
+
const output = flags.output ?? declared.output;
|
|
86
|
+
const productName = flags.productName ?? declared.productName;
|
|
87
|
+
const exclude = declared.exclude ?? ["@rightkit/"];
|
|
88
|
+
if (output === undefined) fail("no output file: set legal.npmNotices.output or pass --output");
|
|
89
|
+
relativeInside(output, "legal.npmNotices.output");
|
|
90
|
+
if (typeof productName !== "string" || !productName.trim()) fail("no product name: set legal.npmNotices.productName or pass --product-name");
|
|
91
|
+
if (!Array.isArray(exclude) || exclude.some((prefix) => typeof prefix !== "string")) fail("legal.npmNotices.exclude must be a list of name prefixes");
|
|
92
|
+
if ((config?.packageManager ?? "pnpm") !== "pnpm") fail(`license-notices npm supports pnpm apps only (config.packageManager is ${config.packageManager})`);
|
|
93
|
+
return { output, productName, exclude };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Generate (or check) the appendix for the app at `appRoot`. `spawnSyncFn` is injectable. */
|
|
97
|
+
export function generateNpmNotices({ appRoot, config, flags = {}, check = false, spawnSyncFn = spawnSync }) {
|
|
98
|
+
const { output, productName, exclude } = resolveNoticeOptions(config, flags);
|
|
99
|
+
const version = JSON.parse(readFileSync(path.join(appRoot, "package.json"), "utf8")).version;
|
|
100
|
+
const result = spawnSyncFn("pnpm", ["licenses", "list", "--prod", "--json"], {
|
|
101
|
+
cwd: appRoot,
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
104
|
+
shell: process.platform === "win32",
|
|
105
|
+
});
|
|
106
|
+
if (result.error) throw result.error;
|
|
107
|
+
if (result.status !== 0) fail(result.stderr || "pnpm licenses list failed");
|
|
108
|
+
const packages = collectNpmPackages(JSON.parse(result.stdout), exclude);
|
|
109
|
+
const lockHash = createHash("sha256").update(readFileSync(path.join(appRoot, "pnpm-lock.yaml"))).digest("hex");
|
|
110
|
+
const { html, packageCount, fallbackCount } = renderNpmNotices({ productName, version, lockHash, packages });
|
|
111
|
+
const target = path.join(appRoot, output);
|
|
112
|
+
if (check) {
|
|
113
|
+
const current = existsSync(target) ? readFileSync(target, "utf8") : null;
|
|
114
|
+
return { target, packageCount, fallbackCount, upToDate: current === html };
|
|
115
|
+
}
|
|
116
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
117
|
+
writeFileSync(target, html);
|
|
118
|
+
return { target, packageCount, fallbackCount, upToDate: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function parseNoticeArgs(argv) {
|
|
122
|
+
const opts = { config: "right-release.config.mjs", output: undefined, productName: undefined, check: false };
|
|
123
|
+
const rest = argv[0] === "npm" ? argv.slice(1) : argv;
|
|
124
|
+
if (argv[0] !== "npm") throw new Error("expected: license-notices npm");
|
|
125
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
126
|
+
const arg = rest[i];
|
|
127
|
+
if (arg === "--config" || arg === "--output" || arg === "--product-name") {
|
|
128
|
+
const value = rest[i + 1];
|
|
129
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} needs a value`);
|
|
130
|
+
opts[arg === "--product-name" ? "productName" : arg.slice(2)] = value;
|
|
131
|
+
i += 1;
|
|
132
|
+
} else if (arg === "--check") opts.check = true;
|
|
133
|
+
else throw new Error(`unknown license-notices argument: ${arg}`);
|
|
134
|
+
}
|
|
135
|
+
return opts;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
139
|
+
const opts = parseNoticeArgs(argv);
|
|
140
|
+
const { config, appRoot } = await loadAppConfig(opts.config);
|
|
141
|
+
const outcome = generateNpmNotices({ appRoot, config, flags: { output: opts.output, productName: opts.productName }, check: opts.check });
|
|
142
|
+
if (opts.check) {
|
|
143
|
+
if (!outcome.upToDate) {
|
|
144
|
+
console.error(`right-release license-notices: ${outcome.target} is out of date; run right-release license-notices npm`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
console.log(`${outcome.target} is up to date (${outcome.packageCount} package notices)`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
console.log(`wrote ${outcome.packageCount} package notices (${outcome.fallbackCount} used an SPDX fallback text)`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
155
|
+
main().then((code) => { process.exitCode = code; }, (error) => {
|
|
156
|
+
console.error(`right-release license-notices: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
});
|
|
159
|
+
}
|
package/ownership-command.mjs
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// right-release ownership [--app <dir>] --audit-json (Legion governance.capability-ownership provider)
|
|
7
7
|
// right-release ownership trust-compare [--json] (print this host's protected-trust digest; policy-bundle drift is a warning, never a block)
|
|
8
8
|
// right-release ownership trust-sync (retired: prints why and exits 0)
|
|
9
|
+
// right-release ownership trust-refresh [--dry-run] [--app <dir>] (elevated, key-neutral: merge new repository registrations and floors from the installed release into protected trust)
|
|
9
10
|
// right-release ownership --write-manifest [--rightkit <rightkit>] [--approve-new-capabilities]
|
|
10
11
|
// --approve-new-capabilities permits new ids during local generation only.
|
|
11
12
|
// Existing owner changes are always refused; installation needs protected authorization.
|
|
@@ -75,7 +76,7 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
|
|
|
75
76
|
if (args[0] === "trust-compare") return trustCompare(args, { print, trustLoader });
|
|
76
77
|
if (args.includes("--staged")) return stagedMain(args, { print });
|
|
77
78
|
if (args[0] === "trust-sync") { print(`[right-release] ${TRUST_SYNC_RETIRED}`); return 0; }
|
|
78
|
-
if (["enroll", "approve-key", "revoke-key"].includes(args[0])) {
|
|
79
|
+
if (["enroll", "approve-key", "revoke-key", "trust-refresh"].includes(args[0])) {
|
|
79
80
|
const { main: keyMain } = await import("./ownership-key-command.mjs");
|
|
80
81
|
return keyMain(args, { runner: gitRunner, print });
|
|
81
82
|
}
|
|
@@ -791,6 +791,7 @@
|
|
|
791
791
|
"repositoryIdentity": "https://github.com/bogusyogi/postright",
|
|
792
792
|
"dependencies": [
|
|
793
793
|
"cargo:jiff",
|
|
794
|
+
"cargo:serde",
|
|
794
795
|
"cargo:serde_json",
|
|
795
796
|
"cargo:tauri",
|
|
796
797
|
"cargo:tauri-build",
|
|
@@ -1035,7 +1036,7 @@
|
|
|
1035
1036
|
]
|
|
1036
1037
|
},
|
|
1037
1038
|
{
|
|
1038
|
-
"repositoryIdentity": "https://github.com/
|
|
1039
|
+
"repositoryIdentity": "https://github.com/bogusyogi/pulse",
|
|
1039
1040
|
"dependencies": [
|
|
1040
1041
|
"cargo:chrono",
|
|
1041
1042
|
"cargo:dirs",
|
package/ownership-evidence.mjs
CHANGED
|
@@ -23,6 +23,7 @@ function rootContains(root, candidate) {
|
|
|
23
23
|
|
|
24
24
|
function createEvidence(repoRoot, {
|
|
25
25
|
scanRoot = repoRoot,
|
|
26
|
+
scope: requestedScope = null,
|
|
26
27
|
manifestPath = path.join(path.dirname(fileURLToPath(import.meta.url)), MANIFEST_FILE),
|
|
27
28
|
scan = checkApp,
|
|
28
29
|
trustLoader = loadTrustedExternal,
|
|
@@ -33,6 +34,7 @@ function createEvidence(repoRoot, {
|
|
|
33
34
|
policyBundleProvider = runningPolicyBundle,
|
|
34
35
|
} = {}, readAuthority = readCommittedKeyAuthority, scanCommitted = checkCommittedApp) {
|
|
35
36
|
const root = path.resolve(repoRoot);
|
|
37
|
+
const scope = requestedScope ? [...new Set(requestedScope)].sort() : null;
|
|
36
38
|
const manifestBytes = readFileSync(manifestPath);
|
|
37
39
|
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
38
40
|
let trustState = trustLoader();
|
|
@@ -114,21 +116,27 @@ function createEvidence(repoRoot, {
|
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
118
|
const registration = registrationCheck?.registration ?? null;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
if (scope && !useCommittedSource) throw new Error("a scoped ownership scan requires the committed-source scan; this host's trust enrollment cannot provide it");
|
|
120
|
+
// A scoped scan covers its scope roots; the unscoped scan covers the requested root.
|
|
121
|
+
const coverageTargets = scope ?? [requestedRoot];
|
|
122
|
+
const unregisteredTargets = registration ? coverageTargets.filter((target) => !registration.roots.some((registered) => rootContains(registered, target))) : [];
|
|
123
|
+
const requestedRootRegistered = unregisteredTargets.length === 0;
|
|
124
|
+
for (const target of unregisteredTargets) {
|
|
125
|
+
if (!enrollmentPending) throw new Error(`release app root is outside protected ownership roots: ${target}`);
|
|
126
|
+
evidenceDiagnostics.push(`requested scan root is outside protected ownership roots during pending enrollment: ${target}`);
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
// The release app's manifest/config can be nested, but the external registry
|
|
124
130
|
// defines repository coverage. Scan every protected root so a nested release
|
|
125
131
|
// cannot omit sibling packages or engine source by narrowing scanRoot.
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
const
|
|
132
|
+
const overlapsScope = (registered) => !scope || scope.some((target) => rootContains(registered, target) || rootContains(target, registered));
|
|
133
|
+
const scanRoots = registration?.roots ? registration.roots.filter(overlapsScope) : [];
|
|
134
|
+
if (scope) { for (const target of unregisteredTargets) if (!scanRoots.includes(target)) scanRoots.push(target); }
|
|
135
|
+
else if (!requestedRootRegistered && !scanRoots.includes(requestedRoot)) scanRoots.push(requestedRoot);
|
|
136
|
+
const scans = (scanRoots.length ? scanRoots : [scope ? "." : requestedRoot]).map((registered) => {
|
|
129
137
|
const absolute = registered === "." ? root : path.resolve(root, ...registered.split("/"));
|
|
130
138
|
const scanned = useCommittedSource
|
|
131
|
-
? scanCommitted(absolute, manifest, { trust: trustState.trust, expectedCommit: commit, expectedTree: tree, source })
|
|
139
|
+
? scanCommitted(absolute, manifest, { trust: trustState.trust, expectedCommit: commit, expectedTree: tree, source, ...(scope ? { scope } : {}) })
|
|
132
140
|
: scan(absolute, manifest, { trust: { trust: trustState.trust, enrolled: trustState.enrolled, pending: enrollmentPending }, scannerVersion, catalogVersion, ...(enrollmentPending ? { enrollmentPendingAdvisory: true } : {}) });
|
|
133
141
|
if (repositoryIdentity && canonicalRepositoryIdentity(scanned.metadata?.repositoryIdentity) !== repositoryIdentity) throw new Error(`ownership evidence: scanner observed a different repository identity under ${registered}`);
|
|
134
142
|
if (useCommittedSource && scanned.metadata?.commit !== commit) throw new Error(`ownership evidence: scanner observed a different source commit under ${registered}`);
|
|
@@ -181,6 +189,7 @@ function createEvidence(repoRoot, {
|
|
|
181
189
|
floorOk: floor.ok,
|
|
182
190
|
visibility: registration?.visibility ?? null,
|
|
183
191
|
registrationDigest: registration ? sha256(registration) : null,
|
|
192
|
+
...(scope ? { ownershipScope: scope, ownershipScopeClosed: [...new Set(scans.flatMap(({ scanned }) => scanned.metadata?.ownershipScopeClosed ?? scope))].sort() } : {}),
|
|
184
193
|
registeredRoots: registration ? [...registration.roots] : [],
|
|
185
194
|
contentDigest: enrollmentPending ? null : contentDigest,
|
|
186
195
|
checkedAt: new Date().toISOString(),
|
|
@@ -190,13 +199,13 @@ function createEvidence(repoRoot, {
|
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
/** Create scan evidence bound to committed source, catalog, and signed exceptions. */
|
|
193
|
-
export function createOwnershipEvidence(repoRoot, scanRoot = repoRoot) {
|
|
194
|
-
return createEvidence(repoRoot, { scanRoot, committed: true, requireCleanCheckout: true, allowPendingAdvisory: true });
|
|
202
|
+
export function createOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null) {
|
|
203
|
+
return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true, allowPendingAdvisory: true });
|
|
195
204
|
}
|
|
196
205
|
|
|
197
206
|
/** Alias spelling makes the immutable-source contract explicit at adapter boundaries. */
|
|
198
|
-
export function createCommittedOwnershipEvidence(repoRoot, scanRoot = repoRoot) {
|
|
199
|
-
return createEvidence(repoRoot, { scanRoot, committed: true, requireCleanCheckout: true });
|
|
207
|
+
export function createCommittedOwnershipEvidence(repoRoot, scanRoot = repoRoot, scope = null) {
|
|
208
|
+
return createEvidence(repoRoot, { scanRoot, scope, committed: true, requireCleanCheckout: true });
|
|
200
209
|
}
|
|
201
210
|
|
|
202
211
|
// Injection seam is deliberately named for tests and is not used by release commands.
|
|
@@ -219,21 +228,24 @@ export function runProtectedLocalOwnershipCheck(repoRoot, scanRoot = repoRoot) {
|
|
|
219
228
|
}
|
|
220
229
|
|
|
221
230
|
/** Re-scan exact source and compare every sealed binding before release upload. */
|
|
222
|
-
export function verifyOwnershipEvidence(expected, repoRoot, scanRoot = repoRoot) {
|
|
223
|
-
return verifyEvidence(expected, repoRoot, scanRoot, () => createCommittedOwnershipEvidence(repoRoot, scanRoot));
|
|
231
|
+
export function verifyOwnershipEvidence(expected, repoRoot, scanRoot = repoRoot, scope = null) {
|
|
232
|
+
return verifyEvidence(expected, repoRoot, scanRoot, () => createCommittedOwnershipEvidence(repoRoot, scanRoot, scope), scope);
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
export function verifyOwnershipEvidenceForTest(expected, repoRoot, options) {
|
|
227
|
-
return verifyEvidence(expected, repoRoot, options.scanRoot ?? repoRoot, () => createOwnershipEvidenceForTest(repoRoot, options));
|
|
236
|
+
return verifyEvidence(expected, repoRoot, options.scanRoot ?? repoRoot, () => createOwnershipEvidenceForTest(repoRoot, options), options.scope ?? null);
|
|
228
237
|
}
|
|
229
238
|
|
|
230
|
-
function verifyEvidence(expected, repoRoot, _scanRoot, createCurrent) {
|
|
239
|
+
function verifyEvidence(expected, repoRoot, _scanRoot, createCurrent, scope = null) {
|
|
231
240
|
if (!expected || expected.schema !== 1) return { ok: false, reason: "sealed ownership evidence is missing or malformed" };
|
|
241
|
+
// The sealed scope must equal what the current config yields; the re-scan below uses that same scope.
|
|
242
|
+
const wanted = scope ? [...new Set(scope)].sort() : null;
|
|
243
|
+
if (JSON.stringify(expected.ownershipScope ?? null) !== JSON.stringify(wanted)) return { ok: false, reason: "ownership evidence ownershipScope changed after build" };
|
|
232
244
|
let current;
|
|
233
245
|
try { current = createCurrent(); }
|
|
234
246
|
catch (error) { return { ok: false, reason: error.message }; }
|
|
235
|
-
for (const key of ["repositoryIdentity", "source", "commit", "tree", "catalogDigest", "catalogVersion", "scannerVersion", "approvalsDigest", "keyLogDigest", "keyCheckpoint", "rootFingerprint", "keyAuthority", "visibility", "registrationDigest", "registeredRoots", "contentDigest"]) {
|
|
236
|
-
const unchanged = key === "registeredRoots" || key === "keyCheckpoint" || key === "keyAuthority"
|
|
247
|
+
for (const key of ["repositoryIdentity", "source", "commit", "tree", "catalogDigest", "catalogVersion", "scannerVersion", "approvalsDigest", "keyLogDigest", "keyCheckpoint", "rootFingerprint", "keyAuthority", "visibility", "registrationDigest", "registeredRoots", "contentDigest", "ownershipScopeClosed"]) {
|
|
248
|
+
const unchanged = key === "registeredRoots" || key === "ownershipScopeClosed" || key === "keyCheckpoint" || key === "keyAuthority"
|
|
237
249
|
? JSON.stringify(expected[key]) === JSON.stringify(current.evidence[key])
|
|
238
250
|
: expected[key] === current.evidence[key];
|
|
239
251
|
if (!unchanged) return { ok: false, reason: `ownership evidence ${key} changed after build` };
|
|
@@ -8,6 +8,8 @@ import { createKeyEvent, keyFingerprint, verifyKeyLog, verifyKeyRequest } from "
|
|
|
8
8
|
import { canonicalRepositoryIdentity, DEFAULT_TRUST_PATH, TRUST_SYNC_RETIRED, loadTrustedExternal, resolveKeyLogTrust, __testOnly as approvalTestOnly } from "./ownership-approval.mjs";
|
|
9
9
|
import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
|
|
10
10
|
import { buildMacInvocation, hasTerminal } from "./ownership-key-elevation.mjs";
|
|
11
|
+
import { mergeRepositoriesFromInstalledRelease, prepareTrustUpdate, REPOSITORY_REGISTRY_MALFORMED_CODE } from "./ownership-key-trust.mjs";
|
|
12
|
+
import { summarizeTrustChange } from "./ownership-trust-summary.mjs";
|
|
11
13
|
|
|
12
14
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
15
|
const LOG = ".rightkit/ownership-keys.json";
|
|
@@ -355,14 +357,24 @@ export async function main(args, deps = {}) {
|
|
|
355
357
|
if (args[0] === "trust-sync") { print(`[right-release] ${TRUST_SYNC_RETIRED}`); return 0; }
|
|
356
358
|
try {
|
|
357
359
|
if (!platform) throw new Error(`ownership key management unsupported platform: ${process.platform}`);
|
|
360
|
+
const refresh = args[0] === "trust-refresh";
|
|
358
361
|
// Delegate BEFORE taking the command lock: the desktop-session child needs that same lock.
|
|
359
|
-
|
|
362
|
+
// trust-refresh signs nothing, so it never touches the native key helper.
|
|
363
|
+
const helper = deps.helper ?? (refresh ? null : helperFor(run));
|
|
360
364
|
const lockPath = deps.lockPath ?? path.join(os.tmpdir(), "rightkit-key-command.lock");
|
|
361
365
|
let lockFd;
|
|
362
366
|
lockFd = acquireCommandLock(lockPath);
|
|
363
367
|
try {
|
|
364
368
|
commandTemp = mkdtempSync(path.join(os.tmpdir(), "rightkit-key-command-"));
|
|
365
369
|
const authority = resolveAuthority(root, run, deps.authority);
|
|
370
|
+
if (refresh) {
|
|
371
|
+
// Key-neutral: no key log event, no metadata commit, so no minimize hook. A dry run changes nothing at all,
|
|
372
|
+
// so it also skips the fetch and fast-forward that the real ceremony performs.
|
|
373
|
+
const dryRun = args.includes("--dry-run");
|
|
374
|
+
const transport = dryRun ? null : syncBeforeCeremony(root, run);
|
|
375
|
+
const headSnapshot = dryRun ? null : git(run, root, ["rev-parse", "HEAD"]);
|
|
376
|
+
return await trustRefresh(args, { ...deps, run, print, root, authority, transport, headSnapshot });
|
|
377
|
+
}
|
|
366
378
|
const hook = detectMinimizeHook(root, run, deps);
|
|
367
379
|
const transport = syncBeforeCeremony(root, run);
|
|
368
380
|
const headSnapshot = git(run, root, ["rev-parse", "HEAD"]);
|
|
@@ -370,7 +382,7 @@ export async function main(args, deps = {}) {
|
|
|
370
382
|
if (verb === "enroll") return await enroll(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
371
383
|
if (verb === "approve-key") return await approve(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
372
384
|
if (verb === "revoke-key") return await revoke(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
373
|
-
throw new Error("expected enroll, approve-key,
|
|
385
|
+
throw new Error("expected enroll, approve-key, revoke-key, or trust-refresh (trust-sync is retired)");
|
|
374
386
|
} finally {
|
|
375
387
|
closeSync(lockFd);
|
|
376
388
|
rmSync(lockPath, { force: true });
|
|
@@ -571,6 +583,61 @@ async function revoke(args, d) {
|
|
|
571
583
|
print(`Ownership key revoked: ${keyId}; active keys: ${verifyKeyLog(next, { rootFingerprint: pin }).keys.length}`);
|
|
572
584
|
return 0;
|
|
573
585
|
}
|
|
586
|
+
const repositoryNames = (list) => list.map((identity) => String(identity).replace(/^https:\/\/github\.com\//i, "")).join(", ");
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Key-neutral elevated trust update. Builds the same request an approve-key checkpoint update sends (current committed
|
|
590
|
+
* key log, the host's pinned root fingerprint, the protected authority) so performProtectedTrustUpdate runs its
|
|
591
|
+
* additions-only registration merge and floor raise from the INSTALLED release. It adds no key event, writes nothing
|
|
592
|
+
* under .rightkit/ and creates no commit. Elevation, the pinned request digest and the installed-sibling-script rule
|
|
593
|
+
* are the unchanged elevate() path.
|
|
594
|
+
*/
|
|
595
|
+
async function trustRefresh(args, d) {
|
|
596
|
+
const { print, root } = d;
|
|
597
|
+
const dryRun = args.includes("--dry-run");
|
|
598
|
+
const rest = args.slice(1);
|
|
599
|
+
const unexpected = rest.filter((arg, i) => !(arg === "--dry-run" || arg === "--app" || rest[i - 1] === "--app"));
|
|
600
|
+
if (unexpected.length) throw new Error(`trust-refresh accepts only --dry-run and --app <dir>; unexpected: ${unexpected.join(" ")}`);
|
|
601
|
+
assertSafeMetadataParents(root, LOG);
|
|
602
|
+
const hostTrust = (d.trustLoader ?? trustState)();
|
|
603
|
+
const existing = hostTrust?.trust;
|
|
604
|
+
if (!existing?.rootFingerprint) throw new Error("this host is not enrolled: protected ownership trust has no pinned root. trust-refresh never creates trust; run `right-release ownership enroll` first");
|
|
605
|
+
const log = readLog(root);
|
|
606
|
+
if (!log.events.length) throw new Error("no root key log exists in this checkout");
|
|
607
|
+
// Verifies the local log against the pinned root, the protected authority, and (unless injected) the committed HEAD blob.
|
|
608
|
+
currentLogTrust(root, log, hostTrust, d.authority, Boolean(d.resolveTrust));
|
|
609
|
+
const pin = existing.rootFingerprint;
|
|
610
|
+
const requestBytes = Buffer.from(json({ schema: 1, log, rootFingerprint: pin, baseline: null, authority: d.authority }));
|
|
611
|
+
const registry = d.installedRegistry ? { registry: d.installedRegistry } : {};
|
|
612
|
+
const merge = mergeRepositoriesFromInstalledRelease(prepareTrustUpdate(existing, JSON.parse(requestBytes.toString("utf8"))), registry);
|
|
613
|
+
const unusable = merge.warnings.find((item) => item.code === REPOSITORY_REGISTRY_MALFORMED_CODE);
|
|
614
|
+
if (unusable) throw new Error(`the installed release's repository registry is missing or malformed (${unusable.message}); refusing to refresh trust from it`);
|
|
615
|
+
const summary = summarizeTrustChange({ existing, requestBytes, ...registry });
|
|
616
|
+
const before = new Set((existing.repositories ?? []).map((entry) => entry.repositoryIdentity));
|
|
617
|
+
const added = merge.trust.repositories.map((entry) => entry.repositoryIdentity).filter((identity) => !before.has(identity));
|
|
618
|
+
print(added.length
|
|
619
|
+
? `trust-refresh would add ${added.length} repositor${added.length === 1 ? "y" : "ies"} to protected trust: ${repositoryNames(added)}`
|
|
620
|
+
: "trust-refresh would add no repository registrations");
|
|
621
|
+
for (const line of summary.lines) print(` ${line}`);
|
|
622
|
+
if (!summary.changed) { print("Protected ownership trust is already current; nothing to elevate."); return 0; }
|
|
623
|
+
if (dryRun) { print("Dry run: nothing was elevated or written."); return 0; }
|
|
624
|
+
if (git(d.run, root, ["rev-parse", "HEAD"]) !== d.headSnapshot) throw new Error("repository HEAD changed during ownership ceremony");
|
|
625
|
+
const request = makeTrustRequest(log, pin, null, d.authority);
|
|
626
|
+
try {
|
|
627
|
+
if (request.digest !== summary.payloadDigest) throw new Error("trust request bytes differ from the summarized request; refusing to elevate");
|
|
628
|
+
try { await (d.elevate ?? elevate)(d.run, request.file, request.digest); }
|
|
629
|
+
catch (error) {
|
|
630
|
+
if (platform === "macos" && !hasTerminal()) error.message += ". macOS elevation needs a human: run `right-release ownership trust-refresh` at a terminal for the sudo prompt, or approve the administrator dialog on the desktop; an unattended agent session without a tty cannot pass it";
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
} finally { await import("node:fs").then(({ unlinkSync }) => unlinkSync(request.file)).catch(() => {}); }
|
|
634
|
+
postVerify(root, log, d);
|
|
635
|
+
const present = ((d.trustLoader ?? trustState)().trust?.repositories ?? []).map((entry) => entry.repositoryIdentity);
|
|
636
|
+
const missing = added.filter((identity) => !present.includes(identity));
|
|
637
|
+
if (missing.length) throw new Error(`elevated update finished but protected trust still lacks: ${repositoryNames(missing)}`);
|
|
638
|
+
print(`Protected trust now registers ${present.length} repositories: ${repositoryNames(present)}`);
|
|
639
|
+
return 0;
|
|
640
|
+
}
|
|
574
641
|
async function signWithHelper(run, helper, log, operation, subject, signer) {
|
|
575
642
|
const draft = createKeyEvent(log, { operation, subject, signerKeyId: signer.keyId });
|
|
576
643
|
const input = temp("draft"), output = temp("draft-signed");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// right-release ownership-mandatory: Table 1 mandatory-owner-set check (plan task 5.1).
|
|
3
3
|
//
|
|
4
|
+
// This evaluates declarations only; it does not establish active target/features, lock resolution or production adoption.
|
|
4
5
|
// A registered app must declare every Table 1 owner (rows M1-M23) at or above the required version,
|
|
5
6
|
// from the registry. Report mode (default) prints per-app findings and exits 0; --enforce exits 1
|
|
6
7
|
// on any missing owner, below-required version, or path/git/patch override ("not-on-registry").
|
|
@@ -25,14 +26,51 @@ const SKIP_DIRS = new Set(["node_modules", "target", ".git", "rk-snapshot", "ven
|
|
|
25
26
|
const MAX_MANIFESTS = 2000;
|
|
26
27
|
|
|
27
28
|
// ---- versions -------------------------------------------------------------------------------
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Proven minimum of a Cargo/npm requirement: a version every satisfying version is guaranteed >=, or null when
|
|
31
|
+
* none can be proven (upper-only `<9`, `*`, `latest`, malformed, or a `||` branch with no proven minimum).
|
|
32
|
+
* Space/comma comparators AND (proven minimum = max of lower contributions; `<`/`<=` contribute none); `||`
|
|
33
|
+
* branches union (proven minimum = min of branch minima, null if any branch is unprovable). Hyphen ranges
|
|
34
|
+
* contribute their lower end. `>`, `>=`, `=`, `^`, `~`, bare & wildcard partials contribute their version
|
|
35
|
+
* (missing/`x`/`*` components read as 0), so `>1.2.3` proves 1.2.3 (sound, never above the true floor).
|
|
36
|
+
*/
|
|
29
37
|
export function lowerBound(spec) {
|
|
30
38
|
if (typeof spec !== "string") return null;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
39
|
+
const s = spec.trim().replace(/^npm:(?:@[^@/]+\/)?[^@]+@/, "").trim();
|
|
40
|
+
if (!s || /^(\*|latest|x)$/i.test(s)) return null;
|
|
41
|
+
const part = (v) => (v === undefined || /[xX*]/.test(v) ? 0 : Number(v));
|
|
42
|
+
const versionOf = (text) => {
|
|
43
|
+
const m = /^v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(text);
|
|
44
|
+
if (m?.[4] && m[4].split(".").some((id) => !id || (/^\d+$/.test(id) && id.length > 1 && id.startsWith("0")))) return null;
|
|
45
|
+
return m ? { major: Number(m[1]), minor: part(m[2]), patch: part(m[3]), pre: m[4] ?? null } : null;
|
|
46
|
+
};
|
|
47
|
+
const branchBound = (branch) => {
|
|
48
|
+
const raw = branch.split(/[\s,]+/).filter(Boolean);
|
|
49
|
+
const tokens = [];
|
|
50
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
51
|
+
if (/^(>=|<=|>|<|=|\^|~)$/.test(raw[i]) && i + 1 < raw.length) { tokens.push(raw[i] + raw[i + 1]); i += 1; }
|
|
52
|
+
else tokens.push(raw[i]);
|
|
53
|
+
}
|
|
54
|
+
if (!tokens.length) return null;
|
|
55
|
+
const lowers = [];
|
|
56
|
+
let skipUpperEnd = false;
|
|
57
|
+
for (const token of tokens) {
|
|
58
|
+
if (token === "-") { skipUpperEnd = true; continue; }
|
|
59
|
+
if (skipUpperEnd) { skipUpperEnd = false; continue; }
|
|
60
|
+
if (/^(\*|x|latest)$/i.test(token)) continue;
|
|
61
|
+
const m = /^(>=|<=|>|<|=|\^|~)?(.+)$/.exec(token);
|
|
62
|
+
const version = m && versionOf(m[2]);
|
|
63
|
+
if (!version) return null;
|
|
64
|
+
if (m[1] === "<" || m[1] === "<=") continue;
|
|
65
|
+
lowers.push(version);
|
|
66
|
+
}
|
|
67
|
+
return lowers.length ? lowers.reduce((best, v) => (compareVersions(v, best) > 0 ? v : best)) : null;
|
|
68
|
+
};
|
|
69
|
+
const branches = s.split("||").map((b) => b.trim());
|
|
70
|
+
if (branches.some((b) => !b)) return null;
|
|
71
|
+
const bounds = branches.map(branchBound);
|
|
72
|
+
if (bounds.some((b) => b === null)) return null;
|
|
73
|
+
return bounds.reduce((best, v) => (compareVersions(v, best) < 0 ? v : best));
|
|
36
74
|
}
|
|
37
75
|
export const parseVersion = lowerBound;
|
|
38
76
|
export function compareVersions(a, b) {
|
|
@@ -40,7 +78,19 @@ export function compareVersions(a, b) {
|
|
|
40
78
|
if (a.pre === b.pre) return 0;
|
|
41
79
|
if (a.pre === null) return 1;
|
|
42
80
|
if (b.pre === null) return -1;
|
|
43
|
-
|
|
81
|
+
const left = a.pre.split("."), right = b.pre.split(".");
|
|
82
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
83
|
+
if (left[i] === undefined) return -1;
|
|
84
|
+
if (right[i] === undefined) return 1;
|
|
85
|
+
if (left[i] === right[i]) continue;
|
|
86
|
+
const an = /^\d+$/.test(left[i]), bn = /^\d+$/.test(right[i]);
|
|
87
|
+
if (an && bn) {
|
|
88
|
+
const av = BigInt(left[i]), bv = BigInt(right[i]);
|
|
89
|
+
if (av !== bv) return av < bv ? -1 : 1;
|
|
90
|
+
} else if (an !== bn) return an ? -1 : 1;
|
|
91
|
+
else return left[i] < right[i] ? -1 : 1;
|
|
92
|
+
}
|
|
93
|
+
return 0;
|
|
44
94
|
}
|
|
45
95
|
const show = (v) => `${v.major}.${v.minor}.${v.patch}${v.pre ? `-${v.pre}` : ""}`;
|
|
46
96
|
|
|
@@ -75,21 +125,21 @@ export function readManifest(file, root) {
|
|
|
75
125
|
try { doc = parseToml(text); } catch (error) { return { rel, facts, error: `unparseable Cargo.toml: ${error.message}` }; }
|
|
76
126
|
workspaceDeps = doc.workspace?.dependencies;
|
|
77
127
|
const wsTable = workspaceDeps ?? {};
|
|
78
|
-
const sections = (table, dev) => {
|
|
128
|
+
const sections = (table, dev, target = null) => {
|
|
79
129
|
for (const [section, isDev] of [["dependencies", dev], ["build-dependencies", dev], ["dev-dependencies", true]]) {
|
|
80
|
-
for (const [key, raw] of Object.entries(table?.[section] ?? {})) add(key, raw, isDev);
|
|
130
|
+
for (const [key, raw] of Object.entries(table?.[section] ?? {})) add(key, raw, isDev, target);
|
|
81
131
|
}
|
|
82
132
|
};
|
|
83
|
-
const add = (key, raw, dev) => {
|
|
133
|
+
const add = (key, raw, dev, target) => {
|
|
84
134
|
let value = raw;
|
|
85
135
|
if (value && typeof value === "object" && value.workspace === true && wsTable[key] !== undefined) value = { ...(typeof wsTable[key] === "object" ? wsTable[key] : { version: wsTable[key] }), ...value };
|
|
86
136
|
const name = typeof value === "object" && typeof value.package === "string" ? value.package : key;
|
|
87
137
|
const spec = typeof value === "string" ? value : value?.version;
|
|
88
138
|
const unresolved = raw && typeof raw === "object" && raw.workspace === true && wsTable[key] === undefined;
|
|
89
|
-
facts.push({ owner: name, ecosystem: "cargo", kind: isNonRegistry(value) ? "override" : "registry", dev, spec: spec ?? null, via: isNonRegistry(value) ? ("path" in value ? "path dependency" : "git dependency") : "dependency", file: rel, ...(unresolved ? { inheritsWorkspace: key } : {}) });
|
|
139
|
+
facts.push({ target, optional: Boolean(value?.optional), owner: name, ecosystem: "cargo", kind: isNonRegistry(value) ? "override" : "registry", dev, spec: spec ?? null, via: isNonRegistry(value) ? ("path" in value ? "path dependency" : "git dependency") : "dependency", file: rel, ...(unresolved ? { inheritsWorkspace: key } : {}) });
|
|
90
140
|
};
|
|
91
141
|
sections(doc, false);
|
|
92
|
-
for (const target of Object.
|
|
142
|
+
for (const [target, table] of Object.entries(doc.target ?? {})) sections(table, false, target);
|
|
93
143
|
for (const [key, raw] of Object.entries(wsTable)) {
|
|
94
144
|
// workspace.dependencies only counts through members that opt in (above); record path/git overrides here.
|
|
95
145
|
if (isNonRegistry(raw)) facts.push({ owner: typeof raw.package === "string" ? raw.package : key, ecosystem: "cargo", kind: "override", dev: false, spec: raw.version ?? null, via: "workspace.dependencies path/git", file: rel, declarationOnly: true });
|
|
@@ -107,7 +157,7 @@ export function readManifest(file, root) {
|
|
|
107
157
|
const alias = typeof spec === "string" ? /^npm:((?:@[^@/]+\/)?[^@]+)@/.exec(spec) : null;
|
|
108
158
|
const owner = alias ? alias[1] : name;
|
|
109
159
|
const local = typeof spec === "string" && /^(workspace:|file:|link:|portal:|git\+|git:|github:|https?:)/.test(spec);
|
|
110
|
-
facts.push({ owner, ecosystem: "npm", kind: local ? "override" : "registry", dev, spec: typeof spec === "string" ? spec : null, via: local ? `${spec.split(":")[0]}: specifier` : "dependency", file: rel });
|
|
160
|
+
facts.push({ declarationSection: section, optional: section === "optionalDependencies", peer: section === "peerDependencies", owner, ecosystem: "npm", kind: local ? "override" : "registry", dev, spec: typeof spec === "string" ? spec : null, via: local ? `${spec.split(":")[0]}: specifier` : "dependency", file: rel });
|
|
111
161
|
}
|
|
112
162
|
}
|
|
113
163
|
for (const table of [doc.pnpm?.overrides, doc.overrides, doc.resolutions]) {
|
|
@@ -164,7 +214,7 @@ function judge(req, facts) {
|
|
|
164
214
|
const counted = mine.filter((f) => f.kind === "registry" && (req.allowDev || !f.dev));
|
|
165
215
|
const devOnly = !counted.length && mine.some((f) => f.kind === "registry" && f.dev);
|
|
166
216
|
const min = lowerBound(req.min);
|
|
167
|
-
const base = { owner: req.name, ecosystem: req.ecosystem, rows: [...new Set(req.rows)], required: req.min };
|
|
217
|
+
const base = { evidenceKind: "declaration", qualification: "unverified", activeTarget: null, lockResolution: null, productionUse: null, declarations: mine.map((f) => ({ file: f.file, spec: f.spec, dev: f.dev, target: f.target ?? null, optional: f.optional ?? false, peer: f.peer ?? false })), owner: req.name, ecosystem: req.ecosystem, rows: [...new Set(req.rows)], required: req.min };
|
|
168
218
|
const bounds = counted.map((f) => ({ f, v: lowerBound(f.spec) }));
|
|
169
219
|
const best = bounds.filter((b) => b.v).sort((a, b) => compareVersions(b.v, a.v))[0];
|
|
170
220
|
const declaredAt = best ? show(best.v) : null;
|
|
@@ -218,7 +268,7 @@ export function checkApp(root, { data = loadData(), profile, amendments = [] } =
|
|
|
218
268
|
resolveWorkspaceInheritance(manifests);
|
|
219
269
|
for (const m of manifests) if (m.error) problems.push(`${m.rel}: ${m.error}`);
|
|
220
270
|
const facts = manifests.flatMap((m) => m.facts);
|
|
221
|
-
const owners = [...requirements(applicable).values()].map((req) => judge(req, facts));
|
|
271
|
+
const owners = [...requirements(applicable).values()].map((req) => { const result = judge(req, facts); return { ...result, declarationStatus: result.status }; });
|
|
222
272
|
// A profile that is unknown, marked unratified, or has no rows checks nothing; an app may not select it, so it fails rather than passing empty.
|
|
223
273
|
const profileStatus = !data.profiles[active] ? "unknown-profile" : (data.profiles[active].ratified === false || !data.rows.some((r) => r.profile === active)) ? "unratified" : null;
|
|
224
274
|
if (profileStatus) {
|
|
@@ -236,7 +286,7 @@ export function checkApps(roots, options = {}) {
|
|
|
236
286
|
}
|
|
237
287
|
|
|
238
288
|
export function renderText(report) {
|
|
239
|
-
const out = [`ownership-mandatory (${report.mode}) — Table 1
|
|
289
|
+
const out = [`ownership-mandatory (${report.mode}) — Table 1 owner declarations (active-target, lock & production qualification unverified)`];
|
|
240
290
|
for (const app of report.apps) {
|
|
241
291
|
const count = (s) => app.owners.filter((o) => o.status === s).length;
|
|
242
292
|
out.push("", `${app.app} [${app.profile}] ${app.scanned ? `ok=${count("ok")} missing=${count("missing")} below-required=${count("below-required")} not-on-registry=${count("not-on-registry")}` : "UNSCANNED"}`
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
{ "repositoryIdentity": "https://github.com/orthic-labs/legion", "roots": ["."], "visibility": "public" },
|
|
16
16
|
{ "repositoryIdentity": "https://github.com/bogusyogi/workright", "roots": ["."], "visibility": "private" },
|
|
17
17
|
{ "repositoryIdentity": "https://github.com/orthic-labs/sellright", "roots": ["."], "visibility": "public" },
|
|
18
|
-
{ "repositoryIdentity": "https://github.com/
|
|
18
|
+
{ "repositoryIdentity": "https://github.com/bogusyogi/pulse", "roots": ["."], "visibility": "private" },
|
|
19
19
|
{ "repositoryIdentity": "https://github.com/bogusyogi/swiftright", "roots": ["."], "visibility": "private" }
|
|
20
20
|
]
|
|
21
21
|
}
|