@rightkit/release 0.2.143 → 0.2.144

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/build-release.mjs CHANGED
@@ -24,6 +24,7 @@ import { spawn, spawnSync } from "./background-process.mjs";
24
24
  import { fileURLToPath, pathToFileURL } from "node:url";
25
25
  import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
26
26
  import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
27
+ import { assertSourceQaAdmission } from "./release-qa-receipt.mjs";
27
28
  import { commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine, verifySealedRelease, watchProgress } from "./release-state.mjs";
28
29
  import { acquireCacheLease, acquireSuiteBuildSlot, applyCachePrune, assertWriteVolumeFloors, inspectCache, inspectWriteVolumes, markCacheEntrySuccessful, planCachePrune, readCachePolicy, resolveCacheLayout, resolveSharedCacheIdentity, resolveSharedCacheRoot } from "./cache-policy.mjs";
29
30
  import { createTargetBridge } from "./target-bridge.mjs";
@@ -253,6 +254,15 @@ try {
253
254
  for (const check of preflight) {
254
255
  if (check.status !== "ok") console.error(`[right-release] preflight ${check.name}: ${check.detail}`);
255
256
  }
257
+ // Hardening step 4: source QA admits the candidate build/sign. The receipt digest is sealed into the manifest so
258
+ // upload/publication can require installed QA chained to it. Dev lanes (install-app --dev) never reach this file.
259
+ let qaAdmission;
260
+ try {
261
+ qaAdmission = assertSourceQaAdmission({ repoRoot: layout.repoRoot, releaseId, platform, configName: path.relative(process.cwd(), configPath) || path.basename(configPath), releaseConfigSha256: hashFile(configPath) });
262
+ } catch (error) {
263
+ if (error?.code !== "QA_ADMISSION_REFUSED") throw error;
264
+ fail(error.message);
265
+ }
256
266
 
257
267
  // A tool being INSTALLED is not the same as it being the one PATH picks. The
258
268
  // vendored-OpenSSL build shells out to `perl`, and under Git Bash/MSYS that
@@ -396,7 +406,7 @@ try {
396
406
  });
397
407
  },
