@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.mjs CHANGED
@@ -1,14 +1,168 @@
1
- import { At as Fmp4BoxSplitter, Ct as FFMPEG_PATH_ENV, Dt as getFfmpegArtifact, Et as FFMPEG_RELEASE, Ot as getFfmpegDownloadUrl, P as compositionSourceKey, Pt as buildFfmpegArgs, Rt as isSoftwareDecode, Tt as ffmpegBinaryName, a as CUSTOMIZATION_BLOCK_NAME_PREFIX, kt as canonicalHash, wt as chooseFfmpegBinary } from "./composition-Bzr7GwgP.mjs";
1
+ import { At as Fmp4BoxSplitter, Ct as FFMPEG_PATH_ENV, Dt as getFfmpegArtifact, Et as FFMPEG_RELEASE, Ot as getFfmpegDownloadUrl, P as compositionSourceKey, Pt as buildFfmpegArgs, Rt as isSoftwareDecode, Tt as ffmpegBinaryName, a as CUSTOMIZATION_BLOCK_NAME_PREFIX, kt as canonicalHash, wt as chooseFfmpegBinary } from "./composition-C-WpVYTX.mjs";
2
2
  import { t as errMsg } from "./err-msg-DX6i_MY4.mjs";
3
3
  import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
4
4
  import * as fs from "node:fs";
5
5
  import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
6
6
  import * as path$1 from "node:path";
7
- import path, { basename, join } from "node:path";
7
+ import path, { basename, dirname, join, resolve, sep } from "node:path";
8
8
  import { pipeline } from "node:stream/promises";
9
9
  import { Readable } from "node:stream";
10
10
  import { execFile, execFileSync, spawn } from "node:child_process";
11
+ import { crc32, inflateRawSync } from "node:zlib";
12
+ import { userInfo } from "node:os";
11
13
  import { readFile } from "node:fs/promises";
