@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.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-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
693
|
+
return `linux-${a}`;
|
|
423
694
|
}
|
|
424
|
-
case "darwin": return
|
|
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,
|
|
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
|
-
|
|
460
|
-
|
|
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,
|
|
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;
|