@rightkit/release 0.4.0 → 0.4.1

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.
@@ -59,6 +59,7 @@ import path from "node:path";
59
59
  import { fileURLToPath } from "node:url";
60
60
  import { spawnSync } from "./background-process.mjs";
61
61
  import { parse } from "smol-toml";
62
+ import { RUNTIME_BUDGET_CRATES } from "./runtime-budget-crates.mjs";
62
63
  import { singleHostOfPolicy } from "./publication-gate.mjs";
63
64
  import { findWorkspaceRoot, lockClosure, pathDependencyDirs, readCargoPackage } from "./crate-graph.mjs";
64
65
 
@@ -68,18 +69,7 @@ export const RUNTIME_BUDGET_FILE = "runtime-budget.json";
68
69
  export const RUNTIME_BUDGET_EVIDENCE_DIR = "runtime-budget.evidence";
69
70
  export const RUNTIME_BUDGET_SCHEMA = 1;
70
71
 
71
- export const RUNTIME_BUDGET_CRATES = Object.freeze([
72
- "rightkit-fsindex",
73
- "rightkit-file-watch",
74
- "rightkit-job-queue",
75
- "rightkit-daemon",
76
- "rightkit-cache",
77
- "rightkit-sync",
78
- "rightkit-logs",
79
- "rightkit-tauri-shell",
80
- "rightkit-qa",
81
- "rightkit-send",
82
- ]);
72
+ export { RUNTIME_BUDGET_CRATES };
83
73
 
84
74
  export const BUDGET_FIGURES = Object.freeze(["residentMemoryBytes", "idleCpuPercent", "wakeupsPerSecond", "diskOpsPerSecond"]);