14
+ //#region src/deps/zip-extract.ts
15
+ /**
16
+ * A zip extractor with no external binary behind it.
17
+ *
18
+ * Why it exists: on Windows there is no `unzip`, and the `tar` that is on PATH
19
+ * is whichever the machine has (Git for Windows ships a GNU tar that cannot
20
+ * read a zip). The two archives a Windows node downloads — the portable Python
21
+ * and the ffmpeg build — are both zips, so the extraction is done here, with
22
+ * `node:zlib`, instead of by a program the machine may not have.
23
+ *
24
+ * Scope is deliberately narrow: stored and deflated entries, no encryption, no
25
+ * zip64. Both are refused by NAME rather than mis-read. Symlinks and unix modes
26
+ * are not restored — POSIX hosts keep using `unzip`, which does.
27
+ */
28
+ var EOCD_SIGNATURE = 101010256;
29
+ var CENTRAL_SIGNATURE = 33639248;
30
+ var LOCAL_SIGNATURE = 67324752;
31
+ var EOCD_MIN_BYTES = 22;
32
+ /** The end-of-central-directory record is within the last 22 + 65535 bytes. */
33
+ var EOCD_SEARCH_BYTES = 65557;
34
+ var ZIP64_SENTINEL = 4294967295;
35
+ var METHOD_STORED = 0;
36
+ var METHOD_DEFLATE = 8;
37
+ var FLAG_ENCRYPTED = 1;
38
+ function findEndOfCentralDirectory(buf) {
39
+ const lowest = Math.max(0, buf.length - EOCD_SEARCH_BYTES);
40
+ for (let i = buf.length - EOCD_MIN_BYTES; i >= lowest; i--) if (buf.readUInt32LE(i) === EOCD_SIGNATURE) return i;
41
+ throw new Error("not a zip archive: no end-of-central-directory record");
42
+ }
43
+ function readEntries(buf) {
44
+ const eocd = findEndOfCentralDirectory(buf);
45
+ const count = buf.readUInt16LE(eocd + 10);
46
+ let offset = buf.readUInt32LE(eocd + 16);
47
+ if (count === 65535 || offset === ZIP64_SENTINEL) throw new Error("zip64 archives are not supported");
48
+ const entries = [];
49
+ for (let n = 0; n < count; n++) {
50
+ if (buf.readUInt32LE(offset) !== CENTRAL_SIGNATURE) throw new Error(`corrupt zip: bad central directory entry ${n}`);
51
+ if ((buf.readUInt16LE(offset + 8) & FLAG_ENCRYPTED) !== 0) throw new Error("encrypted zip entries are not supported");
52
+ const compressedSize = buf.readUInt32LE(offset + 20);
53
+ const uncompressedSize = buf.readUInt32LE(offset + 24);
54
+ const localHeaderOffset = buf.readUInt32LE(offset + 42);
55
+ if (compressedSize === ZIP64_SENTINEL || uncompressedSize === ZIP64_SENTINEL || localHeaderOffset === ZIP64_SENTINEL) throw new Error("zip64 entries are not supported");
56
+ const nameLength = buf.readUInt16LE(offset + 28);
57
+ const extraLength = buf.readUInt16LE(offset + 30);
58
+ const commentLength = buf.readUInt16LE(offset + 32);
59
+ entries.push({
60
+ name: buf.toString("utf8", offset + 46, offset + 46 + nameLength),
61
+ method: buf.readUInt16LE(offset + 10),
62
+ crc: buf.readUInt32LE(offset + 16),
63
+ compressedSize,
64
+ uncompressedSize,
65
+ localHeaderOffset
66
+ });
67
+ offset += 46 + nameLength + extraLength + commentLength;
68
+ }
69
+ return entries;
70
+ }
71
+ function readEntryData(buf, entry) {
72
+ const at = entry.localHeaderOffset;
73
+ if (buf.readUInt32LE(at) !== LOCAL_SIGNATURE) throw new Error(`corrupt zip: bad local header for ${entry.name}`);
74
+ const start = at + 30 + buf.readUInt16LE(at + 26) + buf.readUInt16LE(at + 28);
75
+ const raw = buf.subarray(start, start + entry.compressedSize);
76
+ let data;
77
+ if (entry.method === METHOD_STORED) data = raw;
78
+ else if (entry.method === METHOD_DEFLATE) data = inflateRawSync(raw);
79
+ else throw new Error(`unsupported zip compression method ${entry.method} for ${entry.name}`);
80
+ if (data.length !== entry.uncompressedSize || crc32(data) !== entry.crc) throw new Error(`corrupt zip: ${entry.name} does not match its recorded size/CRC`);
81
+ return data;
82
+ }
83
+ /**
84
+ * Where an entry lands, or a refusal when its name climbs out of `destDir`
85
+ * (a "zip slip": `..\..\evil.dll`, an absolute path, a drive letter).
86
+ */
87
+ function targetPath(destDir, entryName) {
88
+ const root = resolve(destDir);
89
+ const target = resolve(root, entryName.replace(/\\/g, "/"));
90
+ if (target !== root && !target.startsWith(root + sep)) throw new Error(`zip entry escapes the target directory: ${entryName}`);
91
+ return target;
92
+ }
93
+ function extractZipArchive(archivePath, destDir) {
94
+ const buf = readFileSync(archivePath);
95
+ mkdirSync(destDir, { recursive: true });
96
+ for (const entry of readEntries(buf)) {
97
+ const target = targetPath(destDir, entry.name);
98
+ if (entry.name.endsWith("/") || entry.name.endsWith("\\")) {
99
+ mkdirSync(target, { recursive: true });
100
+ continue;
101
+ }
102
+ mkdirSync(dirname(target), { recursive: true });
103
+ writeFileSync(target, readEntryData(buf, entry));
104
+ }
105
+ }
106
+ //#endregion
107
+ //#region src/deps/archive-extract.ts
108
+ /**
109
+ * The one place an archive a node downloaded is unpacked.
110
+ *
111
+ * POSIX hosts keep their native tools (`unzip` restores modes and symlinks,
112
+ * which the portable Python relies on). Windows has neither a guaranteed
113
+ * `unzip` nor a guaranteed zip-capable `tar` on PATH, so a zip is unpacked by
114
+ * the in-process extractor, and a tarball by the `tar.exe` Windows ships in
115
+ * System32 (bsdtar) — addressed by absolute path so a GNU tar that Git for
116
+ * Windows put earlier on PATH cannot shadow it.
117
+ */
118
+ var defaultDeps = {
119
+ platform: process.platform,
120
+ systemRoot: process.env["SystemRoot"],
121
+ exec: (file, args) => execFileSync(file, [...args], { stdio: "pipe" }),
122
+ extractZip: extractZipArchive
123
+ };
124
+ /** The tar to run: Windows' own bsdtar by absolute path, `tar` elsewhere. */
125
+ function tarExecutable(platform, systemRoot) {
126
+ if (platform !== "win32") return "tar";
127
+ return join(systemRoot ?? "C:\\Windows", "System32", "tar.exe");
128
+ }
129
+ function extractArchive(archivePath, format, destDir, overrides = {}) {
130
+ const deps = {
131
+ ...defaultDeps,
132
+ ...overrides
133
+ };
134
+ const tar = tarExecutable(deps.platform, deps.systemRoot);
135
+ if (format === "zip") {
136
+ if (deps.platform === "win32") {
137
+ deps.extractZip(archivePath, destDir);
138
+ return;
139
+ }
140
+ try {
141
+ deps.exec("unzip", [
142
+ "-o",
143
+ "-q",
144
+ archivePath,
145
+ "-d",
146
+ destDir
147
+ ]);
148
+ } catch {
149
+ deps.exec(tar, [
150
+ "-xf",
151
+ archivePath,
152
+ "-C",
153
+ destDir
154
+ ]);
155
+ }
156
+ return;
157
+ }
158
+ deps.exec(tar, [
159
+ format === "tar.xz" ? "-xJf" : "-xzf",
160
+ archivePath,
161
+ "-C",
162
+ destDir
163
+ ]);
164
+ }
165
+ //#endregion
12
166
  //#region src/deps/binary-downloader.ts
