specrails-hub 1.61.3 → 1.61.4
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/package.json +1 -1
- package/server/dist/chromium-resolver.js +55 -14
package/package.json
CHANGED
|
@@ -10,6 +10,8 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
10
10
|
const os_1 = __importDefault(require("os"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const child_process_1 = require("child_process");
|
|
13
|
+
const stream_1 = require("stream");
|
|
14
|
+
const promises_1 = require("stream/promises");
|
|
13
15
|
/**
|
|
14
16
|
* Resolve the Chromium executable the browser-capture feature should launch.
|
|
15
17
|
*
|
|
@@ -20,19 +22,26 @@ const child_process_1 = require("child_process");
|
|
|
20
22
|
* become flat duplicate copies) and invalidates its code signature — macOS
|
|
21
23
|
* notarization then rejects the app ("The signature of the binary is invalid").
|
|
22
24
|
*
|
|
23
|
-
* So we ship Chromium as a single OPAQUE
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
25
|
+
* So we ship Chromium as a single OPAQUE, OBFUSCATED blob
|
|
26
|
+
* (`<runtimes>/chromium/chromium.pak`):
|
|
27
|
+
* - It is an XOR-transformed `chromium.tar.gz`. The notarization service recursively
|
|
28
|
+
* unpacks archives it recognises (.zip → .tar.gz → .tar → …) and validates every
|
|
29
|
+
* Mach-O inside; Chromium's ~50 nested binaries are only ad-hoc ("linker") signed
|
|
30
|
+
* by Google, so a plain archive fails notarization. XOR-ing breaks the gzip/tar
|
|
31
|
+
* magic, so the notary cannot identify the file as a container and treats it as
|
|
32
|
+
* opaque data — nothing inside is inspected, and the app notarizes with NO
|
|
33
|
+
* Developer-ID signing of Chromium. (Obfuscation, not security: the key is public.)
|
|
34
|
+
* - Tauri also copies it as one regular file — nothing to dereference.
|
|
35
|
+
* - At first use we reverse the XOR and extract it to a writable cache
|
|
36
|
+
* (`~/.specrails/runtimes/chromium`), restoring the framework symlinks intact. The
|
|
37
|
+
* extracted Chromium keeps Google's ad-hoc signature, which is sufficient to execute
|
|
38
|
+
* on Apple Silicon, and — being self-extracted rather than downloaded — carries no
|
|
39
|
+
* `com.apple.quarantine` xattr, so Gatekeeper does not block it.
|
|
32
40
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
41
|
+
* A plain `chromium.tar.gz`/`chromium.tar` is also accepted (e.g. unobfuscated local
|
|
42
|
+
* builds). When no archive is present (dev, a runtimes-less build, a partial
|
|
43
|
+
* extraction) we fall back to discovering an UNPACKED `<runtimes>/chromium` tree, and
|
|
44
|
+
* finally to `null` so Playwright uses its own managed browser — never dead-ending.
|
|
36
45
|
*
|
|
37
46
|
* We DISCOVER rather than hard-code the executable path because Playwright's layout
|
|
38
47
|
* changes across versions (`chrome-mac/Chromium.app` → `chrome-mac-arm64/Google Chrome
|
|
@@ -146,7 +155,28 @@ function resolveBundledChromiumPath() {
|
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
/** Candidate archive names under `<runtimes>/chromium`, in preference order. */
|
|
149
|
-
const ARCHIVE_NAMES = ['chromium.tar.gz', 'chromium.tar'];
|
|
158
|
+
const ARCHIVE_NAMES = ['chromium.pak', 'chromium.tar.gz', 'chromium.tar'];
|
|
159
|
+
// XOR key for the obfuscated `chromium.pak` blob. Keep byte-identical to KEY in
|
|
160
|
+
// scripts/obfuscate-chromium.mjs — the round-trip is covered by a unit test.
|
|
161
|
+
const OBFUSCATION_KEY = Buffer.from('specrails-hub-chromium-pack-v1', 'utf8');
|
|
162
|
+
/** Streaming XOR transform (symmetric: packs and unpacks). */
|
|
163
|
+
function xorStream() {
|
|
164
|
+
let offset = 0;
|
|
165
|
+
return new stream_1.Transform({
|
|
166
|
+
transform(chunk, _enc, cb) {
|
|
167
|
+
const out = Buffer.allocUnsafe(chunk.length);
|
|
168
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
169
|
+
out[i] = chunk[i] ^ OBFUSCATION_KEY[(offset + i) % OBFUSCATION_KEY.length];
|
|
170
|
+
}
|
|
171
|
+
offset += chunk.length;
|
|
172
|
+
cb(null, out);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** De-obfuscate a `.pak` blob into a real `.tar.gz` at `outPath`. */
|
|
177
|
+
async function deobfuscate(pakPath, outPath) {
|
|
178
|
+
await (0, promises_1.pipeline)(fs_1.default.createReadStream(pakPath), xorStream(), fs_1.default.createWriteStream(outPath));
|
|
179
|
+
}
|
|
150
180
|
/** Writable extraction destination (overridable for tests via env). */
|
|
151
181
|
function chromiumCacheDir() {
|
|
152
182
|
return (process.env.SPECRAILS_CHROMIUM_CACHE_DIR ||
|
|
@@ -206,8 +236,17 @@ async function extractAndDiscover(archivePath, identity) {
|
|
|
206
236
|
const tmpDir = `${destRoot}.tmp-${process.pid}`;
|
|
207
237
|
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
208
238
|
fs_1.default.mkdirSync(tmpDir, { recursive: true });
|
|
239
|
+
// An obfuscated `.pak` is XOR-decoded to a real `.tar.gz` first; plain archives
|
|
240
|
+
// are fed straight to tar.
|
|
241
|
+
const isPak = archivePath.endsWith('.pak');
|
|
242
|
+
const decodedTar = isPak ? `${tmpDir}.tar.gz` : null;
|
|
209
243
|
try {
|
|
210
|
-
|
|
244
|
+
let tarSource = archivePath;
|
|
245
|
+
if (decodedTar) {
|
|
246
|
+
await deobfuscate(archivePath, decodedTar);
|
|
247
|
+
tarSource = decodedTar;
|
|
248
|
+
}
|
|
249
|
+
await runTarExtract(tarSource, tmpDir);
|
|
211
250
|
const exeInTmp = discoverChromiumExecutable(tmpDir);
|
|
212
251
|
if (!exeInTmp)
|
|
213
252
|
throw new Error('no chromium executable found after extraction');
|
|
@@ -223,6 +262,8 @@ async function extractAndDiscover(archivePath, identity) {
|
|
|
223
262
|
}
|
|
224
263
|
finally {
|
|
225
264
|
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
265
|
+
if (decodedTar)
|
|
266
|
+
fs_1.default.rmSync(decodedTar, { force: true });
|
|
226
267
|
}
|
|
227
268
|
}
|
|
228
269
|
/**
|