@camstack/types 1.2.354 → 1.2.356
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/addon.d.ts +7 -1
- package/dist/addon.js +1685 -1
- package/dist/addon.mjs +1679 -2
- package/dist/{composition-Bzr7GwgP.mjs → composition-C-WpVYTX.mjs} +22 -0
- package/dist/{composition-BLWAivB2.js → composition-DCYnv4I6.js} +22 -0
- package/dist/deps/archive-extract.d.ts +11 -0
- package/dist/deps/binary-downloader.d.ts +9 -0
- package/dist/deps/python-downloader.d.ts +4 -0
- package/dist/deps/zip-extract.d.ts +1 -0
- package/dist/generated/addon-api.d.ts +22 -0
- package/dist/generated/addon-cap-facts.d.ts +20 -0
- package/dist/index.js +17 -164
- package/dist/index.mjs +4 -151
- package/dist/node.d.ts +2 -0
- package/dist/node.js +334 -56
- package/dist/node.mjs +330 -58
- package/dist/{sleep-SgkKj2bk.js → sleep-BFVY4_xY.js} +198 -0
- package/dist/{sleep-Tkp9dHXv.mjs → sleep-vWTnHq9G.mjs} +157 -1
- package/dist/types/model-formats.d.ts +9 -0
- package/dist/types/models.d.ts +4 -2
- package/dist/util/owner-only-file.d.ts +28 -0
- package/package.json +1 -1
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-
|
|
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
|
|
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
|
-
|
|
248
|
+
verifySha256(tmpArchive, opts.sha256, name, url);
|
|
95
249
|
const tmpExtractDir = join(targetDir, `${name}-extract`);
|
|
96
250
|
mkdirSync(tmpExtractDir, { recursive: true });
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
670
|
+
return `linux-${a}`;
|
|
400
671
|
}
|
|
401
|
-
case "darwin": return
|
|
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,
|
|
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
|
-
|
|
437
|
-
|
|
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,
|
|
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 };
|