85
75
  const FIGURE_LABEL = Object.freeze({
@@ -260,17 +250,86 @@ const posix = (p) => p.split(path.sep).join("/");
260
250
  /**
261
251
  * The git pathspecs (relative to the workspace root) whose change makes a measurement stale: the
262
252
  * crate's src, benches, Cargo.toml (features) and build.rs, plus src, Cargo.toml and build.rs of every
263
- * workspace crate it reaches through `path` dependencies (transitively). Returns { root, specs }.
253
+ * workspace crate it reaches through `path` dependencies (transitively). Returns { root, specs, problems }.
254
+ * Callers must refuse incomplete metadata rather than using a partial dependency scope.
264
255
  */
265
256
  export function measuredPathspecs(crateDir, deps = {}) {
266
- const root = findWorkspaceRoot(crateDir, deps) ?? path.resolve(crateDir);
257
+ const { exists = existsSync, readFile = readFileSync } = deps;
258
+ const problems = [];
259
+ // Workspace discovery deliberately tolerates bad ancestor manifests elsewhere. Freshness cannot:
260
+ // a swallowed read/parse failure could hide inherited dependencies and shrink the measured graph.
261
+ const checkedRead = (file, encoding) => {
262
+ try {
263
+ const contents = readFile(file, encoding);
264
+ if (path.basename(file) === "Cargo.toml") parse(contents);
265
+ return contents;
266
+ } catch (error) {
267
+ problems.push(`cannot read dependency metadata ${file}: ${error.message}`);
268
+ throw error;
269
+ }
270
+ };
271
+ const checked = { exists, readFile: checkedRead };
272
+ let root = path.resolve(crateDir);
267
273
  const specs = [];
268
274
  const add = (dir, names) => { for (const name of names) specs.push(posix(path.relative(root, path.join(dir, name)))); };
269
- add(path.resolve(crateDir), CRATE_PATHS);
270
- let dependencies = [];
271
- try { dependencies = pathDependencyDirs(crateDir, deps); } catch { /* unreadable manifest: the crate paths alone */ }
272
- for (const dir of dependencies) add(dir, DEPENDENCY_PATHS);
273
- return { root, specs: [...new Set(specs)] };
275
+ try {
276
+ root = findWorkspaceRoot(crateDir, checked) ?? root;
277
+ add(path.resolve(crateDir), CRATE_PATHS);
278
+ const dependencies = pathDependencyDirs(crateDir, checked);
279
+ const workspace = exists(path.join(root, "Cargo.toml")) ? parse(checkedRead(path.join(root, "Cargo.toml"), "utf8")).workspace : undefined;
280
+ for (const dir of [path.resolve(crateDir), ...dependencies]) {
281
+ const manifestPath = path.join(dir, "Cargo.toml");
282
+ if (!exists(manifestPath)) {
283
+ // A manifest-free standalone path has no Cargo graph. A declared path dependency must exist.
284
+ if (dir !== path.resolve(crateDir)) problems.push(`dependency manifest is missing: ${manifestPath}`);
285
+ continue;
286
+ }
287
+ const manifest = parse(checkedRead(manifestPath, "utf8"));
288
+ if (!nonEmpty(manifest.package?.name)) problems.push(`dependency manifest has no package.name: ${manifestPath}`);
289
+ const holders = [manifest, ...Object.values(manifest.target ?? {})];
290
+ for (const holder of holders) {
291
+ for (const key of ["dependencies", "build-dependencies", ...(dir === path.resolve(crateDir) ? ["dev-dependencies"] : [])]) {
292
+ for (const [name, spec] of Object.entries(holder?.[key] ?? {})) {
293
+ if (spec && typeof spec === "object" && spec.workspace === true && workspace?.dependencies?.[name] === undefined) {
294
+ problems.push(`workspace dependency ${name} cannot be resolved for ${manifestPath}`);
295
+ }
296
+ }
297
+ }
298
+ }
299
+ }
300
+ for (const dir of dependencies) add(dir, DEPENDENCY_PATHS);
301
+ } catch (error) {
302
+ if (!problems.length) problems.push(`cannot resolve measured dependency paths for ${crateDir}: ${error.message}`);
303
+ }
304
+ return { root, specs: [...new Set(specs)], problems: [...new Set(problems)] };
305
+ }
306
+
307
+ // lockClosure compares reachable entries, but its permissive parser can return an empty/partial graph.
308
+ // Require the root and every reachable edge before allowing equality to establish freshness.
309
+ function completeLockClosure(contents, name) {
310
+ const packages = parse(contents).package;
311
+ if (!Array.isArray(packages)) throw new Error("missing [[package]] entries");
312
+ const roots = packages.filter((entry) => entry.name === name);
313
+ if (!roots.length) throw new Error(`missing package ${name}`);
314
+ const seen = new Set();
315
+ const pending = [...roots];
316
+ while (pending.length) {
317
+ const entry = pending.pop();
318
+ if (seen.has(entry)) continue;
319
+ seen.add(entry);
320
+ if (!nonEmpty(entry.name) || !nonEmpty(entry.version)) throw new Error(`incomplete package identity for ${entry.name ?? name}`);
321
+ if (entry.dependencies !== undefined && !Array.isArray(entry.dependencies)) throw new Error(`invalid dependencies for ${entry.name}`);
322
+ for (const edge of entry.dependencies ?? []) {
323
+ if (!nonEmpty(edge)) throw new Error(`invalid dependency edge for ${entry.name}`);
324
+ const match = /^(\S+)(?: (\S+))?(?: \(([^)]+)\))?$/.exec(edge);
325
+ if (!match) throw new Error(`invalid dependency ${edge} of ${entry.name}`);
326
+ const candidates = packages.filter((candidate) => candidate.name === match[1]
327
+ && (!match[2] || candidate.version === match[2]) && (!match[3] || candidate.source === match[3]));
328
+ if (!candidates.length) throw new Error(`unresolved dependency ${edge} of ${entry.name}`);
329
+ pending.push(...candidates);
330
+ }
331
+ }
332
+ return lockClosure(contents, name);
274
333
  }
275
334
 
276
335
  /**
@@ -289,7 +348,8 @@ export function measurementStaleness(commit, crateDir, git = defaultGit, deps =
289
348
  const known = git(["cat-file", "-t", commit], crateDir);
290
349
  if (known.status !== 0 || known.stdout !== "commit") return `commit ${commit.slice(0, 12)} is not a commit in this repository (shallow clone?)`;
291
350
  if (git(["merge-base", "--is-ancestor", commit, "HEAD"], crateDir).status !== 0) return `commit ${commit.slice(0, 12)} is not an ancestor of HEAD`;
292
- const { root, specs } = measuredPathspecs(crateDir, { exists, readFile });
351
+ const { root, specs, problems } = measuredPathspecs(crateDir, { exists, readFile });
352
+ if (problems.length) return `cannot resolve measured dependency graph: ${problems.join("; ")}`;
293
353
  // `git diff <commit>` (no second revision) compares the working tree to the commit.
294
354
  const changed = git(["diff", "--name-only", commit, "--", ...specs], root);
295
355
  if (changed.status !== 0) return "cannot diff the crate against the measured commit";
@@ -298,17 +358,29 @@ export function measurementStaleness(commit, crateDir, git = defaultGit, deps =
298
358
  if (untracked.status !== 0) return "cannot list untracked crate files";
299
359
  if (untracked.stdout !== "") return `untracked files since the measurement: ${untracked.stdout.split("\n").slice(0, 5).join(", ")}`;
300
360
  let name = null;
301
- try { name = readCargoPackage(crateDir, { exists, readFile }).name; } catch { /* no manifest: nothing to resolve in the lock */ }
361
+ try {
362
+ if (exists(path.join(crateDir, "Cargo.toml"))) {
363
+ name = readCargoPackage(crateDir, { exists, readFile }).name;
364
+ if (!nonEmpty(name)) return `cannot resolve Cargo.lock: ${path.join(crateDir, "Cargo.toml")} has no package.name`;
365
+ }
366
+ } catch (error) { return `cannot resolve Cargo.lock package identity: ${error.message}`; }
302
367
  if (name) {
303
368
  const lockPath = path.join(root, "Cargo.lock");
304
- const now = exists(lockPath) ? readFile(lockPath, "utf8") : null;
369
+ let now;
370
+ try { now = exists(lockPath) ? readFile(lockPath, "utf8") : null; }
371
+ catch (error) { return `cannot read Cargo.lock ${lockPath}: ${error.message}`; }
305
372
  const then = git(["show", `${commit}:./Cargo.lock`], root);
306
373
  const had = then.status === 0;
307
374
  if (now === null && had) return "Cargo.lock was removed since the measurement";
308
375
  if (now !== null && !had) return "Cargo.lock did not exist at the measured commit";
376
+ if (now === null && !had) {
377
+ // A failed show alone cannot distinguish an absent lock from an unreadable Git object.
378
+ const listed = git(["ls-tree", commit, "--", "Cargo.lock"], root);
379
+ if (listed.status !== 0 || listed.stdout !== "") return "cannot establish that Cargo.lock was absent at the measured commit";
380
+ }
309
381
  if (now !== null) {
310
382
  try {
311
- if (JSON.stringify(lockClosure(now, name)) !== JSON.stringify(lockClosure(then.stdout, name))) {
383
+ if (JSON.stringify(completeLockClosure(now, name)) !== JSON.stringify(completeLockClosure(then.stdout, name))) {
312
384
  return `Cargo.lock changed in the dependency graph of ${name} since commit ${commit.slice(0, 12)}`;
313
385
  }
314
386
  } catch (error) { return `cannot compare Cargo.lock: ${error.message}`; }
@@ -500,7 +572,14 @@ export function statusApiDeclared(api, source) {
500
572
  * `evidence` is the release gate evidence for it (the host-policy authority). `requireInstalled`
501
573
  * (adoption) additionally needs a fresh in-ceiling installed-app measurement, optionally for `app`.
502
574
  */
503
- export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requireInstalled = false, app, heldResource = false }, { readFile = readFileSync, exists = existsSync, git = defaultGit } = {}) {
575
+ export function evaluateRuntimeBudget(options, deps = {}) {
576
+ return evaluateRuntimeBudgetOwned(options, deps);
577
+ }
578
+
579
+ // Private owner mode: no caller-supplied boolean can weaken publication or adoption.
580
+ const STRUCTURE_ONLY = Symbol("runtime-budget-structure-only");
581
+ function evaluateRuntimeBudgetOwned({ crate, crateDir, version, evidence, requireInstalled = false, app, heldResource = false }, { readFile = readFileSync, exists = existsSync, git = defaultGit } = {}, mode) {
582
+ const structural = mode === STRUCTURE_ONLY;
504
583
  const file = path.join(crateDir, RUNTIME_BUDGET_FILE);
505
584
  if (!exists(file)) return { ok: false, problems: [`${RUNTIME_BUDGET_FILE} is missing (docs/RUNTIME-BUDGETS.md)`] };
506
585
  let budget;
@@ -574,7 +653,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
574
653
 
575
654
  // Measurements.
576
655
  const records = Array.isArray(budget.measurements) ? budget.measurements : [];
577
- if (records.length === 0) problems.push("no measurements recorded (a declared ceiling is not evidence)");
656
+ if (!structural && records.length === 0) problems.push("no measurements recorded (a declared ceiling is not evidence)");
578
657
  const valid = [];
579
658
  records.forEach((m, index) => {
580
659
  const bad = validateMeasurement(m, index);
@@ -583,7 +662,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
583
662
  if (evidenceProblems.length) problems.push(...evidenceProblems); else valid.push({ m, index });
584
663
  });
585
664
  const counted = valid.filter(({ m }) => m.crateVersion === version);
586
- if (valid.length && nonEmpty(version) && counted.length === 0) {
665
+ if (!structural && valid.length && nonEmpty(version) && counted.length === 0) {
587
666
  problems.push(`no measurement for version ${version} (recorded: ${[...new Set(valid.map(({ m }) => m.crateVersion))].join(", ")})`);
588
667
  }
589
668
  // Why a measurement fails one figure, or null. A delta-basis figure of a synthetic measurement is gated on
@@ -608,7 +687,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
608
687
  }
609
688
  return null;
610
689
  };
611
- for (const { m, index } of counted) {
690
+ for (const { m, index } of (structural ? valid : counted)) {
612
691
  for (const key of BUDGET_FIGURES) {
613
692
  const failure = figureFailure(m, key);
614
693
  if (failure) problems.push(`measurement[${index}] on ${m.host.os}/${m.host.arch} ${failure}`);
@@ -616,6 +695,14 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
616
695
  }
617
696
  const withinCeilings = (m) => BUDGET_FIGURES.every((key) => ceilings[key] !== undefined && (usesDelta(m, key) ? deltaCeilings[key] !== undefined && figureFailure(m, key) === null : m.observed[key] === null ? figureUnavailableOn(m.host?.os, key) : m.observed[key] <= ceilings[key]));
618
697
  const atScale = (m) => requiredScale !== null && m.dataScale.unit === requiredScale.unit && m.dataScale.count >= requiredScale.minCount;
698
+ if (structural) {
699
+ if (!Array.isArray(budget.measurements)) problems.push("measurements must be an array");
700
+ const scope = measuredPathspecs(crateDir, { exists, readFile });
701
+ problems.push(...scope.problems);
702
+ for (const key of deltaFigures) if (!nonEmpty(budget.deltaSources?.[key])) problems.push(`deltaSources.${key} must name its ceiling source`);
703
+ if (budget.deltaSources !== undefined && (!budget.deltaSources || typeof budget.deltaSources !== "object" || Array.isArray(budget.deltaSources) || Object.keys(budget.deltaSources).some((key) => !deltaFigures.includes(key)))) problems.push("deltaSources must name only declared delta figures");
704
+ for (const { m, index } of valid) if (!atScale(m)) problems.push(`measurement[${index}] is below required scale`);
705
+ }
619
706
  const staleness = new Map();
620
707
  const stale = (m) => {
621
708
  if (!staleness.has(m.commit)) staleness.set(m.commit, measurementStaleness(m.commit, crateDir, git, { exists, readFile }));
@@ -624,7 +711,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
624
711
 
625
712
  // Required hosts: one fresh, in-ceiling, at-scale measurement of this version on each.
626
713
  // A held-resource crate defers measurement to adoption: no per-host publish measurement is required.
627
- const required = heldResource ? [] : requiredBudgetHosts(budget, evidence);
714
+ const required = heldResource || structural ? [] : requiredBudgetHosts(budget, evidence);
628
715
  for (const host of required) {
629
716
  const onHost = counted.filter(({ m }) => m.host.os === host);
630
717
  if (onHost.length === 0) { problems.push(`no ${HOSTS[host]} measurement for version ${version} (required hosts: ${required.map((h) => HOSTS[h]).join(" and ")}${required.length > 1 ? "; apps ship on both and the release gate requires both unless a darwin-only policy with a reason is recorded" : `; ${required[0]}-only policy: ${String((evidence ?? budget).hostPolicyReason).trim()}`})`); continue; }
@@ -728,7 +815,17 @@ export function heldResourceProblems({ crate, crateDir, meta }, { readFile = rea
728
815
  * naming every matched pattern), "held-resource" (resources table, adoption-time proof) or always-on
729
816
  * (runtime-budget.json, fully checked). `readFile`, `exists` and `git` are injectable.
730
817
  */
731
- export function runtimeBudgetCheck({ crate, crateDir, version, evidence, requireInstalled = false, app }, deps = {}) {
818
+ export function runtimeBudgetCheck(options, deps = {}) {
819
+ return runtimeBudgetCheckOwned(options, deps);
820
+ }
821
+
822
+ /** Source structure only; grants no runtime qualification or semantic admission. */
823
+ export function runtimeBudgetStructureCheck(options, deps = {}) {
824
+ const result = runtimeBudgetCheckOwned({ ...options, requireInstalled: false }, deps, STRUCTURE_ONLY);
825
+ return { ...result, name: "runtime-budget-structure" };
826
+ }
827
+
828
+ function runtimeBudgetCheckOwned({ crate, crateDir, version, evidence, requireInstalled = false, app }, deps = {}, mode) {
732
829
  const { readFile = readFileSync, exists = existsSync } = deps;
733
830
  let pkg;
734
831
  try { pkg = readCargoPackage(crateDir, { exists, readFile }); } catch (error) {
@@ -775,9 +872,9 @@ export function runtimeBudgetCheck({ crate, crateDir, version, evidence, require
775
872
  return fail(crate, crateDir, [`undeclared: none of ${RUNTIME_BUDGET_FILE}, continuous = false with a continuous_reason, or continuous = "held-resource" with resources`],
776
873
  `declare the crate (docs/RUNTIME-BUDGETS.md)`);
777
874
  }
778
- const result = evaluateRuntimeBudget({ crate, crateDir, version: version ?? pkg.version, evidence, requireInstalled, app }, deps);
875
+ const result = evaluateRuntimeBudgetOwned({ crate, crateDir, version: version ?? pkg.version, evidence, requireInstalled, app }, deps, mode);
779
876
  if (result.ok) {
780
- return { name: RUNTIME_BUDGET_CHECK, status: "ok", detail: `${crate} ${version ?? pkg.version} has measured runtime budgets on every required host` };
877
+ return { name: RUNTIME_BUDGET_CHECK, status: "ok", detail: mode === STRUCTURE_ONLY ? `${crate} ${version ?? pkg.version} runtime budget structure is valid; qualification is separate` : `${crate} ${version ?? pkg.version} has measured runtime budgets on every required host` };
781
878
  }
782
879
  return fail(crate, crateDir, result.problems);
783
880
  }
package/stage-sidecar.mjs CHANGED
@@ -73,6 +73,28 @@ const MODES = new Set(["dev", "release"]);
73
73
  const PROFILE = /^[a-z][a-z0-9_-]*$/;
74
74
  const PLATFORMS = new Set(["mac", "win", "linux"]);
75
75
  const NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
76
+ /** One path segment of an archive member path: no glob metacharacters, no separators, no drive colon. */
77
+ const MEMBER_SEGMENT = /^[A-Za-z0-9_.+@~-]+$/;
78
+
79
+ /**
80
+ * Normalize and guard a requested archive member. A bare file name (no `/`) selects the one member with that base name; a path
81
+ * (contains `/`) selects the one member with exactly that archive path. A leading `./` is stripped. Absolute paths, drive
82
+ * prefixes, backslashes, empty, `.` and `..` segments and glob metacharacters are refused. Returns { value, byPath } or
83
+ * { error }. The member never influences where the extracted file is written.
84
+ */
85
+ export function normalizeArchiveMember(member) {
86
+ if (typeof member !== "string" || !member) return { error: "must be a non-empty string" };
87
+ if (member.includes("\\")) return { error: "must not contain a backslash" };
88
+ const value = member.startsWith("./") ? member.slice(2) : member;
89
+ if (value.startsWith("/")) return { error: "must not be an absolute path" };
90
+ if (/^[A-Za-z]:/.test(value)) return { error: "must not start with a drive prefix" };
91
+ const segments = value.split("/");
92
+ if (segments.some((segment) => segment === "")) return { error: "must not contain an empty segment" };
93
+ if (segments.some((segment) => segment === "." || segment === "..")) return { error: "must not contain a '.' or '..' segment" };
94
+ // A bare file name keeps today's matching (any characters but the refused ones above); a path is held to a strict segment alphabet so it can never act as a tar glob.
95
+ if (segments.length > 1 && !segments.every((segment) => MEMBER_SEGMENT.test(segment))) return { error: "has a segment with characters outside A-Z a-z 0-9 _ . + @ ~ -" };
96
+ return { value, byPath: segments.length > 1 };
97
+ }
76
98
  const KEYS = Object.freeze({
77
99
  "cargo-bin": ["kind", "package", "bin", "name", "profile", "features", "manifestPath", "sha256", "dest", "clean", "platforms"],
78
100
  ffmpeg: ["kind", "variant", "variants", "tools", "notices", "catalog", "stage", "bundleDir", "sources", "probe", "platforms"],
@@ -390,7 +412,7 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
390
412
  if (item.dest === undefined && item.to === undefined) errors.push(`${where}: dest (or to) is required`);
391
413
  const dest = destOf(item.dest ?? item.to, item.dest === undefined ? "to" : "dest");
392
414
  if (item.size !== undefined && (!Number.isSafeInteger(item.size) || item.size <= 0)) errors.push(`${where}: size must be a positive integer (bytes)`);
393
- if (item.extract !== undefined && (typeof item.extract !== "string" || !NAME.test(item.extract))) errors.push(`${where}: extract must be the bare file name of one archive member (no path)`);
415
+ if (item.extract !== undefined) { const member = normalizeArchiveMember(item.extract); if (member.error || (!member.byPath && !NAME.test(member.value))) errors.push(`${where}: extract must be the bare file name of one archive member or its archive path (posix, no leading /, no .. segment): ${member.error ?? "invalid file name"}`); }
394
416
  if (item.memberSha256 !== undefined && (item.extract === undefined || !SHA256.test(item.memberSha256 ?? ""))) errors.push(`${where}: memberSha256 must be a lowercase 64-hex SHA-256 and needs extract`);
395
417
  if (item.executable !== undefined && typeof item.executable !== "boolean") errors.push(`${where}: executable must be a boolean`);
396
418
  const clean = cleanOf();
@@ -499,15 +521,28 @@ export function defaultRun(command, args, { cwd, env = process.env } = {}) {
499
521
  });
500
522
  }
501
523
 
502
- /** Default archive member extraction (bsdtar reads tgz and zip/nupkg): the one member whose basename is `fileName`. */
524
+ /**
525
+ * Default archive member extraction (bsdtar reads tgz and zip/nupkg). A bare `fileName` selects the one member whose basename it is;
526
+ * a `fileName` containing `/` selects the one member whose full archive path equals it (case-sensitive, leading `./` ignored on both
527
+ * sides), so archives with the same base name in several directories (ORT DirectML nupkgs) can be addressed. The returned file is
528
+ * always under `destDir`; callers copy it to their own declared output name, so the member path never decides where anything is written.
529
+ */
503
530
  export function defaultExtractMember({ archive, fileName, destDir, run = defaultRun }) {
531
+ const wanted = normalizeArchiveMember(fileName);
532
+ if (wanted.error) fail("STAGE_SIDECAR_EXTRACT_FAILED", `archive member ${JSON.stringify(fileName)} ${wanted.error}`);
504
533
  const listing = run("tar", ["-tf", archive], { cwd: path.dirname(archive) });
505
534
  if (listing.status !== 0) fail("STAGE_SIDECAR_EXTRACT_FAILED", `cannot list ${archive}`);
506
- const members = String(listing.stdout).split(/\r?\n/).map((line) => line.trim()).filter((line) => line && path.posix.basename(line) === fileName);
507
- if (members.length !== 1) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${archive} has ${members.length} members named ${fileName}, expected exactly one`);
535
+ const lines = String(listing.stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
536
+ const members = wanted.byPath
537
+ ? lines.filter((line) => (line.startsWith("./") ? line.slice(2) : line) === wanted.value)
538
+ : lines.filter((line) => path.posix.basename(line) === wanted.value);
539
+ if (members.length !== 1) fail("STAGE_SIDECAR_EXTRACT_FAILED", `${archive} has ${members.length} members ${wanted.byPath ? "at the path" : "named"} ${wanted.value}, expected exactly one`);
508
540
  const extracted = run("tar", ["-xf", archive, "-C", destDir, members[0]], { cwd: path.dirname(archive) });
509
541
  if (extracted.status !== 0) fail("STAGE_SIDECAR_EXTRACT_FAILED", `cannot extract ${members[0]} from ${archive}`);
510
- return path.join(destDir, ...members[0].split("/"));
542
+ const out = path.join(destDir, ...members[0].split("/"));
543
+ const inside = path.relative(path.resolve(destDir), path.resolve(out));
544
+ if (!inside || inside === ".." || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside)) fail("STAGE_SIDECAR_EXTRACT_FAILED", `member ${members[0]} of ${archive} resolves outside the extraction directory`);
545
+ return out;
511
546
  }
512
547
 
513
548
  async function defaultFetchBytes(url) {
@@ -900,6 +935,10 @@ async function stageOnnxRuntimeSlice(entry, ctx, loaded, t, { destDir } = {}) {
900
935
  });
901
936
  for (const { source } of notices) if (!fs.existsSync(source)) fail("STAGE_SIDECAR_NOTICE_MISSING", `notice file not found: ${source}`);
902
937
 
938
+ // The archive member is the catalog's archive.member (an archive path) when present, else the library file name; the output name never comes from it.
939
+ const member = row.archive.member ?? row.artifactFileName;
940
+ const memberCheck = normalizeArchiveMember(member);
941
+ if (memberCheck.error) fail("STAGE_SIDECAR_NOT_APPROVED", `the ${where} row ${row.id} has an invalid archive member ${JSON.stringify(member)}: ${memberCheck.error}`);
903
942
  const outName = entry.as ?? row.artifactFileName;
904
943
  const dest = path.resolve(appRoot, destDir ?? entry.resourceDir, outName);
905
944
  if (fs.existsSync(dest) && sha256Of(fs.readFileSync(dest)) === row.sha256) {
@@ -915,9 +954,9 @@ async function stageOnnxRuntimeSlice(entry, ctx, loaded, t, { destDir } = {}) {
915
954
  }
916
955
  const scratch = ctx.makeTempDir();
917
956
  try {
918
- const library = await ctx.extractMember({ archive, fileName: row.artifactFileName, destDir: scratch });
957
+ const library = await ctx.extractMember({ archive, fileName: member, destDir: scratch });
919
958
  const actual = sha256Of(fs.readFileSync(library));
920
- if (actual !== row.sha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${row.artifactFileName} extracted as ${actual}, the catalog pins ${row.sha256}; nothing staged`, { expected: row.sha256, actual });
959
+ if (actual !== row.sha256) fail("STAGE_SIDECAR_HASH_MISMATCH", `${member} extracted as ${actual}, the catalog pins ${row.sha256}; nothing staged`, { expected: row.sha256, actual });
921
960
  copyInto(ctx, library, dest, { executable: false });
922
961
  } finally {
923
962
  ctx.removeDir(scratch);
@@ -1042,7 +1081,7 @@ async function ensureCachedDownload(ctx, { url, sha256, size }) {
1042
1081
 
1043
1082
  /**
1044
1083
  * A pinned https download. `dest` is copied only from the verified cache entry. With `extract` the download is an archive
1045
- * (zip, tgz): the one member whose base name is `extract` is unpacked (digest-checked against `memberSha256` when given) and
1084
+ * (zip, tgz): the one member whose base name is `extract` (or, when `extract` contains `/`, whose archive path equals it) is unpacked (digest-checked against `memberSha256` when given) and
1046
1085
  * only that member is staged, so no `command` entry or shell is needed to unpack it.
1047
1086
  */
1048
1087
  async function stageRemote(entry, ctx) {
@@ -1,7 +1,11 @@
1
1
  // The Developer-ID-signed `bin/darwin/rightkit-swap` that @rightkit/release ships (Orthic-Labs/rightkit#258).
2
- // rightkit-swap is the bin target of crates/rightkit-fs (renamex_np RENAME_SWAP). CI has no Apple signing secrets, so
3
- // @rightkit/release is published through `right-release publish npm --local` on the Mac, which calls
4
- // `prepareSwapHelper` before packing. install-app calls `assertShippedSwapHelperSigned` before it trusts the helper.
2
+ // rightkit-swap is the bin target of crates/rightkit-fs (renamex_np RENAME_SWAP). Two signing routes produce it:
3
+ // (a) local publish — `right-release publish npm --local` on the Mac calls `prepareSwapHelper` before packing;
4
+ // (b) CI qualification — `publish.yml` build-swap -> sign-swap -> pack (docs/CI.md), where sign-swap imports the
5
+ // Developer ID from the `release` environment secrets. Route (b) stays blocked until root provisions
6
+ // APPLE_CERTIFICATE_BASE64/APPLE_CERTIFICATE_PASSWORD there; exporting that identity needs an Aqua login session
7
+ // (SSH export fails errSecInteractionNotAllowed/-25308 & the signing agent has no key-export capability — never
8
+ // retry either). install-app calls `assertShippedSwapHelperSigned` before it trusts the helper.
5
9
  import crypto from "node:crypto";
6
10
  import nodeFs from "node:fs";
7
11
  import path from "node:path";
@@ -1,6 +0,0 @@
1
- {
2
- "schema": 1,
3
- "sha256": "a41ce13fd3bac31db0230d70bed20db0ed9bf554bdae2255bdf2064589b48f29",
4
- "identity": "Developer ID Application: Adrian D'souza (6KLGD3LLKF)",
5
- "codesign": "strict-valid"
6
- }