@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.
- package/README.md +62 -0
- package/addon-command.mjs +53 -34
- package/apple-profile-renew.mjs +30 -12
- package/artifact-storage.mjs +47 -10
- package/background-process.mjs +18 -0
- package/build-release.mjs +1 -1
- package/cache-command.mjs +4 -3
- package/cargo-contract.mjs +1 -1
- package/cargo-guard.mjs +4 -3
- package/cargo-registry-readback.mjs +51 -0
- package/cargo-target.mjs +3 -1
- package/cli/right-release.mjs +26 -0
- package/codesign-environment.mjs +26 -2
- package/codesign-shim.mjs +2 -1
- package/committed-source.mjs +3 -3
- package/development.mjs +7 -7
- package/direct-bootstrap.mjs +19 -9
- package/dmg-build.mjs +117 -0
- package/dmg-layout.mjs +123 -0
- package/github-release.mjs +74 -37
- package/heavy-command.mjs +3 -2
- package/install-app.mjs +358 -0
- package/inventory-check.mjs +980 -0
- package/macos-signing-session.mjs +3 -0
- package/managed-cargo.mjs +23 -19
- package/managed-host-secrets.mjs +30 -0
- package/managed-roots.mjs +39 -0
- package/model-promote.mjs +31 -37
- package/notary-auth.mjs +4 -0
- package/osascript-guard.mjs +24 -0
- package/ownership-approval-command.mjs +15 -4
- package/ownership-approval.mjs +114 -49
- package/ownership-availability.mjs +25 -0
- package/ownership-catalog-config.json +18 -0
- package/ownership-catalog.mjs +94 -0
- package/ownership-command.mjs +20 -10
- package/ownership-declaration.json +15 -0
- package/ownership-default-policy.json +1 -11
- package/ownership-evidence.mjs +10 -8
- package/ownership-git-inventory.mjs +29 -1
- package/ownership-helper-paths.json +2 -2
- package/ownership-hooks.mjs +107 -0
- package/ownership-internal-owners.json +44 -0
- package/ownership-key-authority.mjs +112 -7
- package/ownership-key-command.mjs +76 -19
- package/ownership-key-trust.mjs +97 -9
- package/ownership-lockfiles.mjs +311 -0
- package/ownership-report-command.mjs +12 -3
- package/ownership-report.mjs +20 -17
- package/ownership-repositories.json +19 -0
- package/ownership-scan-worker.mjs +96 -11
- package/ownership-source.mjs +39 -0
- package/ownership-staged.mjs +193 -0
- package/ownership-verdict.mjs +74 -0
- package/ownership.mjs +215 -107
- package/package.json +1 -1
- package/protected-credentials.mjs +111 -0
- package/prune-r2.mjs +4 -3
- package/pub-upload.mjs +3 -85
- package/publication-artifacts.mjs +108 -0
- package/publication-gate.mjs +28 -7
- package/publish-cargo.mjs +47 -16
- package/publish-swift.mjs +46 -17
- package/publish-update.mjs +61 -39
- package/r2-verified-upload.mjs +41 -0
- package/registry-parity.mjs +5 -3
- package/release-state.mjs +1 -0
- package/release-token.mjs +2 -0
- package/rightkit-drift.mjs +3 -2
- package/rightkit-ownership.json +2028 -139
- package/rightkit-signing-agent.mjs +5 -3
- package/rightkit-sync.mjs +6 -2
- package/rightkit-versions.json +215 -103
- package/runtime-artifact-manifest.mjs +20 -6
- package/sign-updater.mjs +5 -0
- package/signing-agent-client.mjs +2 -1
- package/signing-agent-install.mjs +3 -2
- package/standalone-clone-verify.mjs +3 -2
- package/tauri-bundle-marker.mjs +3 -2
- package/upload-large.mjs +36 -82
- package/verifier.mjs +5 -1
package/ownership-approval.mjs
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
3
3
|
import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { spawnSync } from "./background-process.mjs";
|
|
5
|
+
import { spawnSync, windowsPowerShellEnvironment } from "./background-process.mjs";
|
|
6
6
|
import { verifyKeyLog, verifyOwnershipSignature } from "./ownership-key-log.mjs";
|
|
7
7
|
|
|
8
|
-
export const APPROVAL_SCHEMA =
|
|
8
|
+
export const APPROVAL_SCHEMA = 2;
|
|
9
9
|
export const REQUIRED_HOSTS = Object.freeze(["windows", "macos"]);
|
|
10
10
|
export const DEFAULT_TRUST_PATH = Object.freeze({
|
|
11
11
|
win32: path.join("C:\\ProgramData", "Orthic Labs", "RightKit", "ownership", "trust.json"),
|
|
@@ -89,31 +89,75 @@ export function keyIdForPublicKey(host, publicKey) {
|
|
|
89
89
|
return prefix + createHash("sha256").update(key.export({ type: "spki", format: "der" })).digest("hex");
|
|
90
90
|
}
|
|
91
91
|
export function occurrenceHash(finding) {
|
|
92
|
-
|
|
92
|
+
if (!finding || !validFingerprint(finding.fingerprint) || !occurrencePath(finding.file)) throw new Error("invalid occurrence fingerprint or path");
|
|
93
|
+
const kind = finding.kind ?? (finding.ecosystem ? "dependency" : "source");
|
|
94
|
+
if (kind === "source") {
|
|
95
|
+
if (typeof finding.lineText !== "string" || /[\r\n]/.test(finding.lineText) || !Number.isSafeInteger(finding.ordinal) || finding.ordinal < 0) throw new Error("source occurrence requires full lineText and nonnegative ordinal");
|
|
96
|
+
return sha256({ v: 2, kind, fingerprint: finding.fingerprint, path: finding.file, lineText: finding.lineText, ordinal: finding.ordinal });
|
|
97
|
+
}
|
|
98
|
+
if (kind === "transitive-dependency") {
|
|
99
|
+
if (!["cargo", "npm"].includes(finding.ecosystem) || typeof finding.name !== "string" || !finding.name || typeof finding.resolvedVersion !== "string" || !finding.resolvedVersion || typeof finding.source !== "string" || !finding.source
|
|
100
|
+
|| !Array.isArray(finding.dependencyPath) || finding.dependencyPath.length < 3 || finding.dependencyPath.some((name) => typeof name !== "string" || !name) || finding.dependencyPath.at(-1) !== finding.name) throw new Error("malformed transitive dependency occurrence identity");
|
|
101
|
+
return sha256({ v: 2, kind, fingerprint: finding.fingerprint, path: finding.file, ecosystem: finding.ecosystem, name: finding.name, resolvedVersion: finding.resolvedVersion, source: finding.source, dependencyPath: finding.dependencyPath });
|
|
102
|
+
}
|
|
103
|
+
if (kind !== "dependency") throw new Error("unknown occurrence kind");
|
|
104
|
+
const ecosystem = finding.ecosystem === "swiftpm" ? "swift" : finding.ecosystem;
|
|
105
|
+
if (!["cargo", "npm", "swift"].includes(ecosystem) || typeof finding.name !== "string" || !finding.name || typeof finding.section !== "string" || !finding.section
|
|
106
|
+
|| !nullableString(finding.alias) || !nullableString(finding.requirement) || !nullableString(finding.resolvedVersion)
|
|
107
|
+
|| !Array.isArray(finding.features) || finding.features.some((x) => typeof x !== "string" || !x)
|
|
108
|
+
|| ![true, false, null].includes(finding.defaultFeatures) || typeof finding.source !== "string" || !/^(?:registry|git\+[^\s]+|path:.+)$/.test(finding.source)) throw new Error("malformed dependency occurrence identity");
|
|
109
|
+
return sha256({ v: 2, kind, fingerprint: finding.fingerprint, path: finding.file, ecosystem, name: finding.name, alias: finding.alias, section: finding.section, requirement: finding.requirement,
|
|
110
|
+
features: [...new Set(finding.features)].sort(), defaultFeatures: finding.defaultFeatures, source: finding.source, resolvedVersion: finding.resolvedVersion });
|
|
111
|
+
}
|
|
112
|
+
// Schema-v2 envelopes retain source/dependency kinds; lock routes are dependency occurrences.
|
|
113
|
+
const exceptionOccurrenceKind = (f) => f.kind === "transitive-dependency" ? "dependency" : f.kind ?? (f.ecosystem ? "dependency" : "source");
|
|
114
|
+
function nullableString(value) { return value === null || typeof value === "string" && value.length > 0; }
|
|
115
|
+
function validFingerprint(value) { return typeof value === "string" && /^[a-z0-9][a-z0-9._-]{1,127}$/.test(value) && !HEX.test(value); }
|
|
116
|
+
function occurrencePath(value) { return typeof value === "string" && !!value && value !== "." && exactRepositoryPath(value) && !/^[A-Za-z]:/.test(value); }
|
|
117
|
+
function exactKeys(value, keys) { return !!value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).sort().join("\0") === [...keys].sort().join("\0"); }
|
|
118
|
+
function validDate(value) { return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(`${value}T00:00:00Z`)) && new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value; }
|
|
119
|
+
export function policyEntryDigest(manifest, fingerprint) {
|
|
120
|
+
if (!validFingerprint(fingerprint) || !Array.isArray(manifest?.capabilities) || manifest.capabilities.some((c) => !Array.isArray(c?.fingerprints))) throw new Error(`missing or malformed catalog policy entry: ${fingerprint}`);
|
|
121
|
+
const matches = (manifest?.capabilities ?? []).flatMap((c) => (c.fingerprints ?? []).filter((f) => f.id === fingerprint).map((f) => ({ ...f, capability: c.capability, package: c.package, tier: f.tier ?? c.tier })));
|
|
122
|
+
if (matches.length !== 1 || !matches[0].capability || !matches[0].package || !matches[0].tier) throw new Error(`missing, ambiguous, or malformed catalog policy entry: ${fingerprint}`);
|
|
123
|
+
return sha256(matches[0]);
|
|
93
124
|
}
|
|
94
125
|
export function approvalPayload(record) {
|
|
95
126
|
return record?.payload;
|
|
96
127
|
}
|
|
97
128
|
|
|
98
|
-
function validateRecord(record) {
|
|
129
|
+
export function validateRecord(record) {
|
|
99
130
|
const p = [];
|
|
100
|
-
if (!record
|
|
131
|
+
if (!exactKeys(record, ["schema", "payload", "signature"]) || record.schema !== APPROVAL_SCHEMA) return ["approval envelope must use schema 2 and exactly schema, payload, signature"];
|
|
101
132
|
const r = record.payload;
|
|
102
|
-
|
|
103
|
-
if (r[key] === undefined || r[key] === "" || (Array.isArray(r[key]) && !r[key].length)) p.push(`missing ${key}`);
|
|
104
|
-
}
|
|
133
|
+
if (!exactKeys(r, ["repositoryIdentity", "fingerprint", "policyDigest", "minimumScannerVersion", "minimumCatalogVersion", "occurrences", "reason", "owner", "issued", "expires", "requestId", "trackingRef"])) return ["approval payload has missing or unknown keys"];
|
|
105
134
|
if (typeof r.repositoryIdentity !== "string" || r.repositoryIdentity.length > 512) p.push("invalid repository identity");
|
|
106
|
-
if (
|
|
107
|
-
if (!
|
|
108
|
-
if (
|
|
109
|
-
|
|
135
|
+
try { if (canonicalRepositoryIdentity(r.repositoryIdentity) !== r.repositoryIdentity) p.push("approval repository identity is not canonical"); } catch { p.push("invalid repository identity"); }
|
|
136
|
+
if (!validFingerprint(r.fingerprint)) p.push("invalid catalog fingerprint id");
|
|
137
|
+
if (typeof r.policyDigest !== "string" || !HEX.test(r.policyDigest)) p.push("invalid policy digest");
|
|
138
|
+
for (const key of ["minimumScannerVersion", "minimumCatalogVersion"]) if (typeof r[key] !== "string" || !/^\d+(?:\.\d+)*$/.test(r[key])) p.push(`invalid ${key}`);
|
|
139
|
+
if (!Array.isArray(r.occurrences) || !r.occurrences.length || r.occurrences.length > 256) p.push("occurrences must contain 1 to 256 entries");
|
|
140
|
+
else {
|
|
141
|
+
const pairs = new Set();
|
|
142
|
+
for (const item of r.occurrences) {
|
|
143
|
+
if (!exactKeys(item, ["path", "kind", "hash"]) || !occurrencePath(item.path) || !["source", "dependency"].includes(item.kind) || typeof item.hash !== "string" || !HEX.test(item.hash)) { p.push("occurrences require exact repository-relative path, known kind, and hex hash"); continue; }
|
|
144
|
+
const pair = `${item.path}\0${item.hash}`;
|
|
145
|
+
if (pairs.has(pair)) p.push("duplicate occurrence path/hash pair");
|
|
146
|
+
pairs.add(pair);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!validDate(r.issued)) p.push("invalid issued date");
|
|
150
|
+
if (!validDate(r.expires)) p.push("invalid expiry date");
|
|
110
151
|
if (typeof r.requestId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/.test(r.requestId)) p.push("invalid request id");
|
|
111
152
|
let trackingUrl;
|
|
112
153
|
try { trackingUrl = new URL(r.trackingRef?.url); } catch {}
|
|
113
|
-
if (!trackingUrl || trackingUrl.protocol !== "https:" || !trackingUrl.hostname || trackingUrl.username || trackingUrl.password || typeof r.trackingRef.id !== "string" || !r.trackingRef.id || r.trackingRef.verified !== true) p.push("tracking reference must be verified and https-backed");
|
|
154
|
+
if (!exactKeys(r.trackingRef, ["id", "url", "verified"]) || typeof r.trackingRef.url !== "string" || !trackingUrl || trackingUrl.protocol !== "https:" || !trackingUrl.hostname || trackingUrl.username || trackingUrl.password || typeof r.trackingRef.id !== "string" || !r.trackingRef.id || r.trackingRef.verified !== true) p.push("tracking reference must be verified and https-backed");
|
|
114
155
|
if (typeof r.reason !== "string" || !r.reason.trim() || r.reason.length > 4000) p.push("invalid approval reason");
|
|
115
156
|
if (typeof r.owner !== "string" || !r.owner.trim() || r.owner.length > 256) p.push("invalid owner");
|
|
116
|
-
if (
|
|
157
|
+
if (record.signature !== null) {
|
|
158
|
+
const s = record.signature;
|
|
159
|
+
if (!exactKeys(s, ["keyId", "host", "value"]) || !REQUIRED_HOSTS.includes(s.host) || typeof s.keyId !== "string" || !new RegExp(`^${s.host === "windows" ? "win" : "mac"}-[a-f0-9]{64}$`).test(s.keyId) || typeof s.value !== "string" || !s.value || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(s.value) || Buffer.from(s.value, "base64").toString("base64") !== s.value) p.push("malformed signature envelope");
|
|
160
|
+
}
|
|
117
161
|
return p;
|
|
118
162
|
}
|
|
119
163
|
|
|
@@ -204,8 +248,12 @@ export function verifyProtectedProvenance(trust, { repositoryIdentity, paths, fi
|
|
|
204
248
|
return match ? { ok: true, problems: [], record: match } : { ok: false, problems: ["no protected provenance record matches paths and content"] };
|
|
205
249
|
}
|
|
206
250
|
|
|
207
|
-
export function createDraft({ repositoryIdentity,
|
|
208
|
-
|
|
251
|
+
export function createDraft({ repositoryIdentity, policyDigest, fingerprint, findings, minimumScannerVersion, minimumCatalogVersion, reason, owner, issued = new Date().toISOString().slice(0, 10), expires, requestId, trackingRef }) {
|
|
252
|
+
if (!validDate(issued)) throw new Error("invalid issued date");
|
|
253
|
+
if (!Array.isArray(findings)) throw new Error("draft findings must be an array of current occurrences");
|
|
254
|
+
const deadline = new Date(`${issued}T00:00:00Z`); deadline.setUTCDate(deadline.getUTCDate() + 90);
|
|
255
|
+
const occurrences = findings.map((f) => ({ path: f.file, kind: exceptionOccurrenceKind(f), hash: occurrenceHash(f) })).sort((a, b) => a.path.localeCompare(b.path) || a.hash.localeCompare(b.hash));
|
|
256
|
+
const payload = { repositoryIdentity: canonicalRepositoryIdentity(repositoryIdentity), policyDigest, fingerprint, minimumScannerVersion, minimumCatalogVersion, occurrences, reason, owner, issued, expires: expires ?? deadline.toISOString().slice(0, 10), requestId: requestId ?? null, trackingRef: trackingRef ?? null };
|
|
209
257
|
return { schema: APPROVAL_SCHEMA, payload, signature: null };
|
|
210
258
|
}
|
|
211
259
|
|
|
@@ -234,25 +282,25 @@ export function validateOwnershipFloor(trust, { scannerVersion, catalogVersion,
|
|
|
234
282
|
if (trust.minimumCatalogDigest && trust.minimumCatalogDigest !== catalogDigest) problems.push("catalog digest below protected pinned digest");
|
|
235
283
|
return { ok: problems.length === 0, problems };
|
|
236
284
|
}
|
|
237
|
-
function protectedLocation(file, platform) {
|
|
285
|
+
function protectedLocation(file, platform, { runner = spawnSync, inspect = lstatSync, stat = statSync, requireNonElevated = true } = {}) {
|
|
238
286
|
const problems = [];
|
|
239
287
|
if (platform === "win32") {
|
|
240
288
|
const dirs = [];
|
|
241
289
|
let current = path.dirname(file);
|
|
242
290
|
while (true) {
|
|
243
291
|
dirs.push(current);
|
|
244
|
-
if (
|
|
292
|
+
if (path.dirname(current) === current) break;
|
|
245
293
|
current = path.dirname(current);
|
|
246
294
|
}
|
|
247
295
|
dirs.push(file);
|
|
248
296
|
for (const target of dirs) {
|
|
249
|
-
try { if (
|
|
297
|
+
try { if (inspect(target).isSymbolicLink()) problems.push(`trust path contains a reparse/symlink: ${target}`); } catch { problems.push(`cannot inspect trust path: ${target}`); continue; }
|
|
250
298
|
const script = "$ErrorActionPreference='Stop'; $a=Get-Acl -LiteralPath $env:RK_TRUST_CHECK_TARGET; $item=Get-Item -Force -LiteralPath $env:RK_TRUST_CHECK_TARGET; $id=[Security.Principal.WindowsIdentity]::GetCurrent(); $p=New-Object Security.Principal.WindowsPrincipal($id); $ownerSid=$a.GetOwner([Security.Principal.SecurityIdentifier]).Value; $ownerName=$a.Owner; $out=[ordered]@{ownerSID=$ownerSid;ownerName=$ownerName;attributes=$item.Attributes.ToString();directory=[bool]$item.PSIsContainer;nonempty=(!$item.PSIsContainer -or [System.IO.Directory]::EnumerateFileSystemEntries($item.FullName).GetEnumerator().MoveNext());elevated=$p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator);access=@($a.Access | ForEach-Object { [ordered]@{identitySID=$_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value;type=$_.AccessControlType.ToString();mask=[int64]$_.FileSystemRights.value__;propagationFlags=[int]$_.PropagationFlags} })}; $out | ConvertTo-Json -Compress -Depth 5";
|
|
251
|
-
const r =
|
|
299
|
+
const r = runner("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { encoding: "utf8", windowsHide: true, env: windowsPowerShellEnvironment({ RK_TRUST_CHECK_TARGET: target }) });
|
|
252
300
|
if (r.status !== 0) { problems.push(`cannot inspect trust ACL: ${target}`); continue; }
|
|
253
301
|
try {
|
|
254
302
|
const acl = JSON.parse(r.stdout.trim());
|
|
255
|
-
const result = evaluateWindowsAcl(acl, { target });
|
|
303
|
+
const result = evaluateWindowsAcl(acl, { target, requireNonElevated });
|
|
256
304
|
problems.push(...result.problems.map((problem) => `${problem}: ${target}`));
|
|
257
305
|
} catch { problems.push(`malformed ACL result: ${target}`); }
|
|
258
306
|
}
|
|
@@ -260,15 +308,15 @@ function protectedLocation(file, platform) {
|
|
|
260
308
|
}
|
|
261
309
|
let current = file;
|
|
262
310
|
while (true) {
|
|
263
|
-
try { if (
|
|
311
|
+
try { if (inspect(current).isSymbolicLink()) problems.push(`trust path contains a symlink: ${current}`); } catch { problems.push(`cannot inspect trust path: ${current}`); }
|
|
264
312
|
try {
|
|
265
|
-
const st =
|
|
313
|
+
const st = stat(current);
|
|
266
314
|
if (st.uid !== 0 || (st.mode & 0o022)) problems.push(`trust location is not root-owned/private: ${current}`);
|
|
267
315
|
} catch { problems.push(`cannot inspect trust location: ${current}`); }
|
|
268
|
-
const acl =
|
|
316
|
+
const acl = runner("/bin/ls", ["-lde", current], { encoding: "utf8", windowsHide: true });
|
|
269
317
|
if (acl.status !== 0) problems.push(`cannot inspect trust ACL: ${current}`);
|
|
270
318
|
else for (const line of acl.stdout.split(/\r?\n/).slice(1)) {
|
|
271
|
-
if (!/allow/i.test(line) || !/\b(?:write|delete|append|writeattr|writeextattr|
|
|
319
|
+
if (!/allow/i.test(line) || !/\b(?:writesecurity|chown|write|delete|append|writeattr|writeextattr|delete_child|add_file|add_subdirectory)\b/i.test(line)) continue;
|
|
272
320
|
if (!/^\s*\d+:\s+user:root\s+allow\b/i.test(line)) problems.push(`trust ACL grants write authority to a non-root principal: ${current}`);
|
|
273
321
|
}
|
|
274
322
|
const parent = path.dirname(current);
|
|
@@ -290,10 +338,12 @@ export function loadTrustedExternal() {
|
|
|
290
338
|
if (trust.schema !== 1) throw new Error("ownership trust file schema is unsupported");
|
|
291
339
|
const trustCheck = validateTrustRegistry(trust);
|
|
292
340
|
if (!trustCheck.ok) throw new Error(trustCheck.problems.join("; "));
|
|
341
|
+
const normalize = (id) => typeof id === "string" ? id.replace(/^(?:win-|mac-)/i, "").toLowerCase() : "";
|
|
342
|
+
const revoked = new Set((trust.revokedKeyIds ?? []).map(normalize));
|
|
293
343
|
for (const host of REQUIRED_HOSTS) {
|
|
294
344
|
const entry = trust.hosts?.[host];
|
|
295
345
|
if (!entry?.keyId && !entry?.publicKeyPem) continue;
|
|
296
|
-
if (!entry.keyId || !entry.publicKeyPem || entry.keyId !== keyIdForPublicKey(host, entry.publicKeyPem)) throw new Error(`invalid or mismatched ${host} approval key enrollment`);
|
|
346
|
+
if (!entry.keyId || !entry.publicKeyPem || normalize(entry.keyId) !== normalize(keyIdForPublicKey(host, entry.publicKeyPem))) throw new Error(`invalid or mismatched ${host} approval key enrollment`);
|
|
297
347
|
const key = createPublicKey(entry.publicKeyPem);
|
|
298
348
|
if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") throw new Error(`${host} approval key must be P-256`);
|
|
299
349
|
}
|
|
@@ -301,21 +351,28 @@ export function loadTrustedExternal() {
|
|
|
301
351
|
const hostKeys = REQUIRED_HOSTS.map((host) => trust.hosts?.[host]);
|
|
302
352
|
const enrolled = trust.rootFingerprint
|
|
303
353
|
? Boolean(HEX.test(trust.rootFingerprint) && trust.keyCheckpoint && Number.isSafeInteger(trust.keyCheckpoint.sequence) && trust.keyCheckpoint.sequence >= 0 && HEX.test(trust.keyCheckpoint.head ?? "")) // Provisional; scan paths replace this from verified log history.
|
|
304
|
-
: hostKeys.every((host) => host?.keyId && host.publicKeyPem && !
|
|
305
|
-
&& hostKeys[0]?.keyId !== hostKeys[1]?.keyId && hostKeys[0]?.publicKeyPem !== hostKeys[1]?.publicKeyPem;
|
|
354
|
+
: hostKeys.every((host) => host?.keyId && host.publicKeyPem && !revoked.has(normalize(host.keyId)))
|
|
355
|
+
&& normalize(hostKeys[0]?.keyId) !== normalize(hostKeys[1]?.keyId) && hostKeys[0]?.publicKeyPem !== hostKeys[1]?.publicKeyPem;
|
|
306
356
|
return { enrolled, pending: !enrolled, protection: protectionResult, trust, path: file };
|
|
307
357
|
}
|
|
308
358
|
|
|
309
359
|
// Test-only seam: production callers must use loadTrustedExternal's fixed canonical path.
|
|
310
|
-
export const __testOnly = Object.freeze({ protectedLocation });
|
|
360
|
+
export const __testOnly = Object.freeze({ protectedLocation, signatureProblems });
|
|
311
361
|
|
|
312
362
|
function signatureProblems(record, trust) {
|
|
313
|
-
const s = record
|
|
363
|
+
const s = record?.signature;
|
|
364
|
+
if (!s || !trust) return ["signature or protected trust is missing"];
|
|
365
|
+
if (!Array.isArray(trust.revokedKeyIds ?? [])) return ["protected revoked key IDs are malformed"];
|
|
366
|
+
if (trust.rootFingerprint && !Array.isArray(trust.ownershipKeys)) return ["protected ownership key authority is malformed"];
|
|
367
|
+
const ids = new Map();
|
|
368
|
+
const normalize = (id) => { if (!ids.has(id)) ids.set(id, typeof id === "string" ? id.replace(/^(?:win-|mac-)/i, "").toLowerCase() : ""); return ids.get(id); };
|
|
369
|
+
const normalizedId = normalize(s.keyId);
|
|
370
|
+
if (!HEX.test(normalizedId)) return ["signing key ID is malformed"];
|
|
371
|
+
const revoked = new Set((trust.revokedKeyIds ?? []).map(normalize));
|
|
314
372
|
if (!REQUIRED_HOSTS.includes(s.host)) return ["unknown signing host"];
|
|
315
|
-
if (
|
|
373
|
+
if (revoked.has(normalizedId)) return ["signing key is revoked"];
|
|
316
374
|
if (trust.rootFingerprint) {
|
|
317
|
-
const
|
|
318
|
-
const key = trust.ownershipKeys?.find((candidate) => candidate.keyId === normalizedId && candidate.platform === s.host);
|
|
375
|
+
const key = trust.ownershipKeys?.find((candidate) => normalize(candidate.keyId) === normalizedId && candidate.platform === s.host);
|
|
319
376
|
if (!key) return ["signing key is not active in protected ownership key log"];
|
|
320
377
|
try {
|
|
321
378
|
const valid = verifyOwnershipSignature(key.publicKeyPem, Buffer.from(canonicalJson(approvalPayload(record))), Buffer.from(s.value, "base64"));
|
|
@@ -323,12 +380,12 @@ function signatureProblems(record, trust) {
|
|
|
323
380
|
} catch { return ["malformed signature or public key"]; }
|
|
324
381
|
}
|
|
325
382
|
const host = trust.hosts?.[s.host];
|
|
326
|
-
if (!host || host.keyId !==
|
|
383
|
+
if (!host || normalize(host.keyId) !== normalizedId || !host.publicKeyPem) return ["signing key is not enrolled for host"];
|
|
327
384
|
const keys = REQUIRED_HOSTS.map((name) => trust.hosts?.[name]);
|
|
328
|
-
if (!keys.every((k) => k?.keyId && k.publicKeyPem && !
|
|
385
|
+
if (!keys.every((k) => k?.keyId && k.publicKeyPem && !revoked.has(normalize(k.keyId))) || normalize(keys[0]?.keyId) === normalize(keys[1]?.keyId) || keys[0]?.publicKeyPem === keys[1]?.publicKeyPem) return ["pending: distinct Windows and macOS approval keys must be enrolled"];
|
|
329
386
|
try {
|
|
330
387
|
const publicKey = createPublicKey(host.publicKeyPem);
|
|
331
|
-
if (
|
|
388
|
+
if (normalizedId !== normalize(keyIdForPublicKey(s.host, publicKey))) return ["signing key ID does not match enrolled public key"];
|
|
332
389
|
const valid = cryptoVerify("sha256", Buffer.from(canonicalJson(approvalPayload(record))), publicKey, Buffer.from(s.value, "base64"));
|
|
333
390
|
return valid ? [] : ["invalid signature"];
|
|
334
391
|
} catch { return ["malformed signature or public key"];
|
|
@@ -336,13 +393,28 @@ function signatureProblems(record, trust) {
|
|
|
336
393
|
}
|
|
337
394
|
|
|
338
395
|
/** Verify signature, protected enrollment, floor, and exact scan coverage. */
|
|
339
|
-
export function verifyException(record, { repositoryIdentity, catalogDigest, findings, trust, today = new Date().toISOString().slice(0, 10), scannerVersion, catalogVersion } = {}) {
|
|
396
|
+
export function verifyException(record, { repositoryIdentity, catalogDigest, policyDigest, findings, trust, today = new Date().toISOString().slice(0, 10), scannerVersion, catalogVersion } = {}) {
|
|
340
397
|
const problems = validateRecord(record);
|
|
341
398
|
if (problems.length) return { ok: false, problems };
|
|
342
|
-
if (!trust) return { ok: false, problems: ["pending: external ownership trust is not enrolled"] };
|
|
343
|
-
const p = signatureProblems(record, trust);
|
|
344
|
-
if (p.length) return { ok: false, problems: p };
|
|
345
399
|
const r = record.payload;
|
|
400
|
+
if (!validDate(today)) return { ok: false, problems: ["invalid verification date"] };
|
|
401
|
+
if (r.issued > today) problems.push("approval issued date is in the future");
|
|
402
|
+
const duration = Date.parse(`${r.expires}T00:00:00Z`) - Date.parse(`${r.issued}T00:00:00Z`);
|
|
403
|
+
if (duration <= 0 || duration > 90 * 86400000) problems.push("approval expiry must be after issued and within 90 days");
|
|
404
|
+
if (r.expires < today) problems.push(`approval expired ${r.expires}`);
|
|
405
|
+
if (problems.length) return { ok: false, problems };
|
|
406
|
+
if (!versionAtLeast(scannerVersion, r.minimumScannerVersion)) problems.push("scanner version missing, malformed, or below approval minimum");
|
|
407
|
+
if (!versionAtLeast(catalogVersion, r.minimumCatalogVersion)) problems.push("catalog version missing, malformed, or below approval minimum");
|
|
408
|
+
if (!trust) problems.push("pending: external ownership trust is not enrolled");
|
|
409
|
+
else problems.push(...validateOwnershipFloor(trust, { scannerVersion, catalogVersion, catalogDigest }).problems);
|
|
410
|
+
if (problems.length) return { ok: false, problems };
|
|
411
|
+
if (typeof policyDigest !== "string" || !HEX.test(policyDigest) || r.policyDigest !== policyDigest) return { ok: false, problems: ["ownership fingerprint policy digest missing or changed"] };
|
|
412
|
+
if (!Array.isArray(findings)) return { ok: false, problems: ["current findings missing or malformed"] };
|
|
413
|
+
let current;
|
|
414
|
+
try { current = findings.filter((f) => f.fingerprint === r.fingerprint).map((f) => ({ path: f.file, kind: exceptionOccurrenceKind(f), hash: occurrenceHash(f) })); }
|
|
415
|
+
catch (error) { return { ok: false, problems: [error.message] }; }
|
|
416
|
+
for (const item of r.occurrences) if (!current.some((f) => f.path === item.path && f.hash === item.hash && f.kind === item.kind)) problems.push(`baseline occurrence changed or missing: ${item.path} (${item.hash})`);
|
|
417
|
+
if (problems.length) return { ok: false, problems };
|
|
346
418
|
const trackingEvidence = sha256({ repositoryIdentity: r.repositoryIdentity, requestId: r.requestId, trackingRef: r.trackingRef });
|
|
347
419
|
if (!trust.verifiedTrackingEvidence?.includes(trackingEvidence)) problems.push("tracking reference lacks protected verification evidence");
|
|
348
420
|
let recordIdentity, currentIdentity;
|
|
@@ -352,15 +424,8 @@ export function verifyException(record, { repositoryIdentity, catalogDigest, fin
|
|
|
352
424
|
if (recordIdentity && currentIdentity && recordIdentity !== currentIdentity) problems.push("repository identity changed");
|
|
353
425
|
const registration = verifyRepositoryRegistration(trust, r.repositoryIdentity, { files: (findings ?? []).map((f) => f.file) });
|
|
354
426
|
problems.push(...registration.problems);
|
|
355
|
-
if (
|
|
356
|
-
if (
|
|
357
|
-
else if (r.expires < today) problems.push(`approval expired ${r.expires}`);
|
|
358
|
-
const floor = validateOwnershipFloor(trust, { scannerVersion, catalogVersion, catalogDigest });
|
|
359
|
-
problems.push(...floor.problems);
|
|
360
|
-
const selected = (findings ?? []).filter((f) => f.fingerprint === r.fingerprint && r.paths.includes(f.file));
|
|
361
|
-
const actual = selected.map(occurrenceHash).sort();
|
|
362
|
-
if (canonicalJson(actual) !== canonicalJson([...r.occurrenceHashes].sort())) problems.push("baseline occurrence set changed");
|
|
363
|
-
if (!actual.length) problems.push("approval is stale and matches no current occurrences");
|
|
427
|
+
if (record.signature === null) problems.push("unsigned approval record");
|
|
428
|
+
else if (!problems.length) problems.push(...signatureProblems(record, trust));
|
|
364
429
|
return { ok: problems.length === 0, problems };
|
|
365
430
|
}
|
|
366
431
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Publication evidence, rollout staging & adopted pins are separate states.
|
|
2
|
+
export function ownershipAvailability(entry, versions) {
|
|
3
|
+
const name = entry.package;
|
|
4
|
+
const ecosystem = name === "RightKitSwift" || name === "rightkit-swift" ? "swift"
|
|
5
|
+
: name?.startsWith("@rightkit/") ? "npm" : "cargo";
|
|
6
|
+
const key = ecosystem === "swift" ? "rightkit-swift" : name;
|
|
7
|
+
const stagedMap = { npm: "stagedNpm", cargo: "stagedCargo", swift: "stagedSwift" }[ecosystem];
|
|
8
|
+
const pinned = versions[ecosystem]?.[key];
|
|
9
|
+
const recorded = versions.registry?.[ecosystem]?.[key];
|
|
10
|
+
const staged = versions[stagedMap]?.[key];
|
|
11
|
+
const versionOf = (value) => typeof value === "string" ? value : value?.version;
|
|
12
|
+
const publishedVersion = versionOf(recorded ?? pinned);
|
|
13
|
+
const rows = [];
|
|
14
|
+
const add = (value, state) => {
|
|
15
|
+
const version = versionOf(value);
|
|
16
|
+
if (version) rows.push({ registry: { npm: "npm", cargo: "crates.io", swift: "SwiftPM" }[ecosystem], package: key, version, state, ...(value?.url ? { url: value.url } : {}) });
|
|
17
|
+
};
|
|
18
|
+
add(recorded ?? pinned, recorded ? "registry-recorded" : "registry-pinned");
|
|
19
|
+
if (versionOf(staged) && versionOf(staged) !== publishedVersion) add(staged, "staged-unverified");
|
|
20
|
+
return rows;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ownershipDetection(entry) {
|
|
24
|
+
return entry.detection ?? (entry.fingerprints?.length ? "fingerprinted" : "declaration-only");
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "3.0.0",
|
|
3
|
+
"newCapabilityApprovalFlag": "--approve-new-capabilities",
|
|
4
|
+
"approvalScope": "Local catalog generation only; installation still requires protected authorization. This flag cannot approve owner changes.",
|
|
5
|
+
"ownerFamilies": [
|
|
6
|
+
{ "capability": "agent-room", "packages": ["agent-room-core", "@rightkit/agent-room-mcp"], "reason": "Native room core & MCP adapter share room contract" },
|
|
7
|
+
{ "capability": "licensing", "packages": ["rightkit-license", "@rightkit/license"], "reason": "Native entitlement enforcement & renderer/network client have separate roles" },
|
|
8
|
+
{ "capability": "structured-logging", "packages": ["rightkit-logs", "@rightkit/logs"], "reason": "Native JSONL writer & typed renderer invoke adapter" },
|
|
9
|
+
{ "capability": "text-to-speech", "packages": ["rightkit-qts", "rightkit-qts-ggml", "rightkit-qts-ggml-sys", "rightkit-tts"], "reason": "Speech facade & its QTS inference, safe-wrapper & FFI layers" },
|
|
10
|
+
{ "capability": "os-credential-store", "packages": ["rightkit-secrets", "RightKitSwift"], "reason": "Cross-platform Rust & Apple-native Swift credential APIs" },
|
|
11
|
+
{ "capability": "desktop-shell", "packages": ["rightkit-shell", "@rightkit/shell"], "reason": "Native Tauri shell & typed renderer bridge" },
|
|
12
|
+
{ "capability": "tauri-host-adapters", "packages": ["rightkit-tauri", "@rightkit/tauri"], "reason": "Native license/device services & renderer command adapters" }
|
|
13
|
+
],
|
|
14
|
+
"testOwners": [
|
|
15
|
+
{ "path": "crates/rightkit-control/examples/control-test-app/Cargo.toml", "package": "control-test-app", "reason": "Unpublished desktop-control test fixture" },
|
|
16
|
+
{ "path": "crates/rightkit-shell/example-app/Cargo.toml", "package": "shell-example-app", "reason": "Unpublished standalone shell demonstration & test host" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse as parseToml } from "smol-toml";
|
|
5
|
+
import { ownershipAvailability } from "./ownership-availability.mjs";
|
|
6
|
+
|
|
7
|
+
const releaseRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
export const CATALOG_CONFIG = JSON.parse(readFileSync(new URL("./ownership-catalog-config.json", import.meta.url)));
|
|
9
|
+
const json = (file) => JSON.parse(readFileSync(file, "utf8"));
|
|
10
|
+
|
|
11
|
+
export function validateCatalogChanges(previous, next, { approveNewCapabilities = false } = {}) {
|
|
12
|
+
const owners = new Map();
|
|
13
|
+
for (const entry of previous.capabilities ?? []) {
|
|
14
|
+
if (!owners.has(entry.capability)) owners.set(entry.capability, new Set());
|
|
15
|
+
owners.get(entry.capability).add(entry.package);
|
|
16
|
+
}
|
|
17
|
+
const newOwners = new Map();
|
|
18
|
+
const declarations = new Set();
|
|
19
|
+
for (const entry of next.capabilities) {
|
|
20
|
+
const declaration = `${entry.capability}:${entry.package}`;
|
|
21
|
+
if (declarations.has(declaration)) throw new Error(`ownership: duplicate owner declaration ${declaration}`);
|
|
22
|
+
declarations.add(declaration);
|
|
23
|
+
const known = owners.get(entry.capability);
|
|
24
|
+
if (known && !known.has(entry.package)) throw new Error(`ownership: ${entry.capability} already belongs to ${[...known].join(", ")}; refusing ${entry.package}`);
|
|
25
|
+
if (!known && !approveNewCapabilities) throw new Error(`ownership: new capability ${entry.capability} requires ${CATALOG_CONFIG.newCapabilityApprovalFlag}`);
|
|
26
|
+
if (!known && newOwners.has(entry.capability) && newOwners.get(entry.capability) !== entry.package) throw new Error(`ownership: conflicting new owners for ${entry.capability}`);
|
|
27
|
+
newOwners.set(entry.capability, entry.package);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Package declarations are canonical. Availability always comes from versions,
|
|
32
|
+
// never from package versions, descriptions, or generated consumer snapshots.
|
|
33
|
+
export function generateOwnershipCatalog(root) {
|
|
34
|
+
const capabilities = [], ids = new Set();
|
|
35
|
+
const versions = json(path.join(root, "packages/release/rightkit-versions.json"));
|
|
36
|
+
for (const group of ["crates", "packages", "swift"]) {
|
|
37
|
+
const parent = path.join(root, group);
|
|
38
|
+
if (!existsSync(parent)) continue;
|
|
39
|
+
for (const name of readdirSync(parent).sort()) {
|
|
40
|
+
const dir = path.join(parent, name);
|
|
41
|
+
const file = path.join(dir, name === "release" && group === "packages" ? "ownership-declaration.json" : "rightkit-ownership.json");
|
|
42
|
+
if (!existsSync(file)) continue;
|
|
43
|
+
const declaration = json(file);
|
|
44
|
+
if (declaration.schema !== undefined) throw new Error(`ownership: generated catalog cannot be an owner declaration: ${file}`);
|
|
45
|
+
const actual = group === "crates" ? parseToml(readFileSync(path.join(dir, "Cargo.toml"), "utf8")).package?.name
|
|
46
|
+
: group === "packages" ? json(path.join(dir, "package.json")).name : existsSync(path.join(dir, "Package.swift")) ? /\bname\s*:\s*"([^"]+)"/.exec(readFileSync(path.join(dir, "Package.swift"), "utf8"))?.[1] : null;
|
|
47
|
+
if (!actual || declaration.package !== actual) throw new Error(`ownership: declaration owner differs from package metadata: ${file}`);
|
|
48
|
+
for (const entry of declaration.capabilities ?? []) {
|
|
49
|
+
if (entry.package !== undefined || !/^[a-z0-9][a-z0-9._-]{1,127}$/.test(entry.capability) || !Array.isArray(entry.fingerprints)) throw new Error(`ownership: malformed declaration: ${file}`);
|
|
50
|
+
const owner = { package: actual, ...entry };
|
|
51
|
+
if (owner.detection !== (owner.fingerprints.length ? "fingerprinted" : "declaration-only")) throw new Error(`ownership: explicit detection required: ${file}`);
|
|
52
|
+
for (const f of owner.fingerprints) {
|
|
53
|
+
if (!/^[a-z0-9][a-z0-9._-]{1,127}$/.test(f.id) || ids.has(f.id)) throw new Error(`ownership: invalid or duplicate fingerprint ${f.id}`);
|
|
54
|
+
ids.add(f.id);
|
|
55
|
+
if (!["block", "advise"].includes(f.tier) || !f.usageContext || !f.tierReason) throw new Error(`ownership: per-fingerprint tier & context required: ${f.id}`);
|
|
56
|
+
if (f.type === "dependency") {
|
|
57
|
+
if (!["cargo", "npm", "swiftpm"].includes(f.ecosystem) || !f.name || /^(@rightkit\/|rightkit-)/.test(f.name)) throw new Error(`ownership: imprecise dependency: ${f.id}`);
|
|
58
|
+
} else if (f.type === "source") {
|
|
59
|
+
if (!f.codeOnly || !f.extensions?.length) throw new Error(`ownership: source fingerprint must be code & extension scoped: ${f.id}`);
|
|
60
|
+
new RegExp(f.pattern);
|
|
61
|
+
} else throw new Error(`ownership: unknown fingerprint type: ${f.id}`);
|
|
62
|
+
// A banned dependency blocks regardless of owner qualification: the ban stands even
|
|
63
|
+
// when no replacement is used. It names the authority that imposed it.
|
|
64
|
+
if (f.tier === "block" && f.policy === "banned") {
|
|
65
|
+
if (f.type !== "dependency" || !f.policyAuthority) throw new Error(`ownership: banned fingerprint needs a dependency & policyAuthority: ${f.id}`);
|
|
66
|
+
} else if (f.tier === "block") {
|
|
67
|
+
const published = ownershipAvailability(owner, versions).find((row) => row.state !== "staged-unverified");
|
|
68
|
+
const q = f.qualification;
|
|
69
|
+
if (!published || q?.version !== published.version || !["macos", "windows"].every((host) => q.hosts?.includes(host)) || !q.evidence?.length) throw new Error(`ownership: block fingerprint lacks published two-host qualification: ${f.id}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
capabilities.push(owner);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const families = new Map();
|
|
77
|
+
for (const entry of capabilities) {
|
|
78
|
+
if (!families.has(entry.capability)) families.set(entry.capability, new Set());
|
|
79
|
+
families.get(entry.capability).add(entry.package);
|
|
80
|
+
}
|
|
81
|
+
for (const [capability, owners] of families) if (owners.size > 1) {
|
|
82
|
+
const family = CATALOG_CONFIG.ownerFamilies.find((entry) => entry.capability === capability);
|
|
83
|
+
if (!family?.reason || owners.size !== family.packages.length || [...owners].some((owner) => !family.packages.includes(owner))) throw new Error(`ownership: conflicting owner family ${capability}`);
|
|
84
|
+
}
|
|
85
|
+
const internalOwners = json(path.join(root, "packages/release/ownership-internal-owners.json")).owners;
|
|
86
|
+
return { schema: 1, version: CATALOG_CONFIG.version, capabilities, internalOwners };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function writeOwnershipCatalog(root, { approveNewCapabilities = false, previous = json(path.join(releaseRoot, "rightkit-ownership.json")), output = path.join(root, "packages/release/rightkit-ownership.json") } = {}) {
|
|
90
|
+
const catalog = generateOwnershipCatalog(root);
|
|
91
|
+
validateCatalogChanges(previous, catalog, { approveNewCapabilities });
|
|
92
|
+
writeFileSync(output, `${JSON.stringify(catalog, null, 2)}\n`);
|
|
93
|
+
return catalog;
|
|
94
|
+
}
|
package/ownership-command.mjs
CHANGED
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
// the shipped manifest from each package's declaration.
|
|
4
4
|
//
|
|
5
5
|
// right-release ownership [--app <dir>] [--json]
|
|
6
|
-
// right-release ownership --write-manifest [--rightkit <tools/rightkit>]
|
|
7
|
-
|
|
6
|
+
// right-release ownership --write-manifest [--rightkit <tools/rightkit>] [--approve-new-capabilities]
|
|
7
|
+
// --approve-new-capabilities permits new ids during local generation only.
|
|
8
|
+
// Existing owner changes are always refused; installation needs protected authorization.
|
|
9
|
+
import { realpathSync } from "node:fs";
|
|
8
10
|
import path from "node:path";
|
|
9
11
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import {
|
|
12
|
+
import { formatTable } from "./ownership.mjs";
|
|
13
|
+
import { writeOwnershipCatalog } from "./ownership-catalog.mjs";
|
|
11
14
|
import { createOwnershipEvidence } from "./ownership-evidence.mjs";
|
|
12
15
|
import { spawnSync } from "./background-process.mjs";
|
|
16
|
+
import { main as stagedMain } from "./ownership-staged.mjs";
|
|
13
17
|
|
|
14
18
|
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
-
const shipped = path.join(packageRoot, MANIFEST_FILE);
|
|
16
19
|
|
|
17
20
|
function flag(args, name) {
|
|
18
21
|
const i = args.indexOf(name);
|
|
@@ -20,17 +23,21 @@ function flag(args, name) {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export async function main(args, { evidenceFactory = createOwnershipEvidence, gitRunner = spawnSync, print = (value) => console.log(value) } = {}) {
|
|
23
|
-
if (
|
|
26
|
+
if (args.includes("--help")) {
|
|
27
|
+
print("right-release ownership [--app <dir>] [--json]\nright-release ownership --staged [--app <dir>] [--json] (pre-commit gate: scans the index, not the working tree)\nright-release ownership --write-manifest [--rightkit <dir>] [--approve-new-capabilities]\n--approve-new-capabilities permits new capability ids during local generation only. Existing owner conflicts are always refused. Protected installation authorization remains separate.");
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
if (args.includes("--staged")) return stagedMain(args, { print });
|
|
31
|
+
if (["enroll", "approve-key", "revoke-key", "trust-sync"].includes(args[0])) {
|
|
24
32
|
const { main: keyMain } = await import("./ownership-key-command.mjs");
|
|
25
33
|
return keyMain(args, { runner: gitRunner, print });
|
|
26
34
|
}
|
|
27
35
|
if (args.includes("--write-manifest")) {
|
|
28
36
|
const root = path.resolve(flag(args, "--rightkit") ?? path.join(packageRoot, "..", ".."));
|
|
29
|
-
const manifest =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
console.log(`[right-release] ownership manifest: ${manifest.capabilities.length} capabilities -> ${shipped}`);
|
|
37
|
+
const manifest = writeOwnershipCatalog(root, { approveNewCapabilities: args.includes("--approve-new-capabilities") });
|
|
38
|
+
const { syncConsumerCopies } = await import("./rightkit-sync.mjs");
|
|
39
|
+
syncConsumerCopies(root);
|
|
40
|
+
print(`[right-release] ownership manifest: ${manifest.capabilities.length} owner rows -> ${path.join(root, "packages/release/rightkit-ownership.json")}`);
|
|
34
41
|
return 0;
|
|
35
42
|
}
|
|
36
43
|
const app = path.resolve(flag(args, "--app") ?? process.cwd());
|
|
@@ -57,6 +64,9 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
|
|
|
57
64
|
for (const p of result.problems) print(` ${checked.enrollmentPending ? "DIAGNOSTIC" : "EXCEPTION"} ${p}`);
|
|
58
65
|
print(checked.enrollmentPending ? `[right-release] ${checked.warning}` : checked.ok ? "[right-release] ownership passed" : `[right-release] ownership FAILED: ${[...result.problems, ...checked.floor.problems].join("; ") || "move these to RightKit or add a reviewed, dated exception"}`);
|
|
59
66
|
}
|
|
67
|
+
// CI lanes may pass `--advisory-when-pending`: before approval keys are enrolled the scan asserts
|
|
68
|
+
// nothing, so findings print as diagnostics and the step exits 0. Release lanes never pass it.
|
|
69
|
+
if (checked.enrollmentPending && args.includes("--advisory-when-pending")) return 0;
|
|
60
70
|
return checked.ok ? 0 : 1;
|
|
61
71
|
}
|
|
62
72
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"package": "@rightkit/release",
|
|
3
|
+
"capabilities": [
|
|
4
|
+
{
|
|
5
|
+
"capability": "release-tooling",
|
|
6
|
+
"summary": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.; declaration-only detection (no precise implementation fingerprint)",
|
|
7
|
+
"fingerprints": [],
|
|
8
|
+
"detection": "declaration-only",
|
|
9
|
+
"policyNotes": [
|
|
10
|
+
"Package contract retained; dependency declarations do not verify integration.",
|
|
11
|
+
"No precise implementation fingerprint; declaration-only inventory, never enforced ownership or verified integration."
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -6,16 +6,6 @@
|
|
|
6
6
|
"minimumCatalogDigest": "875594919178efe146b32276cb21e21a715ca787fb1cc39db425b21bafb02df6",
|
|
7
7
|
"revokedKeyIds": [],
|
|
8
8
|
"verifiedTrackingEvidence": [],
|
|
9
|
-
"repositories": [
|
|
10
|
-
{ "repositoryIdentity": "https://github.com/bogusyogi/coderight", "roots": ["."], "visibility": "private" },
|
|
11
|
-
{ "repositoryIdentity": "https://github.com/bogusyogi/cutright", "roots": ["."], "visibility": "private" },
|
|
12
|
-
{ "repositoryIdentity": "https://github.com/bogusyogi/genright", "roots": ["."], "visibility": "private" },
|
|
13
|
-
{ "repositoryIdentity": "https://github.com/bogusyogi/heardright", "roots": ["."], "visibility": "private" },
|
|
14
|
-
{ "repositoryIdentity": "https://github.com/bogusyogi/mailright", "roots": ["."], "visibility": "private" },
|
|
15
|
-
{ "repositoryIdentity": "https://github.com/bogusyogi/rightsites", "roots": ["."], "visibility": "private" },
|
|
16
|
-
{ "repositoryIdentity": "https://github.com/bogusyogi/scraperight", "roots": ["."], "visibility": "private" },
|
|
17
|
-
{ "repositoryIdentity": "https://github.com/bogusyogi/viewright", "roots": ["."], "visibility": "private" },
|
|
18
|
-
{ "repositoryIdentity": "https://github.com/bogusyogi/voiceright", "roots": ["."], "visibility": "private" }
|
|
19
|
-
],
|
|
9
|
+
"repositories": [],
|
|
20
10
|
"provenance": []
|
|
21
11
|
}
|
package/ownership-evidence.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync, realpathSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { spawnSync } from "./background-process.mjs";
|
|
6
|
-
import { catalogDigest as scanCatalogDigest, checkApp, checkCommittedApp, MANIFEST_FILE, SCANNER_VERSION } from "./ownership.mjs";
|
|
6
|
+
import { catalogDigest as scanCatalogDigest, checkApp, checkAppForTest, checkCommittedApp, checkCommittedAppForTest, MANIFEST_FILE, SCANNER_VERSION } from "./ownership.mjs";
|
|
7
7
|
import { acquireCommittedSource, assertCommittedCheckoutClean, committedSourceDataForInternalUse } from "./committed-source.mjs";
|
|
8
8
|
import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
|
|
9
9
|
import { canonicalRepositoryIdentity, loadTrustedExternal, resolveKeyLogTrust, sha256, validateOwnershipFloor, verifyRepositoryRegistration } from "./ownership-approval.mjs";
|
|
@@ -29,7 +29,7 @@ function createEvidence(repoRoot, {
|
|
|
29
29
|
committed = false,
|
|
30
30
|
requireCleanCheckout = false,
|
|
31
31
|
allowPendingAdvisory = false,
|
|
32
|
-
} = {}) {
|
|
32
|
+
} = {}, readAuthority = readCommittedKeyAuthority, scanCommitted = checkCommittedApp) {
|
|
33
33
|
const root = path.resolve(repoRoot);
|
|
34
34
|
const manifestBytes = readFileSync(manifestPath);
|
|
35
35
|
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
@@ -48,7 +48,7 @@ function createEvidence(repoRoot, {
|
|
|
48
48
|
const source = useCommittedSource ? acquireCommittedSource(root, { requiredPaths: [...(trustState.trust?.provenance ?? []).flatMap((record) => record.paths ?? []), ...(trustState.trust?.rootFingerprint && !trustState.trust?.keyAuthority ? [".rightkit/ownership-keys.json"] : [])] }) : null;
|
|
49
49
|
let keyLogEvidence = null;
|
|
50
50
|
if (trustState.trust?.keyAuthority) {
|
|
51
|
-
const resolved =
|
|
51
|
+
const resolved = readAuthority(trustState.trust);
|
|
52
52
|
if (!resolved.ok) throw new Error(`protected ownership key authority failed: ${resolved.problems.join("; ")}`);
|
|
53
53
|
trustState = { ...trustState, trust: resolved.resolvedTrust, enrolled: resolved.resolvedTrust.enrollmentComplete, pending: !resolved.resolvedTrust.enrollmentComplete };
|
|
54
54
|
enrollmentPending = trustState.pending;
|
|
@@ -116,7 +116,7 @@ function createEvidence(repoRoot, {
|
|
|
116
116
|
const scans = (scanRoots.length ? scanRoots : [requestedRoot]).map((registered) => {
|
|
117
117
|
const absolute = registered === "." ? root : path.resolve(root, ...registered.split("/"));
|
|
118
118
|
const scanned = useCommittedSource
|
|
119
|
-
?
|
|
119
|
+
? scanCommitted(absolute, manifest, { trust: trustState.trust, expectedCommit: commit, expectedTree: tree, source })
|
|
120
120
|
: scan(absolute, manifest, { trust: { trust: trustState.trust, enrolled: trustState.enrolled, pending: enrollmentPending }, scannerVersion, catalogVersion, ...(enrollmentPending ? { enrollmentPendingAdvisory: true } : {}) });
|
|
121
121
|
if (repositoryIdentity && canonicalRepositoryIdentity(scanned.metadata?.repositoryIdentity) !== repositoryIdentity) throw new Error(`ownership evidence: scanner observed a different repository identity under ${registered}`);
|
|
122
122
|
if (useCommittedSource && scanned.metadata?.commit !== commit) throw new Error(`ownership evidence: scanner observed a different source commit under ${registered}`);
|
|
@@ -171,7 +171,7 @@ function createEvidence(repoRoot, {
|
|
|
171
171
|
checkedAt: new Date().toISOString(),
|
|
172
172
|
};
|
|
173
173
|
const warning = enrollmentPending ? "WARNING: ownership approval enrollment is pending; findings remain backlog diagnostics and no protected evidence or approvals are asserted" : undefined;
|
|
174
|
-
return { ok: enrollmentPending
|
|
174
|
+
return { ok: !enrollmentPending && result.ok && floor.ok && contentCoverage.length === 0, ...(enrollmentPending ? { enrollmentPending: true, warning } : {}), evidence, result, floor, contentCoverage };
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/** Create scan evidence bound to committed source, catalog, and signed exceptions. */
|
|
@@ -186,12 +186,14 @@ export function createCommittedOwnershipEvidence(repoRoot, scanRoot = repoRoot)
|
|
|
186
186
|
|
|
187
187
|
// Injection seam is deliberately named for tests and is not used by release commands.
|
|
188
188
|
export function createOwnershipEvidenceForTest(repoRoot, options) {
|
|
189
|
-
|
|
189
|
+
const reader = options?.readAuthorityForTest;
|
|
190
|
+
return createEvidence(repoRoot, { ...options, scan: options?.scan ?? ((root, manifest, context) => checkAppForTest(root, manifest, { ...context, readAuthorityForTest: reader })) }, reader,
|
|
191
|
+
(root, manifest, context) => checkCommittedAppForTest(root, manifest, { ...context, readAuthorityForTest: reader }));
|
|
190
192
|
}
|
|
191
193
|
|
|
192
194
|
/** Test-only access to the production committed-tree evidence path. */
|
|
193
195
|
export function createCommittedOwnershipEvidenceForTest(repoRoot, options = {}) {
|
|
194
|
-
return
|
|
196
|
+
return createOwnershipEvidenceForTest(repoRoot, { ...options, committed: true });
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
/** Entry point for an OS-protected local hook/adapter on private repositories. */
|
|
@@ -207,7 +209,7 @@ export function verifyOwnershipEvidence(expected, repoRoot, scanRoot = repoRoot)
|
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
export function verifyOwnershipEvidenceForTest(expected, repoRoot, options) {
|
|
210
|
-
return verifyEvidence(expected, repoRoot, options.scanRoot ?? repoRoot, () =>
|
|
212
|
+
return verifyEvidence(expected, repoRoot, options.scanRoot ?? repoRoot, () => createOwnershipEvidenceForTest(repoRoot, options));
|
|
211
213
|
}
|
|
212
214
|
|
|
213
215
|
function verifyEvidence(expected, repoRoot, _scanRoot, createCurrent) {
|