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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-hub",
3
- "version": "1.61.3",
3
+ "version": "1.61.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 archive (`<runtimes>/chromium/chromium.tar.gz`):
24
- * - Tauri copies one regular file — nothing to dereference, and notarization treats
25
- * archive bytes as opaque data (it never inspects Mach-O inside a .tar.gz), so the
26
- * app notarizes cleanly with NO Developer-ID signing of Chromium required.
27
- * - At first use we extract it to a writable cache (`~/.specrails/runtimes/chromium`),
28
- * which restores the framework symlinks intact. The extracted Chromium keeps
29
- * Google's original ad-hoc ("linker-signed") signature, which is sufficient to
30
- * execute on Apple Silicon, and being self-extracted rather than downloaded —
31
- * carries no `com.apple.quarantine` xattr, so Gatekeeper does not block it.
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
- * When no archive is present (dev, a runtimes-less build, a partial extraction) we
34
- * fall back to discovering an UNPACKED `<runtimes>/chromium` tree, and finally to
35
- * `null` so Playwright uses its own managed browser never dead-ending the feature.
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
- await runTarExtract(archivePath, tmpDir);
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
  /**