@camstack/types 1.2.355 → 1.2.357

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/dist/node.js CHANGED
@@ -21,7 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
- const require_composition = require("./composition-BLWAivB2.js");
24
+ const require_composition = require("./composition-DCYnv4I6.js");
25
25
  const require_err_msg = require("./err-msg-Dx2Kor0g.js");
26
26
  let node_crypto = require("node:crypto");
27
27
  let node_fs = require("node:fs");
@@ -31,7 +31,161 @@ node_path = __toESM(node_path);
31
31
  let node_stream_promises = require("node:stream/promises");
32
32
  let node_stream = require("node:stream");
33
33
  let node_child_process = require("node:child_process");
34
+ let node_zlib = require("node:zlib");
35
+ let node_os = require("node:os");
34
36
  let node_fs_promises = require("node:fs/promises");
37
+ //#region src/deps/zip-extract.ts
38
+ /**
39
+ * A zip extractor with no external binary behind it.
40
+ *
41
+ * Why it exists: on Windows there is no `unzip`, and the `tar` that is on PATH
42
+ * is whichever the machine has (Git for Windows ships a GNU tar that cannot
43
+ * read a zip). The two archives a Windows node downloads — the portable Python
44
+ * and the ffmpeg build — are both zips, so the extraction is done here, with
45
+ * `node:zlib`, instead of by a program the machine may not have.
46
+ *
47
+ * Scope is deliberately narrow: stored and deflated entries, no encryption, no
48
+ * zip64. Both are refused by NAME rather than mis-read. Symlinks and unix modes
49
+ * are not restored — POSIX hosts keep using `unzip`, which does.
50
+ */
51
+ var EOCD_SIGNATURE = 101010256;
52
+ var CENTRAL_SIGNATURE = 33639248;
53
+ var LOCAL_SIGNATURE = 67324752;
54
+ var EOCD_MIN_BYTES = 22;
55
+ /** The end-of-central-directory record is within the last 22 + 65535 bytes. */
56
+ var EOCD_SEARCH_BYTES = 65557;
57
+ var ZIP64_SENTINEL = 4294967295;
58
+ var METHOD_STORED = 0;
59
+ var METHOD_DEFLATE = 8;
60
+ var FLAG_ENCRYPTED = 1;
61
+ function findEndOfCentralDirectory(buf) {
62
+ const lowest = Math.max(0, buf.length - EOCD_SEARCH_BYTES);
63
+ for (let i = buf.length - EOCD_MIN_BYTES; i >= lowest; i--) if (buf.readUInt32LE(i) === EOCD_SIGNATURE) return i;
64
+ throw new Error("not a zip archive: no end-of-central-directory record");
65
+ }
66
+ function readEntries(buf) {
67
+ const eocd = findEndOfCentralDirectory(buf);
68
+ const count = buf.readUInt16LE(eocd + 10);
69
+ let offset = buf.readUInt32LE(eocd + 16);
70
+ if (count === 65535 || offset === ZIP64_SENTINEL) throw new Error("zip64 archives are not supported");
71
+ const entries = [];
72
+ for (let n = 0; n < count; n++) {
73
+ if (buf.readUInt32LE(offset) !== CENTRAL_SIGNATURE) throw new Error(`corrupt zip: bad central directory entry ${n}`);
74
+ if ((buf.readUInt16LE(offset + 8) & FLAG_ENCRYPTED) !== 0) throw new Error("encrypted zip entries are not supported");
75
+ const compressedSize = buf.readUInt32LE(offset + 20);
76
+ const uncompressedSize = buf.readUInt32LE(offset + 24);
77
+ const localHeaderOffset = buf.readUInt32LE(offset + 42);
78
+ if (compressedSize === ZIP64_SENTINEL || uncompressedSize === ZIP64_SENTINEL || localHeaderOffset === ZIP64_SENTINEL) throw new Error("zip64 entries are not supported");
79
+ const nameLength = buf.readUInt16LE(offset + 28);
80
+ const extraLength = buf.readUInt16LE(offset + 30);
81
+ const commentLength = buf.readUInt16LE(offset + 32);
82
+ entries.push({
83
+ name: buf.toString("utf8", offset + 46, offset + 46 + nameLength),
84
+ method: buf.readUInt16LE(offset + 10),
85
+ crc: buf.readUInt32LE(offset + 16),
86
+ compressedSize,
87
+ uncompressedSize,
88
+ localHeaderOffset
89
+ });
90
+ offset += 46 + nameLength + extraLength + commentLength;
91
+ }
92
+ return entries;
93
+ }
94
+ function readEntryData(buf, entry) {
95
+ const at = entry.localHeaderOffset;
96
+ if (buf.readUInt32LE(at) !== LOCAL_SIGNATURE) throw new Error(`corrupt zip: bad local header for ${entry.name}`);
97
+ const start = at + 30 + buf.readUInt16LE(at + 26) + buf.readUInt16LE(at + 28);
98
+ const raw = buf.subarray(start, start + entry.compressedSize);
99
+ let data;
100
+ if (entry.method === METHOD_STORED) data = raw;
101
+ else if (entry.method === METHOD_DEFLATE) data = (0, node_zlib.inflateRawSync)(raw);
102
+ else throw new Error(`unsupported zip compression method ${entry.method} for ${entry.name}`);
103
+ if (data.length !== entry.uncompressedSize || (0, node_zlib.crc32)(data) !== entry.crc) throw new Error(`corrupt zip: ${entry.name} does not match its recorded size/CRC`);
104
+ return data;
105
+ }
106
+ /**
107
+ * Where an entry lands, or a refusal when its name climbs out of `destDir`
108
+ * (a "zip slip": `..\..\evil.dll`, an absolute path, a drive letter).
109
+ */
110
+ function targetPath(destDir, entryName) {
111
+ const root = (0, node_path.resolve)(destDir);
112
+ const target = (0, node_path.resolve)(root, entryName.replace(/\\/g, "/"));
113
+ if (target !== root && !target.startsWith(root + node_path.sep)) throw new Error(`zip entry escapes the target directory: ${entryName}`);
114
+ return target;
115
+ }
116
+ function extractZipArchive(archivePath, destDir) {
117
+ const buf = (0, node_fs.readFileSync)(archivePath);
118
+ (0, node_fs.mkdirSync)(destDir, { recursive: true });
119
+ for (const entry of readEntries(buf)) {
120
+ const target = targetPath(destDir, entry.name);
121
+ if (entry.name.endsWith("/") || entry.name.endsWith("\\")) {
122
+ (0, node_fs.mkdirSync)(target, { recursive: true });
123
+ continue;
124
+ }
125
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(target), { recursive: true });
126
+ (0, node_fs.writeFileSync)(target, readEntryData(buf, entry));
127
+ }
128
+ }
129
+ //#endregion
130
+ //#region src/deps/archive-extract.ts
131
+ /**
132
+ * The one place an archive a node downloaded is unpacked.
133
+ *
134
+ * POSIX hosts keep their native tools (`unzip` restores modes and symlinks,
135
+ * which the portable Python relies on). Windows has neither a guaranteed
136
+ * `unzip` nor a guaranteed zip-capable `tar` on PATH, so a zip is unpacked by
137
+ * the in-process extractor, and a tarball by the `tar.exe` Windows ships in
138
+ * System32 (bsdtar) — addressed by absolute path so a GNU tar that Git for
139
+ * Windows put earlier on PATH cannot shadow it.
140
+ */
141
+ var defaultDeps = {
142
+ platform: process.platform,
143
+ systemRoot: process.env["SystemRoot"],
144
+ exec: (file, args) => (0, node_child_process.execFileSync)(file, [...args], { stdio: "pipe" }),
145
+ extractZip: extractZipArchive
146
+ };
147
+ /** The tar to run: Windows' own bsdtar by absolute path, `tar` elsewhere. */
148
+ function tarExecutable(platform, systemRoot) {
149
+ if (platform !== "win32") return "tar";
150
+ return (0, node_path.join)(systemRoot ?? "C:\\Windows", "System32", "tar.exe");
151
+ }
152
+ function extractArchive(archivePath, format, destDir, overrides = {}) {
153
+ const deps = {
154
+ ...defaultDeps,
155
+ ...overrides
156
+ };
157
+ const tar = tarExecutable(deps.platform, deps.systemRoot);
158
+ if (format === "zip") {
159
+ if (deps.platform === "win32") {
160
+ deps.extractZip(archivePath, destDir);
161
+ return;
162
+ }
163
+ try {
164
+ deps.exec("unzip", [
165
+ "-o",
166
+ "-q",
167
+ archivePath,
168
+ "-d",
169
+ destDir
170
+ ]);
171
+ } catch {
172
+ deps.exec(tar, [
173
+ "-xf",
174
+ archivePath,
175
+ "-C",
176
+ destDir
177
+ ]);
178
+ }
179
+ return;
180
+ }
181
+ deps.exec(tar, [
182
+ format === "tar.xz" ? "-xJf" : "-xzf",
183
+ archivePath,
184
+ "-C",
185
+ destDir
186
+ ]);
187
+ }
188
+ //#endregion
35
189
  //#region src/deps/binary-downloader.ts
