browser-cookie-bridge 1.5.1 → 1.5.2

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/README.md CHANGED
@@ -48,13 +48,13 @@
48
48
 
49
49
  ---
50
50
 
51
- > **Version 1.5.1 restores the [Product Hunt](https://www.producthunt.com/products/browser-cookie-bridge-2) launch link.** Version 1.5.0 added experimental Cursor browser import and encrypted Grok Bot `.bcbx` export. Cursor is destination-only and currently imports cookie sessions into its dedicated browser partition. Grok Bot writes a one-time encrypted transfer file with a bundled importer and a one-time key: attach the `.bcbx` file to any Grok Bot, paste the prompt, and enter the key privately when asked. Cookie values must never be logged or pasted into chat. The optional Codex restart flow from 1.4.0 is unchanged and remains off by default.
51
+ > **Version 1.5.2:** Grok Bot `.bcbx` bundles embed the decryption key, so import needs no passphrase—treat the file as credentials because anyone with it can decrypt the cookies. Version 1.5.1 restored the [Product Hunt](https://www.producthunt.com/products/browser-cookie-bridge-2) launch link. Version 1.5.0 added experimental Cursor browser import and encrypted Grok Bot `.bcbx` export. Cursor is destination-only and currently imports cookie sessions into its dedicated browser partition. Attach the `.bcbx` file to any Grok Bot and paste the prompt; the bundled importer decrypts automatically. Cookie values must never be logged or pasted into chat. The optional Codex restart flow from 1.4.0 is unchanged and remains off by default.
52
52
 
53
53
  ## Why Browser Cookie Bridge
54
54
 
55
55
  Signing into the same sites across several browsers is repetitive. Export files are awkward, password managers do not move active sessions, and embedded app browsers do not always offer an import button.
56
56
 
57
- Browser Cookie Bridge gives those browser profiles a small, native control panel. Local browser, Codex, and Cursor transfers stay on the Mac. Grok Bot uses a one-time encrypted transfer file that you attach yourself. If you explicitly select Browserless Cloud and click Upload, the app can instead send authenticated profile state to your own Browserless account.
57
+ Browser Cookie Bridge gives those browser profiles a small, native control panel. Local browser, Codex, and Cursor transfers stay on the Mac. Grok Bot uses an encrypted transfer file with an embedded decryption key that you attach yourself. If you explicitly select Browserless Cloud and click Upload, the app can instead send authenticated profile state to your own Browserless account.
58
58
 
59
59
  ## Windows waitlist
60
60
 
@@ -66,7 +66,7 @@ Browser Cookie Bridge currently ships for macOS. A separate Windows app is being
66
66
  - 🗃️ **Optional full site data for Codex** — replace Codex's compatible Local Storage, IndexedDB, Session Storage, service-worker, and related origin stores from a closed source profile, with backup and rollback. Off by default.
67
67
  - 🌐 **Seven Chromium browsers** — Brave, Chrome, Edge, Arc, Vivaldi, Opera, and Perplexity Comet can be sources or destinations.
68
68
  - ✨ **ChatGPT Codex and experimental Cursor import** — merge cookie sessions into either app's built-in browser; Codex also supports optional history and full site data. Both are destination-only. Cursor import is experimental: it writes only to Cursor's dedicated `Partitions/cursor-browser` profile and refuses unknown schemas.
69
- - 🤖 **Encrypted Grok Bot transfer (`.bcbx`)** — export cookie sessions into a local encrypted bundle with a one-time key and bundled importer. The destination tile uses the official Grok Bot app icon. Attach the file to any Grok Bot; the importer runs on the cloud computer only. Manual only.
69
+ - 🤖 **Encrypted Grok Bot transfer (`.bcbx`)** — export cookie sessions into a local encrypted bundle with an embedded decryption key and bundled importer. The destination tile uses the official Grok Bot app icon. Attach the file to any Grok Bot; the importer runs on the cloud computer only. Manual only.
70
70
  - 🔁 **Optional Codex restart** — manual Sync can force quit a running Codex instance and reopen it only after a successful local transfer; off by default.
71
71
  - 🔄 **Optional two-app restart** — Full site data can force quit the selected source browser and Codex, then reopen only the apps that were running after a successful transfer; off by default.
72
72
  - ☁️ **Optional Browserless upload** — create or refresh a Browserless authenticated profile with cookies, local storage, and IndexedDB; see a local size preflight, live progress, cancellation, and post-upload verification.
@@ -164,21 +164,21 @@ An open destination app blocks the transfer. The optional **Restart Codex automa
164
164
 
165
165
  No browser extension is needed for this path.
166
166
 
167
- Grok Bot cannot receive a local database merge. The app writes a one-time encrypted transfer file that you attach to any Grok Bot.
167
+ Grok Bot cannot receive a local database merge. The app writes an encrypted transfer file that you attach to any Grok Bot.
168
168
 
169
169
  1. Select a source browser and **Grok Bot** as the destination. The tile uses the official Grok Bot app icon.
170
170
  2. Keep **Cookies** on. History URLs and Full site data are excluded from Grok Bot transfer files.
171
171
  3. Optionally limit the export with **Only these domains**. Leave it blank to include every readable cookie.
172
172
  4. Press **Create transfer file** and choose where to save `GrokBot-Import.bcbx`.
173
- 5. The result sheet shows the one-time decryption key and the prompt to paste. Use **Reveal file** or **Copy prompt**. Keep the key off chat logs; do not paste cookie values into chat.
174
- 6. Attach the `.bcbx` file to any Grok Bot, paste the prompt, then enter the one-time key privately through Agent Computer when asked.
173
+ 5. The result sheet shows a sensitivity warning and the prompt to paste. Use **Reveal file** or **Copy prompt**. Do not paste cookie values into chat.
174
+ 6. Attach the `.bcbx` file to any Grok Bot and paste the prompt. The bundled importer decrypts automatically—no separate key entry.
175
175
 
176
- The bundle is a zip-compatible `.bcbx` file containing `manifest.json`, encrypted `payload.enc`, `import.mjs`, and `PROMPT.txt`. Cookies are encrypted with AES-256-GCM using a scrypt-derived key. The file is written with user-only permissions (`0600`). Daily sync and Sync at login never create Grok Bot bundles.
176
+ The bundle is a zip-compatible `.bcbx` file containing `manifest.json`, encrypted `payload.enc`, `decryption.key`, `import.mjs`, and `PROMPT.txt`. Cookies are encrypted with AES-256-GCM using a scrypt-derived key; the same key is stored in `decryption.key` inside the bundle so the importer can decrypt without user input. Treat the file as credentials—anyone with the bundle can read the cookies. The file is written with user-only permissions (`0600`). Daily sync and Sync at login never create Grok Bot bundles.
177
177
 
178
178
  On the Grok Bot cloud computer the bundled importer:
179
179
 
180
180
  1. Expects you to unzip `GrokBot-Import.bcbx` and run `node import.mjs`.
181
- 2. Prompts for the one-time decryption key. Do not print cookie names or values.
181
+ 2. Reads the embedded decryption key and decrypts the payload automatically. Do not print cookie names or values.
182
182
  3. Injects cookies into the cloud browser and reports only how many cookies were imported.
183
183
  4. Deletes the unzipped files and the bundle copy.
184
184
 
@@ -243,7 +243,7 @@ browser-cookie-bridge disable-app-login
243
243
  browser-cookie-bridge remove-schedule
244
244
  ```
245
245
 
246
- Supported source IDs are `brave`, `chrome`, `edge`, `arc`, `vivaldi`, `opera`, and `comet`. Target IDs are the same plus `codex`, `cursor`, `browserless`, and `grok-bot`. The same browser cannot be both endpoints. Cursor import currently supports cookies only. Grok Bot export requires `--output /path/GrokBot-Import.bcbx` and writes cookies only; `--grok-bot-domains` optionally limits the bundle. The CLI prints a one-time decryption key after exportkeep it private and never paste cookie values into chat. Browserless requires `BROWSERLESS_TOKEN` and the explicit `--allow-cloud-upload` flag; the native app supplies the token from Keychain without placing it in the OS command line or app configuration.
246
+ Supported source IDs are `brave`, `chrome`, `edge`, `arc`, `vivaldi`, `opera`, and `comet`. Target IDs are the same plus `codex`, `cursor`, `browserless`, and `grok-bot`. The same browser cannot be both endpoints. Cursor import currently supports cookies only. Grok Bot export requires `--output /path/GrokBot-Import.bcbx` and writes cookies only; `--grok-bot-domains` optionally limits the bundle. The bundle includes an embedded decryption key—treat it as credentials and never paste cookie values into chat. Browserless requires `BROWSERLESS_TOKEN` and the explicit `--allow-cloud-upload` flag; the native app supplies the token from Keychain without placing it in the OS command line or app configuration.
247
247
 
248
248
  ## How it works
249
249
 
@@ -251,16 +251,16 @@ Supported source IDs are `brave`, `chrome`, `edge`, `arc`, `vivaldi`, `opera`, a
251
251
  |---|---|
252
252
  | **Browser → browser** | Unpacked extensions connect to a short-lived broker on IPv4 loopback. Selected data stays in memory and is never written to logs. |
253
253
  | **Browser → Codex / Cursor** | The app reads the selected local Chromium profile, creates a consistent destination SQLite backup, merges cookies into a working copy, validates it, then replaces only the embedded browser's cookie database atomically. Codex also supports optional history and full site data. Cursor import is experimental and limited to the dedicated browser-partition cookie store. |
254
- | **Browser → Grok Bot** | The app reads the selected local Chromium profile, encrypts cookie sessions into a `.bcbx` bundle with a one-time key, and writes that file locally. You attach it to a Grok Bot; the bundled importer decrypts and injects cookies on the cloud computer only. |
254
+ | **Browser → Grok Bot** | The app reads the selected local Chromium profile, encrypts cookie sessions into a `.bcbx` bundle with an embedded decryption key, and writes that file locally. You attach it to a Grok Bot; the bundled importer decrypts and injects cookies on the cloud computer only. |
255
255
  | **Browser → Browserless** | The bundled official Browserless CLI copies a closed local profile, captures cookies/local storage/IndexedDB, and uploads it directly to the selected Browserless region. |
256
256
 
257
257
  The broker validates a random token and extension origin, limits payload size, and normally exits after five minutes. Only the endpoints selected in the app respond to a transfer.
258
258
 
259
259
  ## Security & privacy
260
260
 
261
- - Cookie values and history URLs are never logged. Never paste cookie values or Grok Bot decryption keys into chat.
261
+ - Cookie values and history URLs are never logged. Never paste cookie values into chat.
262
262
  - Browser-to-browser data is held only in broker memory.
263
- - Grok Bot `.bcbx` files are encrypted locally with AES-256-GCM. Treat the bundle and its one-time key as credentials. Enter the key privately through Agent Computer when the importer asks; do not ask Grok Bot to print cookie names or values. The bundled importer reports only import counts, then deletes the unzipped files and the bundle copy.
263
+ - Grok Bot `.bcbx` files encrypt cookie payloads with AES-256-GCM but include the decryption key inside the bundle (`decryption.key`). Treat the entire file as credentials—anyone with the bundle can read the cookies. Do not share it or paste cookie values into chat. The bundled importer decrypts automatically, reports only import counts, then deletes the unzipped files and the bundle copy. Legacy v1 bundles without an embedded key still prompt for a separate passphrase.
264
264
  - Direct-import backups are stored with user-only permissions under `~/Library/Application Support/BraveCodexCookieSync/backups/codex` or `backups/cursor`; the newest 14 per destination are retained.
265
265
  - Full site-data import is opt-in for Codex and requires both Codex and the source browser to be closed. It replaces compatible origin-storage directories rather than attempting an unsafe LevelDB merge; the previous directories are included in the same backup.
266
266
  - The app refuses unknown destination database schemas instead of guessing and writes only to Cursor's dedicated `Partitions/cursor-browser` profile, never Cursor's main application cookie store.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Browser Cookie Bridge",
4
- "version": "1.5.1",
4
+ "version": "1.5.2",
5
5
  "description": "Transfers selected browser data locally between supported browsers and embedded app browsers.",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5o6lDkXzU/23Jm2QRk4mfuxNnr3VlEIjRHcZr93Gdo7PI7asHJQhigaMGT6UOz0lkaTn1F+TGIPlsJo9GNBSsxbCfxWoBSXcFQFrPolY5LEfKaMtZQQ6eb4UAnp+bbUwb1den4On0piWU47Yzl6i0Jcdg4EG5H1kCSF/6nJsbKgF7OTaR/3drULu40yRvflzSXRhly3UDO7c5mgHJsaMwVV+VILsxbcPOoRINjuo3zDC737HFs67dpf38xF3tjSVL0HURAWKKobMvfABiDfVEm5O0Lvf+57d1Ib12QLT2qtVXNDy3iYwX+w2S0wH2vIMsfvoFPlRdmeqft70kZOqXwIDAQAB",
7
7
  "permissions": ["alarms", "cookies", "history"],
@@ -10,8 +10,8 @@
10
10
  <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
11
11
  <key>CFBundleName</key><string>Browser Cookie Bridge</string>
12
12
  <key>CFBundlePackageType</key><string>APPL</string>
13
- <key>CFBundleShortVersionString</key><string>1.5.1</string>
14
- <key>CFBundleVersion</key><string>37</string>
13
+ <key>CFBundleShortVersionString</key><string>1.5.2</string>
14
+ <key>CFBundleVersion</key><string>39</string>
15
15
  <key>LSMinimumSystemVersion</key><string>13.5</string>
16
16
  <key>NSHighResolutionCapable</key><true/>
17
17
  </dict>
@@ -949,7 +949,7 @@ struct ExtensionSetupSheet: View {
949
949
  Text(model.isDirectTarget
950
950
  ? "\(model.targetName) uses a direct local merge, so no extension is required. Quit \(model.targetName) before syncing. Password access is never requested."
951
951
  : model.isGrokBotTarget
952
- ? "Grok Bot uses a one-time encrypted transfer file, so no extension is required. Create the .bcbx bundle, attach it to any Grok Bot, and paste the prompt."
952
+ ? "Grok Bot uses an encrypted transfer file with an embedded decryption key, so no extension is required. Create the .bcbx bundle, attach it to any Grok Bot, and paste the prompt."
953
953
  : "In both endpoints, enable Developer mode and choose Load unpacked. Password access is never requested.")
954
954
  .font(.system(size: 10.5))
955
955
  .foregroundStyle(.secondary)
@@ -1188,12 +1188,13 @@ struct GrokBotResultSheet: View {
1188
1188
  }
1189
1189
 
1190
1190
  VStack(alignment: .leading, spacing: 8) {
1191
- Text("One-time decryption key")
1191
+ Text("Sensitive transfer file")
1192
1192
  .font(.system(size: 11, weight: .semibold))
1193
1193
  .foregroundStyle(.secondary)
1194
- Text(model.grokBotPassphrase)
1195
- .font(.system(size: 15, weight: .semibold, design: .monospaced))
1196
- .textSelection(.enabled)
1194
+ Text("The .bcbx bundle includes the decryption key. Anyone with the file can read your cookies—treat it like a password and do not share it.")
1195
+ .font(.system(size: 11))
1196
+ .foregroundStyle(.secondary)
1197
+ .fixedSize(horizontal: false, vertical: true)
1197
1198
  .padding(10)
1198
1199
  .frame(maxWidth: .infinity, alignment: .leading)
1199
1200
  .background(Color.primary.opacity(0.04), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
@@ -100,7 +100,6 @@ final class SyncModel: ObservableObject {
100
100
  @Published var grokBotOnlyDomains = ""
101
101
  @Published var showingBrowserlessSetup = false
102
102
  @Published var showingGrokBotResult = false
103
- @Published var grokBotPassphrase = ""
104
103
  @Published var grokBotPrompt = ""
105
104
  @Published var grokBotOutputPath = ""
106
105
  @Published var browserlessAssessment: BrowserlessProfileAssessment?
@@ -754,7 +753,6 @@ final class SyncModel: ObservableObject {
754
753
  } else if success {
755
754
  self.state = partial ? .warning : .success
756
755
  if self.isGrokBotTarget, let result = self.parseGrokBotResult(from: output) {
757
- self.grokBotPassphrase = result.passphrase
758
756
  self.grokBotPrompt = result.prompt
759
757
  self.grokBotOutputPath = result.outputPath
760
758
  self.showingGrokBotResult = true
@@ -1167,7 +1165,6 @@ final class SyncModel: ObservableObject {
1167
1165
 
1168
1166
  private struct GrokBotResultPayload: Decodable {
1169
1167
  let outputPath: String
1170
- let passphrase: String
1171
1168
  let prompt: String
1172
1169
  }
1173
1170
 
@@ -1230,7 +1227,7 @@ final class SyncModel: ObservableObject {
1230
1227
  } else {
1231
1228
  state = .ready
1232
1229
  primaryStatus = "Ready to create a Grok Bot transfer file"
1233
- secondaryStatus = "Creates an encrypted .bcbx bundle with a one-time key and bundled importer"
1230
+ secondaryStatus = "Creates an encrypted .bcbx bundle with an embedded decryption key and bundled importer"
1234
1231
  }
1235
1232
  } else if isBrowserlessTarget {
1236
1233
  if selectedSourceID == "comet" {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-cookie-bridge",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "Local-first cookie and session transfer for macOS with optional Browserless upload",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -312,7 +312,6 @@ async function sync(args, { signal } = {}) {
312
312
  console.log(grokBotSummary(result));
313
313
  console.log(`BCB_GROK_RESULT ${JSON.stringify({
314
314
  outputPath: result.outputPath,
315
- passphrase: result.passphrase,
316
315
  cookieCount: result.cookieCount,
317
316
  domainCount: result.domainCount,
318
317
  domains: result.domains,
@@ -7,13 +7,15 @@ import { createZipArchive, readZipArchive } from "./zip-store.js";
7
7
 
8
8
  export const BUNDLE_EXTENSION = "bcbx";
9
9
  export const DEFAULT_BUNDLE_NAME = "GrokBot-Import.bcbx";
10
+ export const BUNDLE_FORMAT_VERSION = 2;
11
+ export const EMBEDDED_KEY_FILENAME = "decryption.key";
12
+
10
13
  export const GROK_BOT_PROMPT = `On your Grok Bot cloud computer only — do not access my local Mac and do not print cookie values.
11
14
 
12
15
  1. Save the attached GrokBot-Import.bcbx to the cloud computer.
13
16
  2. Unzip it: unzip -o GrokBot-Import.bcbx -d bcb-import && cd bcb-import
14
17
  3. Run: node import.mjs
15
- 4. When prompted, ask me to take over and type the one-time decryption key privately through Agent Computer.
16
- 5. Report only how many cookies were imported per domain, then delete the bcb-import folder and any copies of the bundle.`;
18
+ 4. Report only how many cookies were imported per domain, then delete the bcb-import folder and any copies of the bundle.`;
17
19
 
18
20
  const KDF = { name: "scrypt", N: 16384, r: 8, p: 1, keyLength: 32 };
19
21
  const SAME_SITE = {
@@ -50,7 +52,9 @@ export function buildGrokBotBundle({
50
52
  const domains = uniqueDomains(exportCookies);
51
53
  const manifest = {
52
54
  format: "browser-cookie-bridge-grok-bot",
53
- version: 1,
55
+ version: BUNDLE_FORMAT_VERSION,
56
+ keyDelivery: "embedded",
57
+ keyFile: EMBEDDED_KEY_FILENAME,
54
58
  createdAt: now.toISOString(),
55
59
  sourceBrowser,
56
60
  cookieCount: exportCookies.length,
@@ -66,6 +70,7 @@ export function buildGrokBotBundle({
66
70
  const archive = createZipArchive([
67
71
  { name: "manifest.json", data: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8") },
68
72
  { name: "payload.enc", data: payloadBody },
73
+ { name: EMBEDDED_KEY_FILENAME, data: Buffer.from(`${passphrase}\n`, "utf8") },
69
74
  { name: "import.mjs", data: Buffer.from(importerSource, "utf8") },
70
75
  { name: "PROMPT.txt", data: Buffer.from(`${GROK_BOT_PROMPT}\n`, "utf8") },
71
76
  ]);
@@ -138,6 +143,7 @@ export function parseGrokBotBundle(buffer) {
138
143
  return {
139
144
  manifest,
140
145
  payload: entries.get("payload.enc"),
146
+ embeddedKey: entries.get(EMBEDDED_KEY_FILENAME)?.toString("utf8").trim() || "",
141
147
  importer: entries.get("import.mjs")?.toString("utf8") || "",
142
148
  prompt: entries.get("PROMPT.txt")?.toString("utf8") || "",
143
149
  };
@@ -148,7 +154,7 @@ export function grokBotSummary(result) {
148
154
  const skipped = result.sourceCookieSkipped
149
155
  ? ` ${result.sourceCookieSkipped} source cookie${result.sourceCookieSkipped === 1 ? " was" : "s were"} unreadable or unsupported.`
150
156
  : "";
151
- return `Grok Bot transfer file created: ${result.cookieCount} cookies across ${domainNote} from ${result.sourceBrowser}.${skipped} Attach ${path.basename(result.outputPath)} to any Grok Bot, paste the prompt, then enter the one-time key privately when asked.`;
157
+ return `Grok Bot transfer file created: ${result.cookieCount} cookies across ${domainNote} from ${result.sourceBrowser}.${skipped} Attach ${path.basename(result.outputPath)} to any Grok Bot and paste the prompt. The bundle includes the decryption key—treat the file as credentials and do not share it.`;
152
158
  }
153
159
 
154
160
  export function filterCookies(cookies, onlyDomains = []) {
@@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url";
11
11
  import { stdin as input, stdout as output } from "node:process";
12
12
 
13
13
  const CDP_PORTS = [9222, 9223, 9224, 9228, 9229, 9400];
14
+ const EMBEDDED_KEY_FILENAME = "decryption.key";
14
15
  const SAME_SITE = {
15
16
  unspecified: "Lax",
16
17
  no_restriction: "None",
@@ -29,7 +30,7 @@ export async function main(argv = process.argv.slice(2)) {
29
30
 
30
31
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
31
32
  validateManifest(manifest);
32
- const passphrase = await readPassphrase();
33
+ const passphrase = await resolveDecryptionKey({ manifest, bundleDir });
33
34
  const cookies = decryptPayload({
34
35
  manifest,
35
36
  encrypted: fs.readFileSync(payloadPath),
@@ -42,9 +43,12 @@ export async function main(argv = process.argv.slice(2)) {
42
43
  }
43
44
 
44
45
  export function validateManifest(manifest) {
45
- if (manifest?.format !== "browser-cookie-bridge-grok-bot" || manifest?.version !== 1) {
46
+ if (manifest?.format !== "browser-cookie-bridge-grok-bot") {
46
47
  throw new Error("Unsupported Browser Cookie Bridge Grok Bot bundle.");
47
48
  }
49
+ if (manifest?.version !== 1 && manifest?.version !== 2) {
50
+ throw new Error("Unsupported Browser Cookie Bridge Grok Bot bundle version.");
51
+ }
48
52
  for (const key of ["salt", "iv", "authTag"]) {
49
53
  if (typeof manifest[key] !== "string" || !manifest[key]) {
50
54
  throw new Error(`Bundle manifest is missing ${key}.`);
@@ -52,6 +56,26 @@ export function validateManifest(manifest) {
52
56
  }
53
57
  }
54
58
 
59
+ export async function resolveDecryptionKey({ manifest, bundleDir }) {
60
+ if (manifest.version === 2) {
61
+ if (manifest.keyDelivery !== "embedded") {
62
+ throw new Error("Unsupported Grok Bot bundle key delivery method.");
63
+ }
64
+ const keyFile = typeof manifest.keyFile === "string" && manifest.keyFile
65
+ ? manifest.keyFile
66
+ : EMBEDDED_KEY_FILENAME;
67
+ const keyPath = path.join(bundleDir, keyFile);
68
+ if (!fs.existsSync(keyPath)) {
69
+ throw new Error(`Embedded decryption key file is missing: ${keyFile}`);
70
+ }
71
+ const key = fs.readFileSync(keyPath, "utf8").trim();
72
+ if (!key) throw new Error("Embedded decryption key file is empty.");
73
+ return key;
74
+ }
75
+
76
+ return readPassphrase();
77
+ }
78
+
55
79
  export function decryptPayload({ manifest, encrypted, passphrase }) {
56
80
  const key = crypto.scryptSync(
57
81
  passphrase,
@@ -164,7 +188,7 @@ function reportSummary(result, domains) {
164
188
  }
165
189
 
166
190
  function cleanup(bundleDir, bundlePath) {
167
- for (const name of ["manifest.json", "payload.enc", "import.mjs", "PROMPT.txt"]) {
191
+ for (const name of ["manifest.json", "payload.enc", EMBEDDED_KEY_FILENAME, "import.mjs", "PROMPT.txt"]) {
168
192
  fs.rmSync(path.join(bundleDir, name), { force: true });
169
193
  }
170
194
  fs.rmSync(bundlePath, { force: true });