@rightkit/release 0.4.1 → 0.5.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.
@@ -3,6 +3,7 @@ import { closeSync, copyFileSync, existsSync, mkdtempSync, openSync, readFileSyn
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { spawnSync } from "./background-process.mjs";
6
+ import { adhocSafeEntitlements } from "./adhoc-entitlements.mjs";
6
7
  import { runMacCodesignCommands } from "./macos-signing-session.mjs";
7
8
  import { bundleRelativePath, compileBundlePathMap, globToRegExp, isGlobPattern, lookupBundlePath, resolveBundleIdentifier, validateBundleRelativeKey, validateIdentifierTemplate, validateSigningIdentifier } from "./mac-bundle-signing.mjs";
8
9
 
@@ -308,7 +309,7 @@ export function signMacosDevelopmentBundle(options) {
308
309
  const entitlementFile = (file) => {
309
310
  if (!signing.adhoc) return file;
310
311
  if (!safeFiles.has(file)) {
311
- const safe = Object.fromEntries(Object.entries(readPlist(file, context)).filter(([key]) => key.startsWith("com.apple.security.cs.") || key === "com.apple.security.device.audio-input"));
312
+ const safe = adhocSafeEntitlements(readPlist(file, context));
312
313
  const out = path.join(scratch, `adhoc-${safeFiles.size}.plist`);
313
314
  writeFileSync(out, JSON.stringify(safe));
314
315
  checked("plutil", ["-convert", "xml1", out], context, "DEV_MAC_SIGNING_CONFIG_INVALID", identity);
@@ -0,0 +1,90 @@
1
+ // Dependency-free (no imports) refusal shared by every module that can put bytes in a registry. RightKit crates and @rightkit
2
+ // packages reach a registry from CI only (publish.yml qualification, the approval commit in ci/approved-artifacts.json,
3
+ // publish-reviewed.yml). No environment variable, flag or caller-set GITHUB_ACTIONS lifts this: CI uploads go through
4
+ // scripts/ci/publish.mjs and scripts/ci/reviewed-candidate.mjs, never through right-release.
5
+ export const HOST_UPLOAD_REFUSED = "RK_HOST_UPLOAD_REFUSED";
6
+ export function hostUploadRefusalMessage(what) {
7
+ return `${what}: host uploads are retired; RightKit publishes only through CI's approved-hash path (publish.yml, approval in ci/approved-artifacts.json, publish-reviewed.yml). Use --dry-run, preflight or --local --qualify-only to qualify locally.`;
8
+ }
9
+ export function refuseHostUpload(what) {
10
+ throw Object.assign(new Error(hostUploadRefusalMessage(what)), { code: HOST_UPLOAD_REFUSED });
11
+ }
12
+
13
+ // ---- publish capability -------------------------------------------------------------------------------------------------
14
+ // The publisher internals (publishGatedTarball in publication-gate.mjs, runCargoPublish in publish-cargo.mjs) are exported and read
15
+ // registry credentials, so being importable must not mean being able to publish. Each of them refuses (refuseHostUpload) unless
16
+ // it is handed a CAPABILITY, and a capability exists only when createPublishCapability accepted a validated admission:
17
+ // - the hashed admission record the credentialed job builds (schema 1, "semantic-admission" or "local-admission": original
18
+ // binding, source tree, verifier SHA, run and attempt, one passing verdict per archive);
19
+ // - an approval commit entry (ci/approved-artifacts.json: registry, name, version, sha256, reviewer) for every archive in it;
20
+ // - the CI run it names: GITHUB_ACTIONS=true on refs/heads/main of the repository, GITHUB_SHA equal to the admission's verifier
21
+ // SHA, GITHUB_RUN_ID and GITHUB_RUN_ATTEMPT equal to its run.
22
+ // The capability is an object registered in a module-private WeakMap at creation: an object literal, a copy or a mutated clone is
23
+ // not registered and is refused. It is bound to the units of the admission (name, version, archive sha256, registry), so one
24
+ // capability publishes nothing else, and the publisher also compares the sha256 of the bytes it is about to send. Nothing here
25
+ // reads an environment variable to LIFT a refusal: the environment is one of the conditions the admission must match.
26
+ const HEX64 = /^[0-9a-f]{64}$/;
27
+ const HEX40 = /^[0-9a-f]{40}$/;
28
+ const POSITIVE = /^[1-9][0-9]*$/;
29
+ export const PUBLISH_REPOSITORY = "bogusyogi/rightkit";
30
+ const CAPABILITY_REGISTRIES = ["npm", "crates"];
31
+ const ADMISSION_KINDS = ["semantic-admission", "local-admission"];
32
+ const minted = new WeakMap();
33
+ const plain = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
34
+
35
+ /** Every reason `admission` is not a valid, passing admission record for `registry` in this CI run (empty means valid). */
36
+ export function admissionProblems(admission, { registry, env = process.env, approved = [] } = {}) {
37
+ const problems = [];
38
+ const bad = (text) => problems.push(text);
39
+ if (!CAPABILITY_REGISTRIES.includes(registry)) bad(`registry ${JSON.stringify(registry)} is not npm or crates`);
40
+ if (!plain(admission)) return [...problems, "the admission is not an object"];
41
+ if (admission.schema !== 1 || !ADMISSION_KINDS.includes(admission.kind)) bad("the admission schema or kind is not a reviewed admission");
42
+ const original = admission.original;
43
+ if (!plain(original) || typeof original.candidateId !== "string" || !original.candidateId || !HEX64.test(original.candidateSha256 ?? "") || !HEX40.test(original.sourceSha ?? "") || !HEX64.test(original.manifestSha256 ?? "") || !Array.isArray(original.archives) || original.archives.length === 0) bad("the admission does not bind an original candidate, manifest and archives");
44
+ if (!HEX40.test(admission.sourceTree ?? "") || !HEX40.test(admission.verifierSha ?? "") || !POSITIVE.test(String(admission.runId ?? "")) || !POSITIVE.test(String(admission.runAttempt ?? ""))) bad("the admission does not name its source tree, verifier commit and run");
45
+ const archives = Array.isArray(original?.archives) ? original.archives : [];
46
+ const units = Array.isArray(admission.units) ? admission.units : [];
47
+ if (units.length !== archives.length) bad("the admission holds no verdict for exactly its archives");
48
+ const names = new Set();
49
+ for (const archive of archives) {
50
+ const label = `${archive?.name}@${archive?.version}`;
51
+ if (!plain(archive) || typeof archive.name !== "string" || !archive.name || typeof archive.version !== "string" || !archive.version || !HEX64.test(archive.sha256 ?? "")) { bad(`archive ${label} is malformed`); continue; }
52
+ if (names.has(archive.name)) bad(`archive ${label} is listed twice`);
53
+ names.add(archive.name);
54
+ const verdict = units.find((row) => row?.name === archive.name);
55
+ if (!verdict || verdict.version !== archive.version || verdict.archiveSha256 !== archive.sha256 || verdict.ok !== true || !Array.isArray(verdict.failures) || verdict.failures.length !== 0 || !HEX64.test(verdict.evidenceSha256 ?? "")) bad(`${label} has no passing verdict for its archive`);
56
+ const entries = (Array.isArray(approved) ? approved : []).filter((entry) => entry?.registry === registry && entry.name === archive.name && entry.version === archive.version);
57
+ if (entries.length === 0 || entries.some((entry) => entry.sha256 !== archive.sha256)) bad(`${label} is not approved in ci/approved-artifacts.json with this archive hash`);
58
+ }
59
+ if (env.GITHUB_ACTIONS !== "true" || env.GITHUB_REF !== "refs/heads/main" || env.GITHUB_REPOSITORY !== PUBLISH_REPOSITORY) bad("not running in GitHub Actions on refs/heads/main of the RightKit repository");
60
+ if (!HEX40.test(env.GITHUB_SHA ?? "") || env.GITHUB_SHA !== admission.verifierSha || env.GITHUB_RUN_ID !== String(admission.runId) || env.GITHUB_RUN_ATTEMPT !== String(admission.runAttempt)) bad("the admission was built by another commit, run or attempt");
61
+ return problems;
62
+ }
63
+
64
+ /**
65
+ * The capability to publish exactly the archives of one validated admission to `registry`. Throws (HOST_UPLOAD_REFUSED) unless
66
+ * `admission` passes admissionProblems against the current CI environment and the approval entries in `approved`
67
+ * (ci/approved-artifacts.json `artifacts`, as scripts/ci/upload.mjs loadApprovedArtifacts returns them).
68
+ */
69
+ export function createPublishCapability({ admission, approved, registry }) {
70
+ const problems = admissionProblems(admission, { registry, env: process.env, approved });
71
+ if (problems.length) refuseHostUpload(`a publish capability needs a validated admission (${problems.join("; ")})`);
72
+ const record = Object.freeze({ kind: "rightkit-publish-capability", registry });
73
+ minted.set(record, { registry, units: new Map(admission.original.archives.map((archive) => [archive.name, { version: archive.version, sha256: archive.sha256 }])), admissionVerifier: admission.verifierSha });
74
+ return record;
75
+ }
76
+
77
+ /**
78
+ * Refuses (HOST_UPLOAD_REFUSED) unless `capability` was created by createPublishCapability for this `registry` and names the
79
+ * unit. When `version` and `sha256` are given they must equal the admitted archive's: the publisher passes the version and the
80
+ * sha256 of the bytes it is about to send.
81
+ */
82
+ export function assertPublishCapability(capability, { what, registry, name, version, sha256 }) {
83
+ const grant = typeof capability === "object" && capability !== null ? minted.get(capability) : undefined;
84
+ if (!grant) refuseHostUpload(`${what}: no publish capability (a validated admission is required)`);
85
+ if (grant.registry !== registry) refuseHostUpload(`${what}: the publish capability is for ${grant.registry}, not ${registry}`);
86
+ const unit = grant.units.get(name);
87
+ if (!unit) refuseHostUpload(`${what}: ${name} is not an archive of the admission behind the publish capability`);
88
+ if (version !== undefined && unit.version !== version) refuseHostUpload(`${what}: ${name}@${version} is not the admitted ${unit.version}`);
89
+ if (sha256 !== undefined && unit.sha256 !== sha256) refuseHostUpload(`${what}: the bytes of ${name} (${String(sha256).slice(0, 12)}) are not the admitted archive (${unit.sha256.slice(0, 12)})`);
90
+ }
package/local-publish.mjs CHANGED
@@ -24,6 +24,7 @@ import { localBuildGranted, LOCAL_BUILD_FILE } from "./execution-policy.mjs";
24
24
  import { GATE_SCHEMA, MAX_CARGO_JOBS, defaultEvidencePath, enforcePublicationGate, publishGatedTarball } from "./publication-gate.mjs";
25
25
  import { runCargoPublish, runCargoReadOnly, scanCrateForSecrets } from "./publish-cargo.mjs";
26
26
  import { cargoIndexUrl } from "./cargo-registry-readback.mjs";
27
+ import { refuseHostUpload } from "./host-upload-refusal.mjs";
27
28
  import { pathDependencyDirs, readCargoPackage } from "./crate-graph.mjs";
28
29
  import { recordPublished, stageSourceVersions, workspaceFeedOptions } from "./rightkit-sync.mjs";
29
30
  import { prepareSwapHelper, SWAP_HELPER_CARGO_ARGS, SWAP_HELPER_PACKAGE } from "./swap-helper-bundle.mjs";
@@ -411,7 +412,15 @@ function assertLocalHost(deps, workspaceRoot, kind) {
411
412
  return platform;
412
413
  }
413
414
 
415
+ // Host uploads are retired: only --qualify-only (no upload) runs. The upload engine below is kept for direct calls in
416
+ // tests; no CLI path reaches it (see host-upload-refusal.mjs).
414
417
  export async function runLocalPublish(kind, argv, deps = {}) {
418
+ const parsed = parseLocalPublishArgs(kind, argv, deps.cwd);
419
+ if (!parsed.qualifyOnly) refuseHostUpload(`right-release publish ${kind} --local`);
420
+ return runLocalPublishUploadEngine(kind, argv, deps);
421
+ }
422
+
423
+ export async function runLocalPublishUploadEngine(kind, argv, deps = {}) {
415
424
  const parsed = parseLocalPublishArgs(kind, argv, deps.cwd);
416
425
  if (parsed.batch) return runLocalPublishBatch(parsed, deps);
417
426
  return publishUnit(parsed, deps, null);