36
190
  /**
37
191
  * Recursively find the first file named exactly `name` under `dir`. Used as the
@@ -80,7 +234,7 @@ function findInPath(name) {
80
234
  * verified" is the only state in which running this binary is worse than not
81
235
  * having it. The partial file is removed so a retry cannot pick it up.
82
236
  */
83
- function verifyChecksum(file, expected, name, url) {
237
+ function verifySha256(file, expected, name, url) {
84
238
  if (expected === void 0) return;
85
239
  const actual = (0, node_crypto.createHash)("sha256").update((0, node_fs.readFileSync)(file)).digest("hex");
86
240
  if (actual === expected) return;
@@ -114,42 +268,15 @@ async function downloadBinary(opts) {
114
268
  const ext = archiveFormat ?? "tar.gz";
115
269
  const tmpArchive = (0, node_path.join)(targetDir, `${name}-download.${ext}`);
116
270
  await (0, node_stream_promises.pipeline)(node_stream.Readable.fromWeb(response.body), (0, node_fs.createWriteStream)(tmpArchive));
117
- verifyChecksum(tmpArchive, opts.sha256, name, url);
271
+ verifySha256(tmpArchive, opts.sha256, name, url);
118
272
  const tmpExtractDir = (0, node_path.join)(targetDir, `${name}-extract`);
119
273
  (0, node_fs.mkdirSync)(tmpExtractDir, { recursive: true });
120
- if (ext === "zip") try {
121
- (0, node_child_process.execFileSync)("unzip", [
122
- "-o",
123
- "-q",
124
- tmpArchive,
125
- "-d",
126
- tmpExtractDir
127
- ], { stdio: "pipe" });
128
- } catch {
129
- (0, node_child_process.execFileSync)("tar", [
130
- "-xf",
131
- tmpArchive,
132
- "-C",
133
- tmpExtractDir
134
- ], { stdio: "pipe" });
135
- }
136
- else if (ext === "tar.xz") (0, node_child_process.execFileSync)("tar", [
137
- "-xJf",
138
- tmpArchive,
139
- "-C",
140
- tmpExtractDir
141
- ], { stdio: "pipe" });
142
- else (0, node_child_process.execFileSync)("tar", [
143
- "-xzf",
144
- tmpArchive,
145
- "-C",
146
- tmpExtractDir
147
- ], { stdio: "pipe" });
274
+ extractArchive(tmpArchive, ext, tmpExtractDir);
148
275
  if (archiveInnerPath) {
149
276
  const { copyFileSync } = await import("node:fs");
150
277
  let sourcePath = (0, node_path.join)(tmpExtractDir, archiveInnerPath);
151
278
  if (!(0, node_fs.existsSync)(sourcePath)) {
152
- const found = findFileByName(tmpExtractDir, name);
279
+ const found = findFileByName(tmpExtractDir, targetName) ?? findFileByName(tmpExtractDir, name);
153
280
  if (found === null) throw new Error(`Binary "${name}" not found in archive (tried inner path "${archiveInnerPath}" and a recursive search)`);
154
281
  sourcePath = found;
155
282
  }
@@ -163,7 +290,7 @@ async function downloadBinary(opts) {
163
290
  });
164
291
  } else {
165
292
  await (0, node_stream_promises.pipeline)(node_stream.Readable.fromWeb(response.body), (0, node_fs.createWriteStream)(targetPath));
166
- verifyChecksum(targetPath, opts.sha256, name, url);
293
+ verifySha256(targetPath, opts.sha256, name, url);
167
294
  }
168
295
  (0, node_fs.chmodSync)(targetPath, 493);
169
296
  logger.info("Binary downloaded", { meta: {
@@ -392,6 +519,135 @@ async function ensureFfmpeg(dataDir, logger) {
392
519
  return path;
393
520
  }
394
521
  //#endregion
522
+ //#region src/util/owner-only-file.ts
523
+ /**
524
+ * Files only their owner may read: the one place a secret-bearing file is
525
+ * restricted, on every OS.
526
+ *
527
+ * `mode: 0o600` is a POSIX idea. On Windows Node maps it to the read-only
528
+ * attribute and nothing else, and `chmod` is a no-op for permissions, so a
529
+ * "0600" `agent.json` (the cluster secret, the mesh private key) would have
530
+ * been readable by every local user. Windows has its own tool for this:
531
+ * `icacls`. This module removes the inherited ACL entries and grants access to
532
+ * the current user and SYSTEM only — the Windows reading of "0600".
533
+ *
534
+ * ZERO imports beyond Node built-ins, on purpose: the Electron shell bundles
535
+ * this single file (see `packages/desktop/tsup.config.ts`) instead of the
536
+ * whole `@camstack/types` barrel.
537
+ *
538
+ * Failure is NOT swallowed. A secret file that could not be restricted is a
539
+ * secret file the caller must know about, so `OwnerOnlyError` propagates; the
540
+ * write order below makes sure nothing sensitive is in the file yet when that
541
+ * happens (the temp file is empty until it has been restricted).
542
+ *
543
+ * Out of scope on purpose: files that are only tidy, not secret (the job
544
+ * journal, the addon manifest) keep a plain `mode`, since one `icacls` per
545
+ * write would be a process spawn on a hot path.
546
+ */
547
+ /** `*S-1-5-18` is SYSTEM in every locale — a name like "SYSTEM" is not. */
548
+ var WINDOWS_SYSTEM_SID = "*S-1-5-18";
549
+ var FILE_MODE = 384;
550
+ var DIR_MODE = 448;
551
+ var OwnerOnlyError = class extends Error {
552
+ target;
553
+ constructor(target, detail) {
554
+ super(`could not restrict ${target} to its owner: ${detail}`);
555
+ this.target = target;
556
+ this.name = "OwnerOnlyError";
557
+ }
558
+ };
559
+ function currentPrincipal() {
560
+ const user = process.env["USERNAME"] ?? (0, node_os.userInfo)().username;
561
+ const domain = process.env["USERDOMAIN"];
562
+ return domain !== void 0 && domain.length > 0 ? `${domain}\\${user}` : user;
563
+ }
564
+ /** Resolved per call, not at import: a platform stubbed by a test must be seen. */
565
+ function resolveDeps(overrides) {
566
+ return {
567
+ platform: process.platform,
568
+ exec: (file, args) => {
569
+ (0, node_child_process.execFileSync)(file, [...args], {
570
+ stdio: "pipe",
571
+ windowsHide: true
572
+ });
573
+ },
574
+ principal: currentPrincipal,
575
+ systemRoot: process.env["SystemRoot"],
576
+ ...overrides
577
+ };
578
+ }
579
+ /** The icacls argv for one path: no inheritance, owner + SYSTEM only. */
580
+ function icaclsArgs(target, principal, isDirectory) {
581
+ const inherit = isDirectory ? "(OI)(CI)" : "";
582
+ return [
583
+ target,
584
+ "/inheritance:r",
585
+ "/grant:r",
586
+ `${principal}:${inherit}F`,
587
+ "/grant:r",
588
+ `${WINDOWS_SYSTEM_SID}:${inherit}F`
589
+ ];
590
+ }
591
+ function restrictWindows(target, isDirectory, deps) {
592
+ const icacls = node_path.win32.join(deps.systemRoot ?? "C:\\Windows", "System32", "icacls.exe");
593
+ try {
594
+ deps.exec(icacls, icaclsArgs(target, deps.principal(), isDirectory));
595
+ } catch (err) {
596
+ throw new OwnerOnlyError(target, err instanceof Error ? err.message : String(err));
597
+ }
598
+ }
599
+ /** Restrict an existing file to its owner (chmod 0600 / owner+SYSTEM ACL). */
600
+ function restrictFileToOwnerSync(target, overrides = {}) {
601
+ const deps = resolveDeps(overrides);
602
+ if (deps.platform === "win32") {
603
+ restrictWindows(target, false, deps);
604
+ return;
605
+ }
606
+ node_fs.chmodSync(target, FILE_MODE);
607
+ }
608
+ /** Restrict an existing directory to its owner (chmod 0700 / owner+SYSTEM, inherited). */
609
+ function restrictDirToOwnerSync(target, overrides = {}) {
610
+ const deps = resolveDeps(overrides);
611
+ if (deps.platform === "win32") {
612
+ restrictWindows(target, true, deps);
613
+ return;
614
+ }
615
+ node_fs.chmodSync(target, DIR_MODE);
616
+ }
617
+ /** Create `dir` (and parents) readable by its owner only. */
618
+ function mkdirOwnerOnlySync(dir, overrides = {}) {
619
+ const deps = resolveDeps(overrides);
620
+ node_fs.mkdirSync(dir, {
621
+ recursive: true,
622
+ mode: DIR_MODE
623
+ });
624
+ if (deps.platform === "win32") {
625
+ restrictWindows(dir, true, deps);
626
+ return;
627
+ }
628
+ node_fs.chmodSync(dir, DIR_MODE);
629
+ }
630
+ /**
631
+ * Atomically write a secret-bearing file that only its owner can read.
632
+ *
633
+ * Order matters: the temp file is created EMPTY, restricted, and only then
634
+ * given its content, so there is no moment at which the secret exists under a
635
+ * permissive ACL. The rename carries the (already restricted) ACL with it.
636
+ */
637
+ function writeOwnerOnlyFileSync(target, data, overrides = {}) {
638
+ node_fs.mkdirSync(node_path.dirname(target), { recursive: true });
639
+ const tmp = `${target}.tmp.${process.pid}`;
640
+ node_fs.writeFileSync(tmp, "", { mode: FILE_MODE });
641
+ try {
642
+ restrictFileToOwnerSync(tmp, overrides);
643
+ node_fs.writeFileSync(tmp, data);
644
+ node_fs.renameSync(tmp, target);
645
+ } catch (err) {
646
+ node_fs.rmSync(tmp, { force: true });
647
+ throw err;
648
+ }
649
+ }
650
+ //#endregion
395
651
  //#region src/deps/python-downloader.ts
396
652
  /**
397
653
  * Download portable Python (headless) from bjia56/portable-python.
@@ -407,11 +663,26 @@ async function ensureFfmpeg(dataDir, logger) {
407
663
  * The zip's top-level dir is platform-specific (e.g.
408
664
  * `python-headless-3.12.12-darwin-universal2/`); after extraction
409
665
  * we rename it to a stable `python/` so consumers can rely on
410
- * `<dataDir>/deps/python/bin/python3`.
666
+ * `<dataDir>/deps/python/bin/python3` (`bin/python.exe` on Windows: that build
667
+ * keeps its launcher in `bin/` beside the DLLs, with `Lib/` at the root, so
668
+ * `<root>/bin/<interpreter>` holds on every platform).
411
669
  */
412
670
  var PYTHON_VERSION = "3.12.12";
413
671
  var PP_BASE = `https://github.com/bjia56/portable-python/releases/download/cpython-v${PYTHON_VERSION}-build.0`;
414
- function getPythonDownloadUrl(platform, arch) {
672
+ /**
673
+ * One pinned artifact per target. The publisher ships no checksums on its
674
+ * assets, so these are ours, taken (`shasum -a 256`) when the version was
675
+ * pinned; the download is verified against them before a byte is extracted.
676
+ * The Windows build is the same upstream, release and `headless` variant.
677
+ */
678
+ var PYTHON_SHA256 = {
679
+ "linux-x86_64": "b618bcf490ab24fcfcd8f69e091b9d6f3ba23279dd1255aa258617a4d2af6fb7",
680
+ "linux-aarch64": "de204124ee834851c77e3392b00c3950869d349f99dbc7345f8cd5959bdee5bf",
681
+ "darwin-universal2": "7de186d2f865c9c9544dab2cf56d38de99767e243ed21ed7e1b0c31451783584",
682
+ "windows-x86_64": "84c10dd1a202da4694f41a1f99aee359c7c1276355937401d2f8facd9567d026"
683
+ };
684
+ /** The upstream target token for a platform/arch, e.g. `windows-x86_64`. */
685
+ function pythonTarget(platform, arch) {
415
686
  switch (platform) {
416
687
  case "linux": {
417
688
  const a = {
@@ -419,12 +690,28 @@ function getPythonDownloadUrl(platform, arch) {
419
690
  arm64: "aarch64"
420
691
  }[arch];
421
692
  if (!a) throw new Error(`Unsupported Linux architecture for Python: ${arch}`);
422
- return `${PP_BASE}/python-headless-${PYTHON_VERSION}-linux-${a}.zip`;
693
+ return `linux-${a}`;
423
694
  }
424
- case "darwin": return `${PP_BASE}/python-headless-${PYTHON_VERSION}-darwin-universal2.zip`;
695
+ case "darwin": return "darwin-universal2";
696
+ case "win32":
697
+ if (arch !== "x64") throw new Error(`Unsupported Windows architecture for Python: ${arch}`);
698
+ return "windows-x86_64";
425
699
  default: throw new Error(`Unsupported platform for portable Python: ${platform}`);
426
700
  }
427
701
  }
702
+ function getPythonDownloadUrl(platform, arch) {
703
+ return `${PP_BASE}/python-headless-${PYTHON_VERSION}-${pythonTarget(platform, arch)}.zip`;
704
+ }
705
+ /** The SHA-256 the archive for this platform/arch must have. */
706
+ function getPythonSha256(platform, arch) {
707
+ const sha = PYTHON_SHA256[pythonTarget(platform, arch)];
708
+ if (sha === void 0) throw new Error(`No pinned checksum for Python on ${platform}-${arch}`);
709
+ return sha;
710
+ }
711
+ /** The interpreter's path relative to the install root. */
712
+ function pythonInterpreterSegments(platform) {
713
+ return platform === "win32" ? ["bin", "python.exe"] : ["bin", "python3"];
714
+ }
428
715
  /**
429
716
  * Ensure the embedded portable Python is available.
430
717
  *
@@ -439,7 +726,7 @@ function getPythonDownloadUrl(platform, arch) {
439
726
  */
440
727
  async function ensurePython(dataDir, logger) {
441
728
  const pythonDir = (0, node_path.join)(dataDir, "deps", "python");
442
- const pythonBin = (0, node_path.join)(pythonDir, "bin", "python3");
729
+ const pythonBin = (0, node_path.join)(pythonDir, ...pythonInterpreterSegments(process.platform));
443
730
  if ((0, node_fs.existsSync)(pythonBin)) {
444
731
  logger.debug("Portable Python found", { meta: { pythonBin } });
445
732
  return pythonBin;
@@ -456,27 +743,12 @@ async function ensurePython(dataDir, logger) {
456
743
  return null;
457
744
  }
458
745
  await (0, node_stream_promises.pipeline)(node_stream.Readable.fromWeb(response.body), (0, node_fs.createWriteStream)(tmpArchive));
459
- try {
460
- (0, node_child_process.execFileSync)("unzip", [
461
- "-o",
462
- "-q",
463
- tmpArchive,
464
- "-d",
465
- depsDir
466
- ], { stdio: "pipe" });
467
- } catch {
468
- (0, node_child_process.execFileSync)("python3", [
469
- "-m",
470
- "zipfile",
471
- "-e",
472
- tmpArchive,
473
- depsDir
474
- ], { stdio: "pipe" });
475
- }
746
+ verifySha256(tmpArchive, getPythonSha256(process.platform, process.arch), "python", url);
747
+ extractArchive(tmpArchive, "zip", depsDir);
476
748
  const { unlinkSync } = await import("node:fs");
477
749
  unlinkSync(tmpArchive);
478
750
  if (!(0, node_fs.existsSync)(pythonBin)) {
479
- const candidates = (0, node_fs.readdirSync)(depsDir).filter((name) => name.startsWith("python-") && (0, node_fs.existsSync)((0, node_path.join)(depsDir, name, "bin", "python3")));
751
+ const candidates = (0, node_fs.readdirSync)(depsDir).filter((name) => name.startsWith("python-") && (0, node_fs.existsSync)((0, node_path.join)(depsDir, name, ...pythonInterpreterSegments(process.platform))));
480
752
  if (candidates.length === 0) {
481
753
  logger.warn("Python extraction succeeded but no python3 binary found in any subdir", { meta: { depsDir } });
482
754
  return null;
@@ -2006,6 +2278,7 @@ exports.Fmp4FragmentChild = Fmp4FragmentChild;
2006
2278
  exports.Fmp4FragmentPlane = Fmp4FragmentPlane;
2007
2279
  exports.NO_COST_CLAIM = NO_COST_CLAIM;
2008
2280
  exports.NO_PINNED_FFMPEG_DETAIL = NO_PINNED_FFMPEG_DETAIL;
2281
+ exports.OwnerOnlyError = OwnerOnlyError;
2009
2282
  exports.PYTHON_VERSION = PYTHON_VERSION;
2010
2283
  exports.buildBinaryPath = buildBinaryPath;
2011
2284
  exports.canonicalDeviceFingerprint = canonicalDeviceFingerprint;
@@ -2022,8 +2295,10 @@ exports.getFfmpegArtifact = require_composition.getFfmpegArtifact;
2022
2295
  exports.getFfmpegDownloadUrl = require_composition.getFfmpegDownloadUrl;
2023
2296
  exports.getPlatformInfo = getPlatformInfo;
2024
2297
  exports.getPythonDownloadUrl = getPythonDownloadUrl;
2298
+ exports.icaclsArgs = icaclsArgs;
2025
2299
  exports.installPythonPackages = installPythonPackages;
2026
2300
  exports.installPythonRequirements = installPythonRequirements;
2301
+ exports.mkdirOwnerOnlySync = mkdirOwnerOnlySync;
2027
2302
  exports.nodeProcStatReader = nodeProcStatReader;
2028
2303
  exports.parseFfmpegCapabilities = parseFfmpegCapabilities;
2029
2304
  exports.parseProcCpuSeconds = parseProcCpuSeconds;
@@ -2034,6 +2309,9 @@ exports.probeFfmpegBinary = probeFfmpegBinary;
2034
2309
  exports.readProcessCost = readProcessCost;
2035
2310
  exports.resolveExportFingerprint = resolveExportFingerprint;
2036
2311
  exports.resolvePinnedFfmpegSpawner = resolvePinnedFfmpegSpawner;
2312
+ exports.restrictDirToOwnerSync = restrictDirToOwnerSync;
2313
+ exports.restrictFileToOwnerSync = restrictFileToOwnerSync;
2037
2314
  exports.signExpiringUrl = signExpiringUrl;
2038
2315
  exports.verifyExpiringUrl = verifyExpiringUrl;
2039
2316
  exports.writeFileAtomic = writeFileAtomic;
2317
+ exports.writeOwnerOnlyFileSync = writeOwnerOnlyFileSync;