13
167
  /**
14
168
  * Recursively find the first file named exactly `name` under `dir`. Used as the
@@ -57,7 +211,7 @@ function findInPath(name) {
57
211
  * verified" is the only state in which running this binary is worse than not
58
212
  * having it. The partial file is removed so a retry cannot pick it up.
59
213
  */
60
- function verifyChecksum(file, expected, name, url) {
214
+ function verifySha256(file, expected, name, url) {
61
215
  if (expected === void 0) return;
62
216
  const actual = createHash("sha256").update(readFileSync(file)).digest("hex");
63
217
  if (actual === expected) return;
@@ -91,42 +245,15 @@ async function downloadBinary(opts) {
91
245
  const ext = archiveFormat ?? "tar.gz";
92
246
  const tmpArchive = join(targetDir, `${name}-download.${ext}`);
93
247
  await pipeline(Readable.fromWeb(response.body), createWriteStream(tmpArchive));
94
- verifyChecksum(tmpArchive, opts.sha256, name, url);
248
+ verifySha256(tmpArchive, opts.sha256, name, url);
95
249
  const tmpExtractDir = join(targetDir, `${name}-extract`);
96
250
  mkdirSync(tmpExtractDir, { recursive: true });
97
- if (ext === "zip") try {
98
- execFileSync("unzip", [
99
- "-o",
100
- "-q",
101
- tmpArchive,
102
- "-d",
103
- tmpExtractDir
104
- ], { stdio: "pipe" });
105
- } catch {
106
- execFileSync("tar", [
107
- "-xf",
108
- tmpArchive,
109
- "-C",
110
- tmpExtractDir
111
- ], { stdio: "pipe" });
112
- }
113
- else if (ext === "tar.xz") execFileSync("tar", [
114
- "-xJf",
115
- tmpArchive,
116
- "-C",
117
- tmpExtractDir
118
- ], { stdio: "pipe" });
119
- else execFileSync("tar", [
120
- "-xzf",
121
- tmpArchive,
122
- "-C",
123
- tmpExtractDir
124
- ], { stdio: "pipe" });
251
+ extractArchive(tmpArchive, ext, tmpExtractDir);
125
252
  if (archiveInnerPath) {
126
253
  const { copyFileSync } = await import("node:fs");
127
254
  let sourcePath = join(tmpExtractDir, archiveInnerPath);
128
255
  if (!existsSync(sourcePath)) {
129
- const found = findFileByName(tmpExtractDir, name);
256
+ const found = findFileByName(tmpExtractDir, targetName) ?? findFileByName(tmpExtractDir, name);
130
257
  if (found === null) throw new Error(`Binary "${name}" not found in archive (tried inner path "${archiveInnerPath}" and a recursive search)`);
131
258
  sourcePath = found;
132
259
  }
@@ -140,7 +267,7 @@ async function downloadBinary(opts) {
140
267
  });
141
268
  } else {
142
269
  await pipeline(Readable.fromWeb(response.body), createWriteStream(targetPath));
143
- verifyChecksum(targetPath, opts.sha256, name, url);
270
+ verifySha256(targetPath, opts.sha256, name, url);
144
271
  }
145
272
  chmodSync(targetPath, 493);
146
273
  logger.info("Binary downloaded", { meta: {
@@ -369,6 +496,135 @@ async function ensureFfmpeg(dataDir, logger) {
369
496
  return path;
370
497
  }
371
498
  //#endregion
499
+ //#region src/util/owner-only-file.ts
500
+ /**
501
+ * Files only their owner may read: the one place a secret-bearing file is
502
+ * restricted, on every OS.
503
+ *
504
+ * `mode: 0o600` is a POSIX idea. On Windows Node maps it to the read-only
505
+ * attribute and nothing else, and `chmod` is a no-op for permissions, so a
506
+ * "0600" `agent.json` (the cluster secret, the mesh private key) would have
507
+ * been readable by every local user. Windows has its own tool for this:
508
+ * `icacls`. This module removes the inherited ACL entries and grants access to
509
+ * the current user and SYSTEM only — the Windows reading of "0600".
510
+ *
511
+ * ZERO imports beyond Node built-ins, on purpose: the Electron shell bundles
512
+ * this single file (see `packages/desktop/tsup.config.ts`) instead of the
513
+ * whole `@camstack/types` barrel.
514
+ *
515
+ * Failure is NOT swallowed. A secret file that could not be restricted is a
516
+ * secret file the caller must know about, so `OwnerOnlyError` propagates; the
517
+ * write order below makes sure nothing sensitive is in the file yet when that
518
+ * happens (the temp file is empty until it has been restricted).
519
+ *
520
+ * Out of scope on purpose: files that are only tidy, not secret (the job
521
+ * journal, the addon manifest) keep a plain `mode`, since one `icacls` per
522
+ * write would be a process spawn on a hot path.
523
+ */
524
+ /** `*S-1-5-18` is SYSTEM in every locale — a name like "SYSTEM" is not. */
525
+ var WINDOWS_SYSTEM_SID = "*S-1-5-18";
526
+ var FILE_MODE = 384;
527
+ var DIR_MODE = 448;
528
+ var OwnerOnlyError = class extends Error {
529
+ target;
530
+ constructor(target, detail) {
531
+ super(`could not restrict ${target} to its owner: ${detail}`);
532
+ this.target = target;
533
+ this.name = "OwnerOnlyError";
534
+ }
535
+ };
536
+ function currentPrincipal() {
537
+ const user = process.env["USERNAME"] ?? userInfo().username;
538
+ const domain = process.env["USERDOMAIN"];
539
+ return domain !== void 0 && domain.length > 0 ? `${domain}\\${user}` : user;
540
+ }
541
+ /** Resolved per call, not at import: a platform stubbed by a test must be seen. */
542
+ function resolveDeps(overrides) {
543
+ return {
544
+ platform: process.platform,
545
+ exec: (file, args) => {
546
+ execFileSync(file, [...args], {
547
+ stdio: "pipe",
548
+ windowsHide: true
549
+ });
550
+ },
551
+ principal: currentPrincipal,
552
+ systemRoot: process.env["SystemRoot"],
553
+ ...overrides
554
+ };
555
+ }
556
+ /** The icacls argv for one path: no inheritance, owner + SYSTEM only. */
557
+ function icaclsArgs(target, principal, isDirectory) {
558
+ const inherit = isDirectory ? "(OI)(CI)" : "";
559
+ return [
560
+ target,
561
+ "/inheritance:r",
562
+ "/grant:r",
563
+ `${principal}:${inherit}F`,
564
+ "/grant:r",
565
+ `${WINDOWS_SYSTEM_SID}:${inherit}F`
566
+ ];
567
+ }
568
+ function restrictWindows(target, isDirectory, deps) {
569
+ const icacls = path$1.win32.join(deps.systemRoot ?? "C:\\Windows", "System32", "icacls.exe");
570
+ try {
571
+ deps.exec(icacls, icaclsArgs(target, deps.principal(), isDirectory));
572
+ } catch (err) {
573
+ throw new OwnerOnlyError(target, err instanceof Error ? err.message : String(err));
574
+ }
575
+ }
576
+ /** Restrict an existing file to its owner (chmod 0600 / owner+SYSTEM ACL). */
577
+ function restrictFileToOwnerSync(target, overrides = {}) {
578
+ const deps = resolveDeps(overrides);
579
+ if (deps.platform === "win32") {
580
+ restrictWindows(target, false, deps);
581
+ return;
582
+ }
583
+ fs.chmodSync(target, FILE_MODE);
584
+ }
585
+ /** Restrict an existing directory to its owner (chmod 0700 / owner+SYSTEM, inherited). */
586
+ function restrictDirToOwnerSync(target, overrides = {}) {
587
+ const deps = resolveDeps(overrides);
588
+ if (deps.platform === "win32") {
589
+ restrictWindows(target, true, deps);
590
+ return;
591
+ }
592
+ fs.chmodSync(target, DIR_MODE);
593
+ }
594
+ /** Create `dir` (and parents) readable by its owner only. */
595
+ function mkdirOwnerOnlySync(dir, overrides = {}) {
596
+ const deps = resolveDeps(overrides);
597
+ fs.mkdirSync(dir, {
598
+ recursive: true,
599
+ mode: DIR_MODE
600
+ });
601
+ if (deps.platform === "win32") {
602
+ restrictWindows(dir, true, deps);
603
+ return;
604
+ }
605
+ fs.chmodSync(dir, DIR_MODE);
606
+ }
607
+ /**
608
+ * Atomically write a secret-bearing file that only its owner can read.
609
+ *
610
+ * Order matters: the temp file is created EMPTY, restricted, and only then
611
+ * given its content, so there is no moment at which the secret exists under a
612
+ * permissive ACL. The rename carries the (already restricted) ACL with it.
613
+ */
614
+ function writeOwnerOnlyFileSync(target, data, overrides = {}) {
615
+ fs.mkdirSync(path$1.dirname(target), { recursive: true });
616
+ const tmp = `${target}.tmp.${process.pid}`;
617
+ fs.writeFileSync(tmp, "", { mode: FILE_MODE });
618
+ try {
619
+ restrictFileToOwnerSync(tmp, overrides);
620
+ fs.writeFileSync(tmp, data);
621
+ fs.renameSync(tmp, target);
622
+ } catch (err) {
623
+ fs.rmSync(tmp, { force: true });
624
+ throw err;
625
+ }
626
+ }
627
+ //#endregion
372
628
  //#region src/deps/python-downloader.ts
373
629
  /**
374
630
  * Download portable Python (headless) from bjia56/portable-python.
@@ -384,11 +640,26 @@ async function ensureFfmpeg(dataDir, logger) {
384
640
  * The zip's top-level dir is platform-specific (e.g.
385
641
  * `python-headless-3.12.12-darwin-universal2/`); after extraction
386
642
  * we rename it to a stable `python/` so consumers can rely on
387
- * `<dataDir>/deps/python/bin/python3`.
643
+ * `<dataDir>/deps/python/bin/python3` (`bin/python.exe` on Windows: that build
644
+ * keeps its launcher in `bin/` beside the DLLs, with `Lib/` at the root, so
645
+ * `<root>/bin/<interpreter>` holds on every platform).
388
646
  */
389
647
  var PYTHON_VERSION = "3.12.12";
390
648
  var PP_BASE = `https://github.com/bjia56/portable-python/releases/download/cpython-v${PYTHON_VERSION}-build.0`;
391
- function getPythonDownloadUrl(platform, arch) {
649
+ /**
650
+ * One pinned artifact per target. The publisher ships no checksums on its
651
+ * assets, so these are ours, taken (`shasum -a 256`) when the version was
652
+ * pinned; the download is verified against them before a byte is extracted.
653
+ * The Windows build is the same upstream, release and `headless` variant.
654
+ */
655
+ var PYTHON_SHA256 = {
656
+ "linux-x86_64": "b618bcf490ab24fcfcd8f69e091b9d6f3ba23279dd1255aa258617a4d2af6fb7",
657
+ "linux-aarch64": "de204124ee834851c77e3392b00c3950869d349f99dbc7345f8cd5959bdee5bf",
658
+ "darwin-universal2": "7de186d2f865c9c9544dab2cf56d38de99767e243ed21ed7e1b0c31451783584",
659
+ "windows-x86_64": "84c10dd1a202da4694f41a1f99aee359c7c1276355937401d2f8facd9567d026"
660
+ };
661
+ /** The upstream target token for a platform/arch, e.g. `windows-x86_64`. */
662
+ function pythonTarget(platform, arch) {
392
663
  switch (platform) {
393
664
  case "linux": {
394
665
  const a = {
@@ -396,12 +667,28 @@ function getPythonDownloadUrl(platform, arch) {
396
667
  arm64: "aarch64"
397
668
  }[arch];
398
669
  if (!a) throw new Error(`Unsupported Linux architecture for Python: ${arch}`);
399
- return `${PP_BASE}/python-headless-${PYTHON_VERSION}-linux-${a}.zip`;
670
+ return `linux-${a}`;
400
671
  }
401
- case "darwin": return `${PP_BASE}/python-headless-${PYTHON_VERSION}-darwin-universal2.zip`;
672
+ case "darwin": return "darwin-universal2";
673
+ case "win32":
674
+ if (arch !== "x64") throw new Error(`Unsupported Windows architecture for Python: ${arch}`);
675
+ return "windows-x86_64";
402
676
  default: throw new Error(`Unsupported platform for portable Python: ${platform}`);
403
677
  }
404
678
  }
679
+ function getPythonDownloadUrl(platform, arch) {
680
+ return `${PP_BASE}/python-headless-${PYTHON_VERSION}-${pythonTarget(platform, arch)}.zip`;
681
+ }
682
+ /** The SHA-256 the archive for this platform/arch must have. */
683
+ function getPythonSha256(platform, arch) {
684
+ const sha = PYTHON_SHA256[pythonTarget(platform, arch)];
685
+ if (sha === void 0) throw new Error(`No pinned checksum for Python on ${platform}-${arch}`);
686
+ return sha;
687
+ }
688
+ /** The interpreter's path relative to the install root. */
689
+ function pythonInterpreterSegments(platform) {
690
+ return platform === "win32" ? ["bin", "python.exe"] : ["bin", "python3"];
691
+ }
405
692
  /**
406
693
  * Ensure the embedded portable Python is available.
407
694
  *
@@ -416,7 +703,7 @@ function getPythonDownloadUrl(platform, arch) {
416
703
  */
417
704
  async function ensurePython(dataDir, logger) {
418
705
  const pythonDir = join(dataDir, "deps", "python");
419
- const pythonBin = join(pythonDir, "bin", "python3");
706
+ const pythonBin = join(pythonDir, ...pythonInterpreterSegments(process.platform));
420
707
  if (existsSync(pythonBin)) {
421
708
  logger.debug("Portable Python found", { meta: { pythonBin } });
422
709
  return pythonBin;
@@ -433,27 +720,12 @@ async function ensurePython(dataDir, logger) {
433
720
  return null;
434
721
  }
435
722
  await pipeline(Readable.fromWeb(response.body), createWriteStream(tmpArchive));
436
- try {
437
- execFileSync("unzip", [
438
- "-o",
439
- "-q",
440
- tmpArchive,
441
- "-d",
442
- depsDir
443
- ], { stdio: "pipe" });
444
- } catch {
445
- execFileSync("python3", [
446
- "-m",
447
- "zipfile",
448
- "-e",
449
- tmpArchive,
450
- depsDir
451
- ], { stdio: "pipe" });
452
- }
723
+ verifySha256(tmpArchive, getPythonSha256(process.platform, process.arch), "python", url);
724
+ extractArchive(tmpArchive, "zip", depsDir);
453
725
  const { unlinkSync } = await import("node:fs");
454
726
  unlinkSync(tmpArchive);
455
727
  if (!existsSync(pythonBin)) {
456
- const candidates = readdirSync(depsDir).filter((name) => name.startsWith("python-") && existsSync(join(depsDir, name, "bin", "python3")));
728
+ const candidates = readdirSync(depsDir).filter((name) => name.startsWith("python-") && existsSync(join(depsDir, name, ...pythonInterpreterSegments(process.platform))));
457
729
  if (candidates.length === 0) {
458
730
  logger.warn("Python extraction succeeded but no python3 binary found in any subdir", { meta: { depsDir } });
459
731
  return null;
@@ -1975,4 +2247,4 @@ function resolveExportFingerprint(input) {
1975
2247
  return input.persisted ?? input.fresh;
1976
2248
  }
1977
2249
  //#endregion
1978
- export { ChildCostRegistry, FFMPEG_RELEASE, FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, NO_COST_CLAIM, NO_PINNED_FFMPEG_DETAIL, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, containsOrEquals, customizationBlockName, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegArtifact, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, installPythonPackages, installPythonRequirements, nodeProcStatReader, parseFfmpegCapabilities, parseProcCpuSeconds, parseProcRssBytes, physicalRootOf, pinnedFfmpegSpawner, probeFfmpegBinary, readProcessCost, resolveExportFingerprint, resolvePinnedFfmpegSpawner, signExpiringUrl, verifyExpiringUrl, writeFileAtomic };
2250
+ export { ChildCostRegistry, FFMPEG_RELEASE, FfmpegProcess, FilesystemStorageProvider, Fmp4FragmentChild, Fmp4FragmentPlane, NO_COST_CLAIM, NO_PINNED_FFMPEG_DETAIL, OwnerOnlyError, PYTHON_VERSION, buildBinaryPath, canonicalDeviceFingerprint, canonicalHash, containsOrEquals, customizationBlockName, diffExportTargets, downloadBinary, ensureBinary, ensureFfmpeg, ensurePython, findInPath, getFfmpegArtifact, getFfmpegDownloadUrl, getPlatformInfo, getPythonDownloadUrl, icaclsArgs, installPythonPackages, installPythonRequirements, mkdirOwnerOnlySync, nodeProcStatReader, parseFfmpegCapabilities, parseProcCpuSeconds, parseProcRssBytes, physicalRootOf, pinnedFfmpegSpawner, probeFfmpegBinary, readProcessCost, resolveExportFingerprint, resolvePinnedFfmpegSpawner, restrictDirToOwnerSync, restrictFileToOwnerSync, signExpiringUrl, verifyExpiringUrl, writeFileAtomic, writeOwnerOnlyFileSync };
@@ -3821,6 +3821,23 @@ function emitDownForOwnedCaps(registry, owned) {
3821
3821
  for (const { capName, scope } of owned) registry.emitDown(capName, scope);
3822
3822
  }
3823
3823
  //#endregion
3824
+ //#region src/types/model-formats.ts
3825
+ /**
3826
+ * The model weight formats a catalog entry can ship. Schema-free on purpose:
3827
+ * `types/models.ts` builds Zod schemas from it, but a runner-closure consumer
3828
+ * (the model downloader in `@camstack/system`) needs only the list and must
3829
+ * reach it through `@camstack/types/addon` without building the cap schema
3830
+ * graph (D28).
3831
+ */
3832
+ var MODEL_FORMATS = [
3833
+ "onnx",
3834
+ "coreml",
3835
+ "openvino",
3836
+ "tflite",
3837
+ "pt",
3838
+ "gguf"
3839
+ ];
3840
+ //#endregion
3824
3841
  //#region src/cap-call-context.ts
3825
3842
  /**
3826
3843
  * Per-call node pinning for `ctx.api` capability calls.
@@ -4337,6 +4354,65 @@ var deviceOpsCapability = {
4337
4354
  }
4338
4355
  };
4339
4356
  //#endregion
4357
+ //#region src/constants.ts
4358
+ var HF_REPO = "camstack/camstack-models";
4359
+ var HF_BASE_URL = `https://huggingface.co/${HF_REPO}/resolve/main`;
4360
+ /**
4361
+ * The ONE default session-JWT lifetime.
4362
+ *
4363
+ * There used to be three, and they disagreed: the `system-config` schema said
4364
+ * `30d`, `RUNTIME_DEFAULTS['auth.tokenExpiry']` said `7d`, and
4365
+ * `AuthManager.signToken` hardcoded `30d` as its own fallback. Because the
4366
+ * production signer (`AuthService extends AuthManager`, wired to
4367
+ * `ConfigManager`) resolves through `RUNTIME_DEFAULTS` BEFORE it can ever
4368
+ * reach the hardcoded fallback, the value actually in force was `7d` while
4369
+ * every surface an operator could read advertised `30d`.
4370
+ *
4371
+ * `30d` is the value the product documents and the one an operator decided on
4372
+ * (2026-07-17: the viewer's silent refresh rotates tokens long before expiry,
4373
+ * so a long lifetime only covers devices left unopened for weeks). It is
4374
+ * declared here once and imported by the schema, by the runtime defaults and
4375
+ * by the signer's fallback — a second default that disagrees with the first is
4376
+ * the same defect in miniature.
4377
+ */
4378
+ var DEFAULT_TOKEN_EXPIRY = "30d";
4379
+ /**
4380
+ * Runtime defaults -- used by ConfigManager.get() for backward compatibility
4381
+ * until Plan B wires all runtime settings to the system_settings SQL table.
4382
+ *
4383
+ * Moved from @camstack/system/config-schema to @camstack/types so that
4384
+ * @camstack/system can reference it without a cross-package kernel import.
4385
+ */
4386
+ var RUNTIME_DEFAULTS = {
4387
+ "features.streaming": true,
4388
+ "features.notifications": true,
4389
+ "features.objectDetection": false,
4390
+ "features.remoteAccess": true,
4391
+ "features.agentCluster": false,
4392
+ "features.smartHome": true,
4393
+ "features.recordings": true,
4394
+ "features.backup": true,
4395
+ "features.repl": true,
4396
+ "retention.detectionEventsDays": 30,
4397
+ "retention.audioLevelsDays": 7,
4398
+ "logging.level": "info",
4399
+ "logging.retentionDays": 30,
4400
+ "eventBus.ringBufferSize": 1e4,
4401
+ "storage.provider": "sqlite-storage",
4402
+ "storage.locations": {
4403
+ data: "camstack-data/data",
4404
+ media: "camstack-data/media",
4405
+ recordings: "camstack-data/recordings",
4406
+ cache: "/tmp/camstack-cache",
4407
+ logs: "camstack-data/logs",
4408
+ models: "camstack-data/models"
4409
+ },
4410
+ providers: [],
4411
+ "ffmpeg.hwAccel": "auto",
4412
+ "ffmpeg.threadCount": 0,
4413
+ "auth.tokenExpiry": "30d"
4414
+ };
4415
+ //#endregion
4340
4416
  //#region src/device/zod-secret.ts
4341
4417
  /**
4342
4418
  * Secret declaration on a Zod config schema (D783).
@@ -5728,6 +5804,11 @@ var BOOT_RECOVERY_BACKOFF_MS = [
5728
5804
  90 * 6e4
5729
5805
  ];
5730
5806
  //#endregion
5807
+ //#region src/utils/hf-url.ts
5808
+ function hfModelUrl(repo, path) {
5809
+ return `https://huggingface.co/${repo}/resolve/main/${path}`;
5810
+ }
5811
+ //#endregion
5731
5812
  //#region src/utils/json-safe.ts
5732
5813
  /**
5733
5814
  * Type-safe JSON parsing helpers.
@@ -6006,6 +6087,12 @@ Object.defineProperty(exports, "DEFAULT_RUNTIME_STATE_DURABILITY", {
6006
6087
  return DEFAULT_RUNTIME_STATE_DURABILITY;
6007
6088
  }
6008
6089
  });
6090
+ Object.defineProperty(exports, "DEFAULT_TOKEN_EXPIRY", {
6091
+ enumerable: true,
6092
+ get: function() {
6093
+ return DEFAULT_TOKEN_EXPIRY;
6094
+ }
6095
+ });
6009
6096
  Object.defineProperty(exports, "DEVICE_CHILDREN_BATCH_MAX", {
6010
6097
  enumerable: true,
6011
6098
  get: function() {
@@ -6048,6 +6135,18 @@ Object.defineProperty(exports, "FrameHandleSchema", {
6048
6135
  return FrameHandleSchema;
6049
6136
  }
6050
6137
  });
6138
+ Object.defineProperty(exports, "HF_BASE_URL", {
6139
+ enumerable: true,
6140
+ get: function() {
6141
+ return HF_BASE_URL;
6142
+ }
6143
+ });
6144
+ Object.defineProperty(exports, "HF_REPO", {
6145
+ enumerable: true,
6146
+ get: function() {
6147
+ return HF_REPO;
6148
+ }
6149
+ });
6051
6150
  Object.defineProperty(exports, "KeychainWriteRefusedError", {
6052
6151
  enumerable: true,
6053
6152
  get: function() {
@@ -6060,6 +6159,12 @@ Object.defineProperty(exports, "MIN_SCRUBBED_LENGTH", {
6060
6159
  return MIN_SCRUBBED_LENGTH;
6061
6160
  }
6062
6161
  });
6162
+ Object.defineProperty(exports, "MODEL_FORMATS", {
6163
+ enumerable: true,
6164
+ get: function() {
6165
+ return MODEL_FORMATS;
6166
+ }
6167
+ });
6063
6168
  Object.defineProperty(exports, "PEER_BYTES_CHUNK_BYTES", {
6064
6169
  enumerable: true,
6065
6170
  get: function() {
@@ -6126,6 +6231,12 @@ Object.defineProperty(exports, "REVALIDATE_AFTER_MS", {
6126
6231
  return REVALIDATE_AFTER_MS;
6127
6232
  }
6128
6233
  });
6234
+ Object.defineProperty(exports, "RUNTIME_DEFAULTS", {
6235
+ enumerable: true,
6236
+ get: function() {
6237
+ return RUNTIME_DEFAULTS;
6238
+ }
6239
+ });
6129
6240
  Object.defineProperty(exports, "RawStateResultSchema", {
6130
6241
  enumerable: true,
6131
6242
  get: function() {
@@ -6432,6 +6543,12 @@ Object.defineProperty(exports, "globalSecretScope", {
6432
6543
  return globalSecretScope;
6433
6544
  }
6434
6545
  });
6546
+ Object.defineProperty(exports, "hfModelUrl", {
6547
+ enumerable: true,
6548
+ get: function() {
6549
+ return hfModelUrl;
6550
+ }
6551
+ });
6435
6552
  Object.defineProperty(exports, "hydrateSchema", {
6436
6553
  enumerable: true,
6437
6554
  get: function() {