398
408
  seal: async ({ sealedDir }) => {
399
- sealRelease({ configRoot: appRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, managedRoot });
409
+ sealRelease({ configRoot: appRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot });
400
410
  verifySealedRelease(sealedDir);
401
411
  if (cacheMode === "shared") markCacheEntrySuccessful({ layout: sharedLayout });
402
412
  checkpoint(stateRoot, "sealed");
@@ -436,7 +446,7 @@ function sqlCipherFeatures(text) {
436
446
  .filter((value) => /sqlcipher|openssl/i.test(value));
437
447
  }
438
448
 
439
- function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, managedRoot }) {
449
+ function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence, qaAdmission, managedRoot }) {
440
450
  // Every app's right-release.config.mjs names installer/updater artifacts
441
451
  // relative to its resolved native target. On a broker-managed host that
442
452
  // directory is never populated (the broker owns CARGO_TARGET_DIR and
@@ -507,6 +517,7 @@ function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir,
507
517
  version: config.version,
508
518
  commit,
509
519
  ownershipEvidence,
520
+ qa: { sourceReceiptDigest: qaAdmission.digest, floorDigest: qaAdmission.floorDigest },
510
521
  platform,
511
522
  cacheKey,
512
523
  // Portable contracts sign only raw executables — installer/embedding
@@ -23,6 +23,7 @@ const commands = new Map([
23
23
  ["mirror-root-artifact", "mirror-root-artifact.mjs"],
24
24
  ["github", "github-release.mjs"],
25
25
  ["cargo-target", "cargo-target.mjs"],
26
+ ["qa", "release-qa-command.mjs"],
26
27
  ["ownership", "ownership-command.mjs"],
27
28
  ["ownership-approval", "ownership-approval-command.mjs"],
28
29
  ["ownership-report", "ownership-report-command.mjs"],
@@ -183,6 +184,7 @@ Commands:
183
184
  Stage SwiftPM app; build+sign MSIX (Windows) or assemble+sign+notarize .app (macOS); never uploads
184
185
  upload --release <id> --platform mac|win --tier patch|update
185
186
  Upload one sealed release; never builds
187
+ qa --stage source|installed --platform mac|win [--release <id>] Run rightkit-qa and write the sealed QA receipt (required to build/upload)
186
188
  publish-update --release <id> --platform mac|win --tier patch|update
187
189
  Upload sealed bytes to R2 & register RightApps
188
190
  release Compatibility alias for build
@@ -212,6 +214,7 @@ Commands:
212
214
  ownership approve-key <fingerprint|request.json> Approve one pending device key from an active host
213
215
  ownership revoke-key <fingerprint> Revoke one active device key
214
216
  ownership trust-sync Advance this host's protected trust to the committed key log
217
+ ownership trust-compare [--json] Print this host's protected-trust digest (Mac is the reference)
215
218
  ownership-approval --draft <input.json> [--out <draft.json>]
216
219
  Draft an exception for native owner approval
217
220
  ownership-approval --verify <signed.json> --context <scan.json>
@@ -7,6 +7,7 @@ import { readVerifiedArtifact, assertArtifactUnchanged, digestBytes, verifyArtif
7
7
  import { spawnSync } from "./background-process.mjs";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { verifySealedRelease } from "./release-state.mjs";
10
+ import { assertInstalledQaAdmission } from "./release-qa-receipt.mjs";
10
11
  import { addonManifestSha256, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
11
12
  import { RIGHTRELEASE_MANIFEST_SIGNER, verifyManifestCatalogSignature } from "./sign-release-manifest.mjs";
12
13
 
@@ -394,6 +395,14 @@ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(
394
395
  plan = prepareGitHubAddonRelease({ repoRoot, config, configRoot: path.dirname(configPath), ...options });
395
396
  } else {
396
397
  options.repo ||= repositoryFromRemote(repoRoot);
398
+ // Hardening step 4: the direct GitHub lane is a publication path too; it needs installed QA like `upload` does.
399
+ if (!options.dryRun) {
400
+ const sealed = verifySealedRelease(path.join(repoRoot, ".right-release", "sealed", options.releaseId, options.platform === "win" ? "windows" : "mac"));
401
+ const configPath = path.resolve("right-release.config.mjs");
402
+ try {
403
+ assertInstalledQaAdmission({ repoRoot, manifest: sealed.manifest, platform: options.platform, configName: "right-release.config.mjs", releaseConfigSha256: existsSync(configPath) ? createHash("sha256").update(readFileSync(configPath)).digest("hex") : undefined });
404
+ } catch (error) { if (error?.code !== "QA_ADMISSION_REFUSED") throw error; console.error(`right-release github: ${error.message}`); process.exit(1); }
405
+ }
397
406
  plan = prepareGitHubRelease({ repoRoot, ...options });
398
407
  }
399
408
  const result = publishGitHubRelease(plan, options);
@@ -4,6 +4,7 @@ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { spawnSync, windowsPowerShellEnvironment } from "./background-process.mjs";
6
6
  import { verifyKeyLog, verifyOwnershipSignature } from "./ownership-key-log.mjs";
7
+ import { validateAllowlist } from "./ownership-dependency-allowlist.mjs";
7
8
 
8
9
  export const APPROVAL_SCHEMA = 2;
9
10
  export const REQUIRED_HOSTS = Object.freeze(["windows", "macos"]);
@@ -171,6 +172,16 @@ export function validateTrustRegistry(trust) {
171
172
  if (!Array.isArray(trust?.revokedKeyIds) || !Array.isArray(trust?.verifiedTrackingEvidence) || trust.verifiedTrackingEvidence.some((x) => !HEX.test(x))) problems.push("trust revocation or tracking evidence lists are malformed");
172
173
  if (!/^\d+(?:\.\d+)*$/.test(trust?.minimumScannerVersion ?? "") || !/^\d+(?:\.\d+)*$/.test(trust?.minimumCatalogVersion ?? "")) problems.push("scanner/catalog floors are missing or malformed");
173
174
  if (trust?.minimumCatalogDigest !== undefined && !HEX.test(trust.minimumCatalogDigest)) problems.push("minimum catalog digest is malformed");
175
+ // Policy bundle + allowlist are optional so trust written before they existed stays valid (treated as unpinned).
176
+ if (trust?.policyBundle !== undefined) {
177
+ const bundle = trust.policyBundle;
178
+ if (!bundle || typeof bundle !== "object" || Array.isArray(bundle) || bundle.schema !== 1 || !HEX.test(bundle.digest ?? "") || !HEX.test(bundle.catalogDigest ?? "")
179
+ || !/^\d+(?:\.\d+)*$/.test(bundle.scannerVersion ?? "") || !Array.isArray(bundle.files) || bundle.files.some((file) => typeof file?.path !== "string" || !HEX.test(file.sha256 ?? ""))) problems.push("protected policy bundle is malformed");
180
+ }
181
+ if (trust?.dependencyAllowlist !== undefined) {
182
+ const allowlist = validateAllowlist(trust.dependencyAllowlist);
183
+ if (!allowlist.ok) problems.push(`protected dependency allowlist is malformed: ${allowlist.problems[0]}`);
184
+ }
174
185
  if (trust?.rootFingerprint !== undefined || trust?.keyCheckpoint !== undefined) {
175
186
  if (!HEX.test(trust.rootFingerprint ?? "") || !trust.keyCheckpoint || !Number.isSafeInteger(trust.keyCheckpoint.sequence) || trust.keyCheckpoint.sequence < 0 || !HEX.test(trust.keyCheckpoint.head ?? "")) problems.push("protected key-log root pin or checkpoint is malformed");
176
187
  }
@@ -274,12 +285,14 @@ function versionAtLeast(actual, minimum) {
274
285
  }
275
286
  return true;
276
287
  }
277
- export function validateOwnershipFloor(trust, { scannerVersion, catalogVersion, catalogDigest } = {}) {
288
+ export function validateOwnershipFloor(trust, { scannerVersion, catalogVersion, catalogDigest, policyBundleDigest } = {}) {
278
289
  const problems = [];
279
290
  if (!trust) return { ok: false, problems: ["external ownership trust unavailable"] };
280
291
  if (!versionAtLeast(scannerVersion, trust.minimumScannerVersion)) problems.push("scanner version missing, malformed, or below protected minimum");
281
292
  if (!versionAtLeast(catalogVersion, trust.minimumCatalogVersion)) problems.push("catalog version missing, malformed, or below protected minimum");
282
293
  if (trust.minimumCatalogDigest && trust.minimumCatalogDigest !== catalogDigest) problems.push("catalog digest below protected pinned digest");
294
+ // Trust written before the bundle existed has no policyBundle and stays unpinned until the next trust-sync.
295
+ if (trust.policyBundle?.digest && policyBundleDigest !== undefined && trust.policyBundle.digest !== policyBundleDigest) problems.push("ownership policy bundle (scanner source, catalog, registry or dependency allowlist) differs from the protected pinned bundle; run trust-sync");
283
296
  return { ok: problems.length === 0, problems };
284
297
  }
285
298
  function protectedLocation(file, platform, { runner = spawnSync, inspect = lstatSync, stat = statSync, requireNonElevated = true } = {}) {
@@ -3,7 +3,9 @@
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>] [--approve-new-capabilities]
6
+ // right-release ownership [--app <dir>] --audit-json (Legion governance.capability-ownership provider)
7
+ // right-release ownership trust-compare [--json] (print this host's protected-trust digest)
8
+ // right-release ownership --write-manifest [--rightkit <rightkit>] [--approve-new-capabilities]
7
9
  // --approve-new-capabilities permits new ids during local generation only.
8
10
  // Existing owner changes are always refused; installation needs protected authorization.
9
11
  import { realpathSync } from "node:fs";
@@ -14,6 +16,8 @@ import { writeOwnershipCatalog } from "./ownership-catalog.mjs";
14
16
  import { createOwnershipEvidence } from "./ownership-evidence.mjs";
15
17
  import { spawnSync } from "./background-process.mjs";
16
18
  import { main as stagedMain } from "./ownership-staged.mjs";
19
+ import { loadTrustedExternal } from "./ownership-approval.mjs";
20
+ import { trustComparison } from "./ownership-policy-bundle.mjs";
17
21
 
18
22
  const packageRoot = path.dirname(fileURLToPath(import.meta.url));
19
23
 
@@ -22,11 +26,23 @@ function flag(args, name) {
22
26
  return i >= 0 ? args[i + 1] : undefined;
23
27
  }
24
28
 
25
- export async function main(args, { evidenceFactory = createOwnershipEvidence, gitRunner = spawnSync, print = (value) => console.log(value) } = {}) {
29
+ /** Print this host's protected-trust digest. The Mac's digest is the reference; see docs/OWNERSHIP-ENFORCEMENT.md. */
30
+ export function trustCompare(args, { print, trustLoader = loadTrustedExternal } = {}) {
31
+ let state;
32
+ try { state = trustLoader(); } catch (error) { print(`[right-release] trust-compare: protected trust unreadable (${error.message})`); return 1; }
33
+ if (!state?.trust) { print("[right-release] trust-compare: no protected trust on this host (not enrolled)"); return 1; }
34
+ const { digest, components } = trustComparison(state.trust);
35
+ if (args.includes("--json")) print(JSON.stringify({ platform: process.platform, digest, components }, null, 2));
36
+ else { print(`trust digest (${process.platform}): ${digest}`); for (const [name, value] of Object.entries(components)) print(` ${name}: ${typeof value === "object" && value !== null ? JSON.stringify(value) : value ?? "none"}`); }
37
+ return 0;
38
+ }
39
+
40
+ export async function main(args, { evidenceFactory = createOwnershipEvidence, gitRunner = spawnSync, print = (value) => console.log(value), trustLoader = loadTrustedExternal } = {}) {
26
41
  if (args.includes("--help")) {
27
42
  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
43
  return 0;
29
44
  }
45
+ if (args[0] === "trust-compare") return trustCompare(args, { print, trustLoader });
30
46
  if (args.includes("--staged")) return stagedMain(args, { print });
31
47
  if (["enroll", "approve-key", "revoke-key", "trust-sync"].includes(args[0])) {
32
48
  const { main: keyMain } = await import("./ownership-key-command.mjs");
@@ -48,6 +64,7 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
48
64
  try { checked = evidenceFactory(repoRoot, app); }
49
65
  catch (error) { return failClosed(args, "protected-evidence", error.message, print); }
50
66
  const result = checked.result;
67
+ if (args.includes("--audit-json")) return printAuditJson(result, print);
51
68
  if (args.includes("--json")) {
52
69
  const error = checked.enrollmentPending ? undefined : !checked.floor.ok
53
70
  ? { type: "ownership-floor", message: checked.floor.problems.join("; ") }
@@ -70,7 +87,36 @@ export async function main(args, { evidenceFactory = createOwnershipEvidence, gi
70
87
  return checked.ok ? 0 : 1;
71
88
  }
72
89
 
90
+ // Audit provider output: every scan violation (block and advise tiers) as a repo-relative finding.
91
+ // Exit 0 whenever the scan completes; findings never change the exit code.
92
+ export function auditFindings(result) {
93
+ return {
94
+ schemaVersion: 1,
95
+ findings: result.violations.map((v) => {
96
+ const tier = v.tier ?? "block";
97
+ const file = String(v.file ?? "").replaceAll("\\", "/");
98
+ if (!file || file.startsWith("/") || /^[A-Za-z]:/.test(file) || file === ".." || file.startsWith("../") || file.split("/").includes("..")) throw new Error(`audit finding path is outside the audited root: ${v.file}`);
99
+ const body = v.text ?? `${v.ecosystem ?? "dependency"} ${v.name ?? ""}`.trim();
100
+ const finding = { rule: String(v.fingerprint), path: file };
101
+ if (Number.isSafeInteger(v.line) && v.line > 0) finding.line = v.line;
102
+ finding.message = `${tier === "advise" ? "advise: " : ""}${v.capability} duplicates RightKit owner ${v.package} (${tier}): ${body}`;
103
+ return finding;
104
+ }),
105
+ };
106
+ }
107
+
108
+ function printAuditJson(result, print) {
109
+ try {
110
+ print(JSON.stringify(auditFindings(result)));
111
+ return 0;
112
+ } catch (error) {
113
+ console.error(`[right-release] ownership audit failed: ${error.message}`);
114
+ return 1;
115
+ }
116
+ }
117
+
73
118
  function failClosed(args, type, message, print = (value) => console.log(value)) {
119
+ if (args.includes("--audit-json")) { console.error(`[right-release] ownership audit failed: ${message}`); return 1; }
74
120
  if (args.includes("--json")) print(JSON.stringify({ ok: false, error: { type, message }, rows: [], violations: [], covered: [], problems: [message], coverage: [], metadata: null }, null, 2));
75
121
  else print(`[right-release] ownership FAILED: ${message}`);
76
122
  return 1;
@@ -3,12 +3,183 @@
3
3
  "capabilities": [
4
4
  {
5
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",
6
+ "summary": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
7
+ "fingerprints": [
8
+ {
9
+ "id": "release.sign.codesign-signtool",
10
+ "type": "source",
11
+ "pattern": "\\b(?:Command\\s*::\\s*new|spawn\\w*|execFile\\w*|exec\\w*)\\s*\\(\\s*(?:[\"`]\\s*(?:codesign\\b[^\"`';]{0,160}--sign|signtool(?:\\.exe)?\\s+sign\\b)|[\"'](?:codesign|signtool(?:\\.exe)?)[\"'][^;]{0,300}?[\"'](?:--sign|sign)[\"'])",
12
+ "codeOnly": true,
13
+ "extensions": [
14
+ ".ts",
15
+ ".tsx",
16
+ ".js",
17
+ ".mjs",
18
+ ".cjs",
19
+ ".jsx",
20
+ ".rs",
21
+ ".py"
22
+ ],
23
+ "tier": "advise",
24
+ "usageContext": {
25
+ "hosts": [
26
+ "macos",
27
+ "windows"
28
+ ],
29
+ "scope": "Executable code only; authenticated owner provenance required for exclusions"
30
+ },
31
+ "tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
32
+ },
33
+ {
34
+ "id": "release.notarize.notarytool",
35
+ "type": "source",
36
+ "pattern": "\\b(?:Command\\s*::\\s*new|spawn\\w*|execFile\\w*|exec\\w*)\\s*\\([^;]{0,200}?[\"'`\\s]notarytool\\b",
37
+ "codeOnly": true,
38
+ "extensions": [
39
+ ".ts",
40
+ ".tsx",
41
+ ".js",
42
+ ".mjs",
43
+ ".cjs",
44
+ ".jsx",
45
+ ".rs",
46
+ ".py"
47
+ ],
48
+ "tier": "advise",
49
+ "usageContext": {
50
+ "hosts": [
51
+ "macos",
52
+ "windows"
53
+ ],
54
+ "scope": "Executable code only; authenticated owner provenance required for exclusions"
55
+ },
56
+ "tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
57
+ },
58
+ {
59
+ "id": "release.npm.electron-builder",
60
+ "type": "dependency",
61
+ "ecosystem": "npm",
62
+ "name": "electron-builder",
63
+ "tier": "advise",
64
+ "usageContext": {
65
+ "hosts": [
66
+ "macos",
67
+ "windows"
68
+ ],
69
+ "scope": "npm direct competing dependency; transitive findings require authenticated reachability"
70
+ },
71
+ "tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
72
+ },
73
+ {
74
+ "id": "release.npm.electron-notarize",
75
+ "type": "dependency",
76
+ "ecosystem": "npm",
77
+ "name": "electron-notarize",
78
+ "tier": "advise",
79
+ "usageContext": {
80
+ "hosts": [
81
+ "macos",
82
+ "windows"
83
+ ],
84
+ "scope": "npm direct competing dependency; transitive findings require authenticated reachability"
85
+ },
86
+ "tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
87
+ },
88
+ {
89
+ "id": "release.npm.electron-notarize-scoped",
90
+ "type": "dependency",
91
+ "ecosystem": "npm",
92
+ "name": "@electron/notarize",
93
+ "tier": "advise",
94
+ "usageContext": {
95
+ "hosts": [
96
+ "macos",
97
+ "windows"
98
+ ],
99
+ "scope": "npm direct competing dependency; transitive findings require authenticated reachability"
100
+ },
101
+ "tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
102
+ },
103
+ {
104
+ "id": "release.npm.electron-osx-sign",
105
+ "type": "dependency",
106
+ "ecosystem": "npm",
107
+ "name": "electron-osx-sign",
108
+ "tier": "advise",
109
+ "usageContext": {
110
+ "hosts": [
111
+ "macos",
112
+ "windows"
113
+ ],
114
+ "scope": "npm direct competing dependency; transitive findings require authenticated reachability"
115
+ },
116
+ "tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
117
+ },
118
+ {
119
+ "id": "release.npm.electron-osx-sign-scoped",
120
+ "type": "dependency",
121
+ "ecosystem": "npm",
122
+ "name": "@electron/osx-sign",
123
+ "tier": "advise",
124
+ "usageContext": {
125
+ "hosts": [
126
+ "macos",
127
+ "windows"
128
+ ],
129
+ "scope": "npm direct competing dependency; transitive findings require authenticated reachability"
130
+ },
131
+ "tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
132
+ },
133
+ {
134
+ "id": "release.npm.create-dmg",
135
+ "type": "dependency",
136
+ "ecosystem": "npm",
137
+ "name": "create-dmg",
138
+ "tier": "advise",
139
+ "usageContext": {
140
+ "hosts": [
141
+ "macos",
142
+ "windows"
143
+ ],
144
+ "scope": "npm direct competing dependency; transitive findings require authenticated reachability"
145
+ },
146
+ "tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
147
+ },
148
+ {
149
+ "id": "release.npm.appdmg",
150
+ "type": "dependency",
151
+ "ecosystem": "npm",
152
+ "name": "appdmg",
153
+ "tier": "advise",
154
+ "usageContext": {
155
+ "hosts": [
156
+ "macos",
157
+ "windows"
158
+ ],
159
+ "scope": "npm direct competing dependency; transitive findings require authenticated reachability"
160
+ },
161
+ "tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
162
+ },
163
+ {
164
+ "id": "release.cargo.apple-codesign",
165
+ "type": "dependency",
166
+ "ecosystem": "cargo",
167
+ "name": "apple-codesign",
168
+ "tier": "advise",
169
+ "usageContext": {
170
+ "hosts": [
171
+ "macos",
172
+ "windows"
173
+ ],
174
+ "scope": "cargo direct competing dependency; transitive findings require authenticated reachability"
175
+ },
176
+ "tierReason": "No recorded published-version qualification for this usage context on both macOS & Windows."
177
+ }
178
+ ],
179
+ "detection": "fingerprinted",
9
180
  "policyNotes": [
10
181
  "Package contract retained; dependency declarations do not verify integration.",
11
- "No precise implementation fingerprint; declaration-only inventory, never enforced ownership or verified integration."
182
+ "Fingerprints identify review candidates (competing dependencies and distinctive implementation code); published two-host replacement qualification is not recorded."
12
183
  ]
13
184
  }
14
185
  ]
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ // Propose or regenerate dependency-allowlist baselines from each registered repository's CURRENT direct
3
+ // dependencies. This only edits the committed proposal file; it takes effect solely through Adrian's trust-sync.
4
+ // node ownership-dependency-allowlist-seed.mjs --workspace <dir> [--write]
5
+ // Existing approvals are never removed (union with the current file); block-tier catalog dependencies
6
+ // and RightKit packages are never recorded as approvals.
7
+ import { existsSync, readdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
8
+ import path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { spawnSync } from "./background-process.mjs";
11
+ import { canonicalRepositoryIdentity } from "./ownership-approval.mjs";
12
+ import { directDependencies } from "./ownership.mjs";
13
+ import { ALLOWLIST_FILE, entryKey, isRightKitPackage, sourceKind, validateAllowlist } from "./ownership-dependency-allowlist.mjs";
14
+
15
+ const here = path.dirname(fileURLToPath(import.meta.url));
16
+ const readJson = (name) => JSON.parse(readFileSync(path.join(here, name), "utf8"));
17
+
18
+ // Universally approved tooling: tiny on purpose. Everything else is per-repository.
19
+ export const SEED_GLOBAL = Object.freeze(["npm:@types/node", "npm:typescript", "npm:vitest"]);
20
+ // A renamed repository that has no checkout of its own reuses the baseline of its current name.
21
+ export const SEED_ALIASES = Object.freeze({ "https://github.com/orthic-labs/cockpit": "https://github.com/orthic-labs/pulse" });
22
+
23
+ function localCheckouts(workspace) {
24
+ const found = new Map();
25
+ for (const name of readdirSync(workspace)) {
26
+ const dir = path.join(workspace, name);
27
+ if (!existsSync(path.join(dir, ".git"))) continue;
28
+ const origin = spawnSync("git", ["-C", dir, "config", "--get", "remote.origin.url"], { encoding: "utf8", windowsHide: true });
29
+ if (origin.status !== 0) continue;
30
+ try { found.set(canonicalRepositoryIdentity(origin.stdout.trim()), dir); } catch { /* not a canonical remote */ }
31
+ }
32
+ return found;
33
+ }
34
+
35
+ export function seedRepository(root, manifest) {
36
+ const blocked = new Set(manifest.capabilities.flatMap((c) => c.fingerprints.filter((f) => f.type === "dependency" && (f.tier ?? c.tier ?? "block") === "block").map((f) => `${f.ecosystem}:${f.name}`)));
37
+ const owners = manifest.capabilities.map((c) => c.package);
38
+ const entries = new Set(); let skippedBlock = 0, skippedFirstParty = 0, skippedRightKit = 0;
39
+ const deps = directDependencies(root);
40
+ for (const d of deps) {
41
+ const kind = sourceKind(d.source);
42
+ if (kind === "path") { skippedFirstParty += 1; continue; }
43
+ if (isRightKitPackage(d.name, owners)) { skippedRightKit += 1; continue; }
44
+ if (blocked.has(`${d.ecosystem}:${d.name}`)) { skippedBlock += 1; continue; }
45
+ entries.add(entryKey(d.ecosystem, d.name, kind));
46
+ }
47
+ return { entries: [...entries].sort(), direct: deps.length, skippedBlock, skippedFirstParty, skippedRightKit, unscanned: deps.coverage?.length ?? 0 };
48
+ }
49
+
50
+ export function buildSeed({ workspace, registry = readJson("ownership-repositories.json"), manifest = readJson("rightkit-ownership.json"), existing = null, log = () => {} } = {}) {
51
+ const checkouts = localCheckouts(workspace);
52
+ const previous = new Map((existing?.repositories ?? []).map((entry) => [entry.repositoryIdentity, entry]));
53
+ const seeded = new Map(), stats = [];
54
+ for (const { repositoryIdentity } of registry.repositories) {
55
+ const root = checkouts.get(repositoryIdentity);
56
+ if (!root) continue;
57
+ const result = seedRepository(root, manifest);
58
+ seeded.set(repositoryIdentity, result.entries);
59
+ stats.push({ repositoryIdentity, root, ...result });
60
+ }
61
+ const repositories = registry.repositories.map(({ repositoryIdentity }) => {
62
+ const prior = previous.get(repositoryIdentity)?.dependencies ?? [];
63
+ let note;
64
+ let fresh = seeded.get(repositoryIdentity);
65
+ if (!fresh && SEED_ALIASES[repositoryIdentity] && seeded.has(SEED_ALIASES[repositoryIdentity])) { fresh = seeded.get(SEED_ALIASES[repositoryIdentity]); note = `seeded from ${SEED_ALIASES[repositoryIdentity]} (same repository, renamed)`; stats.push({ repositoryIdentity, alias: SEED_ALIASES[repositoryIdentity], entries: fresh }); }
66
+ if (!fresh) { fresh = []; note = "no local checkout when seeded; baseline is empty until proposed"; stats.push({ repositoryIdentity, unseeded: true, entries: [] }); }
67
+ return { repositoryIdentity, ...(note ? { note } : {}), dependencies: [...new Set([...prior, ...fresh])].sort() };
68
+ });
69
+ const allowlist = { schema: 1, global: [...new Set([...(existing?.global ?? []), ...SEED_GLOBAL])].sort(), repositories };
70
+ const check = validateAllowlist(allowlist);
71
+ if (!check.ok) throw new Error(`generated allowlist is invalid: ${check.problems.join("; ")}`);
72
+ log(stats);
73
+ return { allowlist, stats };
74
+ }
75
+
76
+ const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
77
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
78
+ const args = process.argv.slice(2);
79
+ const flag = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
80
+ const workspace = path.resolve(flag("--workspace") ?? path.resolve(here, "../../.."));
81
+ const file = path.join(here, ALLOWLIST_FILE);
82
+ const existing = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : null;
83
+ const { allowlist, stats } = buildSeed({ workspace, existing });
84
+ for (const s of stats) process.stdout.write(`${s.repositoryIdentity}: ${s.entries.length} entries${s.direct !== undefined ? ` (direct ${s.direct}, first-party ${s.skippedFirstParty}, rightkit ${s.skippedRightKit}, block-tier ${s.skippedBlock}, unscanned ${s.unscanned})` : s.alias ? ` (alias of ${s.alias})` : " (UNSEEDED)"}\n`);
85
+ if (args.includes("--write")) { writeFileSync(file, `${JSON.stringify(allowlist, null, 2)}\n`); process.stdout.write(`wrote ${file}\n`); }
86
+ }