brew-tui 1.2.0 → 1.2.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.
Files changed (36) hide show
  1. package/build/{brewbar-installer-GWJ76J6G.js → brewbar-installer-BKE6Z7OI.js} +5 -3
  2. package/build/{brewbar-installer-GWJ76J6G.js.map → brewbar-installer-BKE6Z7OI.js.map} +1 -1
  3. package/build/{brewfile-manager-G7Q4IOG3.js → brewfile-manager-CPVXIVZC.js} +4 -3
  4. package/build/{chunk-KN4GCMIE.js → chunk-5PJWI4XS.js} +20 -1
  5. package/build/chunk-5PJWI4XS.js.map +1 -0
  6. package/build/{chunk-BRXZG7ZL.js → chunk-CMIC4N74.js} +11 -3
  7. package/build/chunk-CMIC4N74.js.map +1 -0
  8. package/build/{chunk-KDKXGXN2.js → chunk-EDQPT5EF.js} +15 -8
  9. package/build/chunk-EDQPT5EF.js.map +1 -0
  10. package/build/{chunk-J6HCX7RG.js → chunk-JNEIP2LJ.js} +2 -2
  11. package/build/chunk-NRRQECXA.js +63 -0
  12. package/build/chunk-NRRQECXA.js.map +1 -0
  13. package/build/{chunk-WDRT6G63.js → chunk-WX7MPVPH.js} +6 -46
  14. package/build/chunk-WX7MPVPH.js.map +1 -0
  15. package/build/{chunk-QX5DEW3S.js → chunk-XI743B6D.js} +408 -2
  16. package/build/chunk-XI743B6D.js.map +1 -0
  17. package/build/{compliance-checker-IXZHIMQG.js → compliance-checker-3FDEX4OI.js} +3 -3
  18. package/build/index.js +299 -566
  19. package/build/index.js.map +1 -1
  20. package/build/{policy-io-EECGRKNA.js → policy-io-P5YIH6C7.js} +2 -2
  21. package/build/{snapshot-ZOJETCED.js → snapshot-RQ444U5L.js} +2 -2
  22. package/build/{sync-engine-76YMONYH.js → sync-engine-Q4B2PPQS.js} +5 -4
  23. package/build/{version-check-MJZDQG73.js → version-check-FKY5HGSI.js} +2 -2
  24. package/package.json +1 -1
  25. package/build/chunk-BRXZG7ZL.js.map +0 -1
  26. package/build/chunk-KDKXGXN2.js.map +0 -1
  27. package/build/chunk-KN4GCMIE.js.map +0 -1
  28. package/build/chunk-QX5DEW3S.js.map +0 -1
  29. package/build/chunk-WDRT6G63.js.map +0 -1
  30. /package/build/{brewfile-manager-G7Q4IOG3.js.map → brewfile-manager-CPVXIVZC.js.map} +0 -0
  31. /package/build/{chunk-J6HCX7RG.js.map → chunk-JNEIP2LJ.js.map} +0 -0
  32. /package/build/{compliance-checker-IXZHIMQG.js.map → compliance-checker-3FDEX4OI.js.map} +0 -0
  33. /package/build/{policy-io-EECGRKNA.js.map → policy-io-P5YIH6C7.js.map} +0 -0
  34. /package/build/{snapshot-ZOJETCED.js.map → snapshot-RQ444U5L.js.map} +0 -0
  35. /package/build/{sync-engine-76YMONYH.js.map → sync-engine-Q4B2PPQS.js.map} +0 -0
  36. /package/build/{version-check-MJZDQG73.js.map → version-check-FKY5HGSI.js.map} +0 -0
@@ -1,7 +1,9 @@
1
1
  import {
2
- fetchWithTimeout,
2
+ fetchWithTimeout
3
+ } from "./chunk-NRRQECXA.js";
4
+ import {
3
5
  t
4
- } from "./chunk-WDRT6G63.js";
6
+ } from "./chunk-WX7MPVPH.js";
5
7
  import "./chunk-KDHEUNRI.js";
6
8
 
7
9
  // src/lib/brewbar-installer.ts
@@ -124,4 +126,4 @@ export {
124
126
  launchBrewBar,
125
127
  uninstallBrewBar
126
128
  };
127
- //# sourceMappingURL=brewbar-installer-GWJ76J6G.js.map
129
+ //# sourceMappingURL=brewbar-installer-BKE6Z7OI.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/brewbar-installer.ts"],"sourcesContent":["import { rm, access, readFile } from 'node:fs/promises';\nimport { createWriteStream } from 'node:fs';\nimport { createHash, randomUUID } from 'node:crypto';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { t } from '../i18n/index.js';\nimport { fetchWithTimeout } from './fetch-timeout.js';\n\nconst execFileAsync = promisify(execFile);\nconst BREWBAR_APP_PATH = '/Applications/BrewBar.app';\nconst DOWNLOAD_URL = 'https://github.com/MoLinesDesigns/Brew-TUI/releases/latest/download/BrewBar.app.zip';\nconst MAX_SIZE = 200 * 1024 * 1024; // 200 MB\n\nexport async function isBrewBarInstalled(): Promise<boolean> {\n try {\n await access(BREWBAR_APP_PATH);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function installBrewBar(isPro: boolean, force = false): Promise<void> {\n // macOS only\n if (process.platform !== 'darwin') {\n throw new Error(t('cli_brewbarMacOnly'));\n }\n\n // Pro check\n if (!isPro) {\n throw new Error(t('cli_brewbarProRequired'));\n }\n\n // Already installed check\n if (!force && await isBrewBarInstalled()) {\n throw new Error(t('cli_brewbarAlreadyInstalled'));\n }\n\n // EP-013: Use unique temp path\n const TMP_ZIP = join(tmpdir(), 'BrewBar-' + randomUUID() + '.zip');\n\n // Download zip (120s timeout for large binary)\n const res = await fetchWithTimeout(DOWNLOAD_URL, {}, 120_000);\n if (!res.ok || !res.body) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: `HTTP ${res.status}` }));\n }\n\n // Reject downloads larger than 200 MB (from Content-Length header)\n const contentLength = Number(res.headers.get('content-length') ?? '0');\n if (contentLength > MAX_SIZE) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'Download exceeds 200 MB size limit' }));\n }\n\n // EP-005: Track downloaded bytes during the stream\n let downloadedBytes = 0;\n\n // Write to tmp file with byte counting\n const fileStream = createWriteStream(TMP_ZIP);\n const transformedBody = new ReadableStream({\n async start(controller) {\n const bodyReader = (res.body as ReadableStream<Uint8Array>).getReader();\n try {\n while (true) {\n const { done, value } = await bodyReader.read();\n if (done) break;\n downloadedBytes += value.length;\n if (downloadedBytes > MAX_SIZE) {\n controller.error(new Error('Download exceeds 200 MB limit'));\n return;\n }\n controller.enqueue(value);\n }\n controller.close();\n } catch (err) {\n controller.error(err);\n }\n },\n });\n await pipeline(transformedBody as unknown as NodeJS.ReadableStream, fileStream);\n\n // SEG-001: SHA-256 integrity check with proper error handling\n let expectedHash: string | null = null;\n try {\n const checksumRes = await fetchWithTimeout(`${DOWNLOAD_URL}.sha256`, {}, 15_000);\n if (checksumRes.ok) {\n const text = await checksumRes.text();\n // EP-009: Validate split result is defined\n const hash = text.trim().split(/\\s+/)[0];\n // EP-010: Validate hash format\n if (hash && /^[0-9a-f]{64}$/i.test(hash)) {\n expectedHash = hash.toLowerCase();\n }\n }\n } catch {\n /* checksum file not available */\n }\n\n if (expectedHash) {\n const fileBuffer = await readFile(TMP_ZIP);\n const actual = createHash('sha256').update(fileBuffer).digest('hex');\n if (actual !== expectedHash) {\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'SHA-256 mismatch: binary may have been tampered with' }));\n }\n } else {\n // NUEVO-003: Treat missing checksum as fatal — don't install unverified binaries\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'SHA-256 checksum unavailable — cannot verify download integrity' }));\n }\n\n // Remove old app if force reinstall\n if (force && await isBrewBarInstalled()) {\n await rm(BREWBAR_APP_PATH, { recursive: true, force: true });\n }\n\n // Unzip to /Applications\n try {\n await execFileAsync('ditto', ['-xk', TMP_ZIP, '/Applications/']);\n } catch (err) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: err instanceof Error ? err.message : String(err) }), { cause: err });\n } finally {\n // Clean up tmp zip\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n }\n}\n\n/// Launches BrewBar detached from the parent process so it survives terminal close.\n/// `open -g -a` runs the app in the background without bringing it to foreground.\nexport async function launchBrewBar(): Promise<void> {\n if (process.platform !== 'darwin') return;\n if (!await isBrewBarInstalled()) return;\n try {\n await execFileAsync('open', ['-g', '-a', BREWBAR_APP_PATH]);\n } catch {\n // Non-fatal: BrewBar may already be running, or LaunchServices may need a moment.\n }\n}\n\nexport async function uninstallBrewBar(): Promise<void> {\n if (!await isBrewBarInstalled()) {\n throw new Error(t('cli_brewbarNotInstalled'));\n }\n\n await rm(BREWBAR_APP_PATH, { recursive: true, force: true });\n}\n"],"mappings":";;;;;;;AAAA,SAAS,IAAI,QAAQ,gBAAgB;AACrC,SAAS,yBAAyB;AAClC,SAAS,YAAY,kBAAkB;AACvC,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB,SAAS,gBAAgB;AACzB,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAI1B,IAAM,gBAAgB,UAAU,QAAQ;AACxC,IAAM,mBAAmB;AACzB,IAAM,eAAe;AACrB,IAAM,WAAW,MAAM,OAAO;AAE9B,eAAsB,qBAAuC;AAC3D,MAAI;AACF,UAAM,OAAO,gBAAgB;AAC7B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,eAAe,OAAgB,QAAQ,OAAsB;AAEjF,MAAI,QAAQ,aAAa,UAAU;AACjC,UAAM,IAAI,MAAM,EAAE,oBAAoB,CAAC;AAAA,EACzC;AAGA,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,EAAE,wBAAwB,CAAC;AAAA,EAC7C;AAGA,MAAI,CAAC,SAAS,MAAM,mBAAmB,GAAG;AACxC,UAAM,IAAI,MAAM,EAAE,6BAA6B,CAAC;AAAA,EAClD;AAGA,QAAM,UAAU,KAAK,OAAO,GAAG,aAAa,WAAW,IAAI,MAAM;AAGjE,QAAM,MAAM,MAAM,iBAAiB,cAAc,CAAC,GAAG,IAAO;AAC5D,MAAI,CAAC,IAAI,MAAM,CAAC,IAAI,MAAM;AACxB,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,QAAQ,IAAI,MAAM,GAAG,CAAC,CAAC;AAAA,EACjF;AAGA,QAAM,gBAAgB,OAAO,IAAI,QAAQ,IAAI,gBAAgB,KAAK,GAAG;AACrE,MAAI,gBAAgB,UAAU;AAC5B,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,qCAAqC,CAAC,CAAC;AAAA,EACjG;AAGA,MAAI,kBAAkB;AAGtB,QAAM,aAAa,kBAAkB,OAAO;AAC5C,QAAM,kBAAkB,IAAI,eAAe;AAAA,IACzC,MAAM,MAAM,YAAY;AACtB,YAAM,aAAc,IAAI,KAAoC,UAAU;AACtE,UAAI;AACF,eAAO,MAAM;AACX,gBAAM,EAAE,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK;AAC9C,cAAI,KAAM;AACV,6BAAmB,MAAM;AACzB,cAAI,kBAAkB,UAAU;AAC9B,uBAAW,MAAM,IAAI,MAAM,+BAA+B,CAAC;AAC3D;AAAA,UACF;AACA,qBAAW,QAAQ,KAAK;AAAA,QAC1B;AACA,mBAAW,MAAM;AAAA,MACnB,SAAS,KAAK;AACZ,mBAAW,MAAM,GAAG;AAAA,MACtB;AAAA,IACF;AAAA,EACF,CAAC;AACD,QAAM,SAAS,iBAAqD,UAAU;AAG9E,MAAI,eAA8B;AAClC,MAAI;AACF,UAAM,cAAc,MAAM,iBAAiB,GAAG,YAAY,WAAW,CAAC,GAAG,IAAM;AAC/E,QAAI,YAAY,IAAI;AAClB,YAAM,OAAO,MAAM,YAAY,KAAK;AAEpC,YAAM,OAAO,KAAK,KAAK,EAAE,MAAM,KAAK,EAAE,CAAC;AAEvC,UAAI,QAAQ,kBAAkB,KAAK,IAAI,GAAG;AACxC,uBAAe,KAAK,YAAY;AAAA,MAClC;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,MAAI,cAAc;AAChB,UAAM,aAAa,MAAM,SAAS,OAAO;AACzC,UAAM,SAAS,WAAW,QAAQ,EAAE,OAAO,UAAU,EAAE,OAAO,KAAK;AACnE,QAAI,WAAW,cAAc;AAC3B,YAAM,GAAG,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AACjD,YAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,uDAAuD,CAAC,CAAC;AAAA,IACnH;AAAA,EACF,OAAO;AAEL,UAAM,GAAG,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACjD,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,uEAAkE,CAAC,CAAC;AAAA,EAC9H;AAGA,MAAI,SAAS,MAAM,mBAAmB,GAAG;AACvC,UAAM,GAAG,kBAAkB,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAC7D;AAGA,MAAI;AACF,UAAM,cAAc,SAAS,CAAC,OAAO,SAAS,gBAAgB,CAAC;AAAA,EACjE,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC,GAAG,EAAE,OAAO,IAAI,CAAC;AAAA,EAC7H,UAAE;AAEA,UAAM,GAAG,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACnD;AACF;AAIA,eAAsB,gBAA+B;AACnD,MAAI,QAAQ,aAAa,SAAU;AACnC,MAAI,CAAC,MAAM,mBAAmB,EAAG;AACjC,MAAI;AACF,UAAM,cAAc,QAAQ,CAAC,MAAM,MAAM,gBAAgB,CAAC;AAAA,EAC5D,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,mBAAkC;AACtD,MAAI,CAAC,MAAM,mBAAmB,GAAG;AAC/B,UAAM,IAAI,MAAM,EAAE,yBAAyB,CAAC;AAAA,EAC9C;AAEA,QAAM,GAAG,kBAAkB,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC7D;","names":[]}
1
+ {"version":3,"sources":["../src/lib/brewbar-installer.ts"],"sourcesContent":["import { rm, access, readFile } from 'node:fs/promises';\nimport { createWriteStream } from 'node:fs';\nimport { createHash, randomUUID } from 'node:crypto';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { pipeline } from 'node:stream/promises';\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport { t } from '../i18n/index.js';\nimport { fetchWithTimeout } from './fetch-timeout.js';\n\nconst execFileAsync = promisify(execFile);\nconst BREWBAR_APP_PATH = '/Applications/BrewBar.app';\nconst DOWNLOAD_URL = 'https://github.com/MoLinesDesigns/Brew-TUI/releases/latest/download/BrewBar.app.zip';\nconst MAX_SIZE = 200 * 1024 * 1024; // 200 MB\n\nexport async function isBrewBarInstalled(): Promise<boolean> {\n try {\n await access(BREWBAR_APP_PATH);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function installBrewBar(isPro: boolean, force = false): Promise<void> {\n // macOS only\n if (process.platform !== 'darwin') {\n throw new Error(t('cli_brewbarMacOnly'));\n }\n\n // Pro check\n if (!isPro) {\n throw new Error(t('cli_brewbarProRequired'));\n }\n\n // Already installed check\n if (!force && await isBrewBarInstalled()) {\n throw new Error(t('cli_brewbarAlreadyInstalled'));\n }\n\n // EP-013: Use unique temp path\n const TMP_ZIP = join(tmpdir(), 'BrewBar-' + randomUUID() + '.zip');\n\n // Download zip (120s timeout for large binary)\n const res = await fetchWithTimeout(DOWNLOAD_URL, {}, 120_000);\n if (!res.ok || !res.body) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: `HTTP ${res.status}` }));\n }\n\n // Reject downloads larger than 200 MB (from Content-Length header)\n const contentLength = Number(res.headers.get('content-length') ?? '0');\n if (contentLength > MAX_SIZE) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'Download exceeds 200 MB size limit' }));\n }\n\n // EP-005: Track downloaded bytes during the stream\n let downloadedBytes = 0;\n\n // Write to tmp file with byte counting\n const fileStream = createWriteStream(TMP_ZIP);\n const transformedBody = new ReadableStream({\n async start(controller) {\n const bodyReader = (res.body as ReadableStream<Uint8Array>).getReader();\n try {\n while (true) {\n const { done, value } = await bodyReader.read();\n if (done) break;\n downloadedBytes += value.length;\n if (downloadedBytes > MAX_SIZE) {\n controller.error(new Error('Download exceeds 200 MB limit'));\n return;\n }\n controller.enqueue(value);\n }\n controller.close();\n } catch (err) {\n controller.error(err);\n }\n },\n });\n await pipeline(transformedBody as unknown as NodeJS.ReadableStream, fileStream);\n\n // SEG-001: SHA-256 integrity check with proper error handling\n let expectedHash: string | null = null;\n try {\n const checksumRes = await fetchWithTimeout(`${DOWNLOAD_URL}.sha256`, {}, 15_000);\n if (checksumRes.ok) {\n const text = await checksumRes.text();\n // EP-009: Validate split result is defined\n const hash = text.trim().split(/\\s+/)[0];\n // EP-010: Validate hash format\n if (hash && /^[0-9a-f]{64}$/i.test(hash)) {\n expectedHash = hash.toLowerCase();\n }\n }\n } catch {\n /* checksum file not available */\n }\n\n if (expectedHash) {\n const fileBuffer = await readFile(TMP_ZIP);\n const actual = createHash('sha256').update(fileBuffer).digest('hex');\n if (actual !== expectedHash) {\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'SHA-256 mismatch: binary may have been tampered with' }));\n }\n } else {\n // NUEVO-003: Treat missing checksum as fatal — don't install unverified binaries\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n throw new Error(t('cli_brewbarDownloadFailed', { error: 'SHA-256 checksum unavailable — cannot verify download integrity' }));\n }\n\n // Remove old app if force reinstall\n if (force && await isBrewBarInstalled()) {\n await rm(BREWBAR_APP_PATH, { recursive: true, force: true });\n }\n\n // Unzip to /Applications\n try {\n await execFileAsync('ditto', ['-xk', TMP_ZIP, '/Applications/']);\n } catch (err) {\n throw new Error(t('cli_brewbarDownloadFailed', { error: err instanceof Error ? err.message : String(err) }), { cause: err });\n } finally {\n // Clean up tmp zip\n await rm(TMP_ZIP, { force: true }).catch(() => {});\n }\n}\n\n/// Launches BrewBar detached from the parent process so it survives terminal close.\n/// `open -g -a` runs the app in the background without bringing it to foreground.\nexport async function launchBrewBar(): Promise<void> {\n if (process.platform !== 'darwin') return;\n if (!await isBrewBarInstalled()) return;\n try {\n await execFileAsync('open', ['-g', '-a', BREWBAR_APP_PATH]);\n } catch {\n // Non-fatal: BrewBar may already be running, or LaunchServices may need a moment.\n }\n}\n\nexport async function uninstallBrewBar(): Promise<void> {\n if (!await isBrewBarInstalled()) {\n throw new Error(t('cli_brewbarNotInstalled'));\n }\n\n await rm(BREWBAR_APP_PATH, { recursive: true, force: true });\n}\n"],"mappings":";;;;;;;;;AAAA,SAAS,IAAI,QAAQ,gBAAgB;AACrC,SAAS,yBAAyB;AAClC,SAAS,YAAY,kBAAkB;AACvC,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB,SAAS,gBAAgB;AACzB,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAI1B,IAAM,gBAAgB,UAAU,QAAQ;AACxC,IAAM,mBAAmB;AACzB,IAAM,eAAe;AACrB,IAAM,WAAW,MAAM,OAAO;AAE9B,eAAsB,qBAAuC;AAC3D,MAAI;AACF,UAAM,OAAO,gBAAgB;AAC7B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,eAAe,OAAgB,QAAQ,OAAsB;AAEjF,MAAI,QAAQ,aAAa,UAAU;AACjC,UAAM,IAAI,MAAM,EAAE,oBAAoB,CAAC;AAAA,EACzC;AAGA,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,EAAE,wBAAwB,CAAC;AAAA,EAC7C;AAGA,MAAI,CAAC,SAAS,MAAM,mBAAmB,GAAG;AACxC,UAAM,IAAI,MAAM,EAAE,6BAA6B,CAAC;AAAA,EAClD;AAGA,QAAM,UAAU,KAAK,OAAO,GAAG,aAAa,WAAW,IAAI,MAAM;AAGjE,QAAM,MAAM,MAAM,iBAAiB,cAAc,CAAC,GAAG,IAAO;AAC5D,MAAI,CAAC,IAAI,MAAM,CAAC,IAAI,MAAM;AACxB,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,QAAQ,IAAI,MAAM,GAAG,CAAC,CAAC;AAAA,EACjF;AAGA,QAAM,gBAAgB,OAAO,IAAI,QAAQ,IAAI,gBAAgB,KAAK,GAAG;AACrE,MAAI,gBAAgB,UAAU;AAC5B,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,qCAAqC,CAAC,CAAC;AAAA,EACjG;AAGA,MAAI,kBAAkB;AAGtB,QAAM,aAAa,kBAAkB,OAAO;AAC5C,QAAM,kBAAkB,IAAI,eAAe;AAAA,IACzC,MAAM,MAAM,YAAY;AACtB,YAAM,aAAc,IAAI,KAAoC,UAAU;AACtE,UAAI;AACF,eAAO,MAAM;AACX,gBAAM,EAAE,MAAM,MAAM,IAAI,MAAM,WAAW,KAAK;AAC9C,cAAI,KAAM;AACV,6BAAmB,MAAM;AACzB,cAAI,kBAAkB,UAAU;AAC9B,uBAAW,MAAM,IAAI,MAAM,+BAA+B,CAAC;AAC3D;AAAA,UACF;AACA,qBAAW,QAAQ,KAAK;AAAA,QAC1B;AACA,mBAAW,MAAM;AAAA,MACnB,SAAS,KAAK;AACZ,mBAAW,MAAM,GAAG;AAAA,MACtB;AAAA,IACF;AAAA,EACF,CAAC;AACD,QAAM,SAAS,iBAAqD,UAAU;AAG9E,MAAI,eAA8B;AAClC,MAAI;AACF,UAAM,cAAc,MAAM,iBAAiB,GAAG,YAAY,WAAW,CAAC,GAAG,IAAM;AAC/E,QAAI,YAAY,IAAI;AAClB,YAAM,OAAO,MAAM,YAAY,KAAK;AAEpC,YAAM,OAAO,KAAK,KAAK,EAAE,MAAM,KAAK,EAAE,CAAC;AAEvC,UAAI,QAAQ,kBAAkB,KAAK,IAAI,GAAG;AACxC,uBAAe,KAAK,YAAY;AAAA,MAClC;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,MAAI,cAAc;AAChB,UAAM,aAAa,MAAM,SAAS,OAAO;AACzC,UAAM,SAAS,WAAW,QAAQ,EAAE,OAAO,UAAU,EAAE,OAAO,KAAK;AACnE,QAAI,WAAW,cAAc;AAC3B,YAAM,GAAG,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AACjD,YAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,uDAAuD,CAAC,CAAC;AAAA,IACnH;AAAA,EACF,OAAO;AAEL,UAAM,GAAG,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACjD,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,uEAAkE,CAAC,CAAC;AAAA,EAC9H;AAGA,MAAI,SAAS,MAAM,mBAAmB,GAAG;AACvC,UAAM,GAAG,kBAAkB,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EAC7D;AAGA,MAAI;AACF,UAAM,cAAc,SAAS,CAAC,OAAO,SAAS,gBAAgB,CAAC;AAAA,EACjE,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,EAAE,6BAA6B,EAAE,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC,GAAG,EAAE,OAAO,IAAI,CAAC;AAAA,EAC7H,UAAE;AAEA,UAAM,GAAG,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACnD;AACF;AAIA,eAAsB,gBAA+B;AACnD,MAAI,QAAQ,aAAa,SAAU;AACnC,MAAI,CAAC,MAAM,mBAAmB,EAAG;AACjC,MAAI;AACF,UAAM,cAAc,QAAQ,CAAC,MAAM,MAAM,gBAAgB,CAAC;AAAA,EAC5D,QAAQ;AAAA,EAER;AACF;AAEA,eAAsB,mBAAkC;AACtD,MAAI,CAAC,MAAM,mBAAmB,GAAG;AAC/B,UAAM,IAAI,MAAM,EAAE,yBAAyB,CAAC;AAAA,EAC9C;AAEA,QAAM,GAAG,kBAAkB,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC7D;","names":[]}
@@ -5,9 +5,10 @@ import {
5
5
  loadBrewfile,
6
6
  reconcile,
7
7
  saveBrewfile
8
- } from "./chunk-QX5DEW3S.js";
9
- import "./chunk-BRXZG7ZL.js";
8
+ } from "./chunk-XI743B6D.js";
9
+ import "./chunk-CMIC4N74.js";
10
10
  import "./chunk-IGDHDXUH.js";
11
+ import "./chunk-WX7MPVPH.js";
11
12
  import "./chunk-KDHEUNRI.js";
12
13
  export {
13
14
  BREWFILE_PATH,
@@ -17,4 +18,4 @@ export {
17
18
  reconcile,
18
19
  saveBrewfile
19
20
  };
20
- //# sourceMappingURL=brewfile-manager-G7Q4IOG3.js.map
21
+ //# sourceMappingURL=brewfile-manager-CPVXIVZC.js.map
@@ -1,11 +1,29 @@
1
1
  // src/lib/compliance/policy-io.ts
2
2
  import { readFile, writeFile } from "fs/promises";
3
+ import { resolve, isAbsolute } from "path";
4
+ function assertSafePath(filePath) {
5
+ if (!filePath || typeof filePath !== "string") {
6
+ throw new Error("Policy path must be a non-empty string");
7
+ }
8
+ const trimmed = filePath.trim();
9
+ if (trimmed.includes("\0")) {
10
+ throw new Error("Policy path contains null byte");
11
+ }
12
+ if (!isAbsolute(trimmed)) {
13
+ throw new Error("Policy path must be absolute");
14
+ }
15
+ const resolved = resolve(trimmed);
16
+ if (resolved !== trimmed) {
17
+ throw new Error("Policy path must be canonical (no .. or trailing /.) \u2014 got: " + filePath);
18
+ }
19
+ }
3
20
  function isValidPolicy(obj) {
4
21
  if (!obj || typeof obj !== "object") return false;
5
22
  const p = obj;
6
23
  return p["version"] === 1 && typeof p["meta"] === "object" && p["meta"] !== null && typeof p["meta"]["teamName"] === "string" && Array.isArray(p["required"]);
7
24
  }
8
25
  async function loadPolicy(filePath) {
26
+ assertSafePath(filePath);
9
27
  const raw = await readFile(filePath, "utf-8");
10
28
  const parsed = JSON.parse(raw);
11
29
  if (!isValidPolicy(parsed)) {
@@ -14,6 +32,7 @@ async function loadPolicy(filePath) {
14
32
  return parsed;
15
33
  }
16
34
  async function exportReport(report, outputPath) {
35
+ assertSafePath(outputPath);
17
36
  await writeFile(outputPath, JSON.stringify(report, null, 2), { encoding: "utf-8", mode: 384 });
18
37
  }
19
38
  async function generatePolicyFromSnapshot(snapshot, teamName, maintainer) {
@@ -45,4 +64,4 @@ export {
45
64
  exportReport,
46
65
  generatePolicyFromSnapshot
47
66
  };
48
- //# sourceMappingURL=chunk-KN4GCMIE.js.map
67
+ //# sourceMappingURL=chunk-5PJWI4XS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/compliance/policy-io.ts"],"sourcesContent":["import { readFile, writeFile } from 'node:fs/promises';\nimport { resolve, isAbsolute } from 'node:path';\nimport type { PolicyFile } from './types.js';\nimport type { ComplianceReport } from './types.js';\nimport type { BrewSnapshot } from '../state-snapshot/snapshot.js';\n\n// BK-005: rechazar paths que contengan `..` o sean relativos ambiguos.\n// El caller (compliance.tsx) puede pasar rutas escritas a mano por el usuario;\n// resolver primero y verificar despues impide escapar a `~/` o subir niveles.\nfunction assertSafePath(filePath: string): void {\n if (!filePath || typeof filePath !== 'string') {\n throw new Error('Policy path must be a non-empty string');\n }\n const trimmed = filePath.trim();\n // Bloquear traversal explicito antes del resolve para mensaje claro.\n if (trimmed.includes('\\0')) {\n throw new Error('Policy path contains null byte');\n }\n // Exigir ruta absoluta — evita ambiguedad respecto al cwd del proceso.\n if (!isAbsolute(trimmed)) {\n throw new Error('Policy path must be absolute');\n }\n const resolved = resolve(trimmed);\n if (resolved !== trimmed) {\n throw new Error('Policy path must be canonical (no .. or trailing /.) — got: ' + filePath);\n }\n}\n\nfunction isValidPolicy(obj: unknown): obj is PolicyFile {\n if (!obj || typeof obj !== 'object') return false;\n const p = obj as Record<string, unknown>;\n return (\n p['version'] === 1 &&\n typeof p['meta'] === 'object' &&\n p['meta'] !== null &&\n typeof (p['meta'] as Record<string, unknown>)['teamName'] === 'string' &&\n Array.isArray(p['required'])\n );\n}\n\nexport async function loadPolicy(filePath: string): Promise<PolicyFile> {\n assertSafePath(filePath);\n const raw = await readFile(filePath, 'utf-8');\n const parsed: unknown = JSON.parse(raw);\n\n if (!isValidPolicy(parsed)) {\n throw new Error('Invalid policy file: must have version=1, meta.teamName, and required array');\n }\n\n return parsed;\n}\n\nexport async function exportReport(report: ComplianceReport, outputPath: string): Promise<void> {\n assertSafePath(outputPath);\n await writeFile(outputPath, JSON.stringify(report, null, 2), { encoding: 'utf-8', mode: 0o600 });\n}\n\nexport async function generatePolicyFromSnapshot(\n snapshot: BrewSnapshot,\n teamName: string,\n maintainer: string,\n): Promise<PolicyFile> {\n return {\n version: 1,\n meta: {\n teamName,\n maintainer,\n createdAt: new Date().toISOString(),\n },\n required: [\n ...snapshot.formulae.map((f) => ({\n name: f.name,\n type: 'formula' as const,\n })),\n ...snapshot.casks.map((c) => ({\n name: c.name,\n type: 'cask' as const,\n })),\n ],\n forbidden: [],\n requiredTaps: [...snapshot.taps],\n strictMode: false,\n };\n}\n"],"mappings":";AAAA,SAAS,UAAU,iBAAiB;AACpC,SAAS,SAAS,kBAAkB;AAQpC,SAAS,eAAe,UAAwB;AAC9C,MAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AAC7C,UAAM,IAAI,MAAM,wCAAwC;AAAA,EAC1D;AACA,QAAM,UAAU,SAAS,KAAK;AAE9B,MAAI,QAAQ,SAAS,IAAI,GAAG;AAC1B,UAAM,IAAI,MAAM,gCAAgC;AAAA,EAClD;AAEA,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,UAAM,IAAI,MAAM,8BAA8B;AAAA,EAChD;AACA,QAAM,WAAW,QAAQ,OAAO;AAChC,MAAI,aAAa,SAAS;AACxB,UAAM,IAAI,MAAM,sEAAiE,QAAQ;AAAA,EAC3F;AACF;AAEA,SAAS,cAAc,KAAiC;AACtD,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,IAAI;AACV,SACE,EAAE,SAAS,MAAM,KACjB,OAAO,EAAE,MAAM,MAAM,YACrB,EAAE,MAAM,MAAM,QACd,OAAQ,EAAE,MAAM,EAA8B,UAAU,MAAM,YAC9D,MAAM,QAAQ,EAAE,UAAU,CAAC;AAE/B;AAEA,eAAsB,WAAW,UAAuC;AACtE,iBAAe,QAAQ;AACvB,QAAM,MAAM,MAAM,SAAS,UAAU,OAAO;AAC5C,QAAM,SAAkB,KAAK,MAAM,GAAG;AAEtC,MAAI,CAAC,cAAc,MAAM,GAAG;AAC1B,UAAM,IAAI,MAAM,6EAA6E;AAAA,EAC/F;AAEA,SAAO;AACT;AAEA,eAAsB,aAAa,QAA0B,YAAmC;AAC9F,iBAAe,UAAU;AACzB,QAAM,UAAU,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,EAAE,UAAU,SAAS,MAAM,IAAM,CAAC;AACjG;AAEA,eAAsB,2BACpB,UACA,UACA,YACqB;AACrB,SAAO;AAAA,IACL,SAAS;AAAA,IACT,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC;AAAA,IACA,UAAU;AAAA,MACR,GAAG,SAAS,SAAS,IAAI,CAAC,OAAO;AAAA,QAC/B,MAAM,EAAE;AAAA,QACR,MAAM;AAAA,MACR,EAAE;AAAA,MACF,GAAG,SAAS,MAAM,IAAI,CAAC,OAAO;AAAA,QAC5B,MAAM,EAAE;AAAA,QACR,MAAM;AAAA,MACR,EAAE;AAAA,IACJ;AAAA,IACA,WAAW,CAAC;AAAA,IACZ,cAAc,CAAC,GAAG,SAAS,IAAI;AAAA,IAC/B,YAAY;AAAA,EACd;AACF;","names":[]}
@@ -12,11 +12,18 @@ import { join } from "path";
12
12
 
13
13
  // src/lib/brew-cli.ts
14
14
  import { spawn } from "child_process";
15
+ import { existsSync } from "fs";
15
16
  var DEFAULT_TIMEOUT_MS = 3e4;
16
17
  var STREAM_IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
18
+ function resolveBrewPath() {
19
+ if (existsSync("/opt/homebrew/bin/brew")) return "/opt/homebrew/bin/brew";
20
+ if (existsSync("/usr/local/bin/brew")) return "/usr/local/bin/brew";
21
+ return "brew";
22
+ }
23
+ var BREW_BIN = resolveBrewPath();
17
24
  async function execBrew(args, timeoutMs = DEFAULT_TIMEOUT_MS) {
18
25
  return new Promise((resolve, reject) => {
19
- const proc = spawn("brew", args, { env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: "1" } });
26
+ const proc = spawn(BREW_BIN, args, { env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: "1" } });
20
27
  let stdout = "";
21
28
  let stderr = "";
22
29
  let killed = false;
@@ -48,7 +55,7 @@ async function execBrew(args, timeoutMs = DEFAULT_TIMEOUT_MS) {
48
55
  });
49
56
  }
50
57
  async function* streamBrew(args) {
51
- const proc = spawn("brew", args, {
58
+ const proc = spawn(BREW_BIN, args, {
52
59
  env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: "1" },
53
60
  stdio: ["ignore", "pipe", "pipe"]
54
61
  });
@@ -264,6 +271,7 @@ async function getLatestSnapshot() {
264
271
  }
265
272
 
266
273
  export {
274
+ BREW_BIN,
267
275
  execBrew,
268
276
  streamBrew,
269
277
  captureSnapshot,
@@ -274,4 +282,4 @@ export {
274
282
  deleteSnapshot,
275
283
  getLatestSnapshot
276
284
  };
277
- //# sourceMappingURL=chunk-BRXZG7ZL.js.map
285
+ //# sourceMappingURL=chunk-CMIC4N74.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/state-snapshot/snapshot.ts","../src/lib/brew-cli.ts"],"sourcesContent":["import { readFile, writeFile, rename, readdir, unlink } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { execBrew } from '../brew-cli.js';\nimport { SNAPSHOTS_DIR, ensureDataDirs } from '../data-dir.js';\nimport { logger } from '../../utils/logger.js';\n\nexport interface BrewSnapshot {\n capturedAt: string;\n label?: string;\n formulae: Array<{ name: string; version: string; pinned: boolean }>;\n casks: Array<{ name: string; version: string }>;\n taps: string[];\n}\n\nfunction isValidSnapshot(v: unknown): v is BrewSnapshot {\n if (!v || typeof v !== 'object') return false;\n const s = v as Record<string, unknown>;\n return (\n typeof s['capturedAt'] === 'string' &&\n Array.isArray(s['formulae']) &&\n Array.isArray(s['casks']) &&\n Array.isArray(s['taps'])\n );\n}\n\n/** Parse `brew list --versions --formula` or `brew list --cask --versions`.\n * Each line: `name version1 version2...` — last version is the current install. */\nfunction parseVersionsList(output: string): Array<{ name: string; version: string }> {\n return output\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => {\n const parts = line.split(/\\s+/);\n const name = parts[0] ?? '';\n const version = parts[parts.length - 1] ?? '';\n return { name, version };\n })\n .filter((e) => e.name !== '');\n}\n\n/** Parse `brew tap` — one tap per line. */\nfunction parseTapsList(output: string): string[] {\n return output\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean);\n}\n\n/** Parse `brew list --pinned` — one formula per line. */\nfunction parsePinnedList(output: string): Set<string> {\n return new Set(\n output\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean),\n );\n}\n\nexport async function captureSnapshot(): Promise<BrewSnapshot> {\n const [formulaeRaw, casksRaw, tapsRaw, pinnedRaw] = await Promise.all([\n execBrew(['list', '--versions', '--formula']),\n execBrew(['list', '--cask', '--versions']),\n execBrew(['tap']),\n execBrew(['list', '--pinned']),\n ]);\n\n const pinned = parsePinnedList(pinnedRaw);\n const formulae = parseVersionsList(formulaeRaw).map((f) => ({\n ...f,\n pinned: pinned.has(f.name),\n }));\n const casks = parseVersionsList(casksRaw);\n const taps = parseTapsList(tapsRaw);\n\n return {\n capturedAt: new Date().toISOString(),\n formulae,\n casks,\n taps,\n };\n}\n\n/** Sanitize a label for safe use in a filename. */\nfunction sanitizeLabel(label: string): string {\n const clean = label.replace(/[^A-Za-z0-9_-]/g, '_');\n return clean.length > 0 ? clean : 'auto';\n}\n\n/** Convert an ISO 8601 timestamp to a filename-safe string by replacing `:` and `.` with `-`. */\nfunction timestampToFilename(iso: string): string {\n return iso.replace(/[:.]/g, '-');\n}\n\n/** Cap on retained snapshots. Older auto snapshots are pruned to keep\n * the rollback feature from filling ~/.brew-tui/snapshots/ indefinitely.\n * 20 entries fits roughly 2-3 weeks of typical usage. */\nexport const SNAPSHOT_RETENTION_LIMIT = 20;\n\nexport async function saveSnapshot(s: BrewSnapshot, label?: string): Promise<void> {\n await ensureDataDirs();\n\n const effectiveLabel = label ? sanitizeLabel(label) : 'auto';\n const filename = `${timestampToFilename(s.capturedAt)}-${effectiveLabel}.json`;\n const filePath = join(SNAPSHOTS_DIR, filename);\n const tmpPath = filePath + '.tmp';\n\n const payload: BrewSnapshot = label ? { ...s, label } : s;\n\n await writeFile(tmpPath, JSON.stringify(payload, null, 2), { encoding: 'utf-8', mode: 0o600 });\n await rename(tmpPath, filePath);\n\n await pruneSnapshots();\n}\n\n/** Delete the oldest auto-labelled snapshots beyond SNAPSHOT_RETENTION_LIMIT.\n * Labelled snapshots (those the user explicitly named) are preserved — they\n * represent intentional checkpoints. */\nexport async function pruneSnapshots(maxCount = SNAPSHOT_RETENTION_LIMIT): Promise<number> {\n let entries: string[];\n try {\n entries = await readdir(SNAPSHOTS_DIR);\n } catch {\n return 0;\n }\n\n const candidates: Array<{ filename: string; capturedAt: string }> = [];\n for (const filename of entries.filter((f) => f.endsWith('.json'))) {\n // Auto snapshots are named ...-auto.json. User-labelled ones get a slug\n // and stay regardless of count.\n if (!filename.endsWith('-auto.json')) continue;\n try {\n const raw = await readFile(join(SNAPSHOTS_DIR, filename), 'utf-8');\n const parsed: unknown = JSON.parse(raw);\n if (isValidSnapshot(parsed)) {\n candidates.push({ filename, capturedAt: parsed.capturedAt });\n }\n } catch { /* skip unreadable */ }\n }\n\n if (candidates.length <= maxCount) return 0;\n\n candidates.sort((a, b) => b.capturedAt.localeCompare(a.capturedAt));\n const toDelete = candidates.slice(maxCount);\n let removed = 0;\n for (const entry of toDelete) {\n try {\n await unlink(join(SNAPSHOTS_DIR, entry.filename));\n removed++;\n } catch (err) {\n logger.warn('Failed to prune snapshot', { filename: entry.filename, error: String(err) });\n }\n }\n if (removed > 0) {\n logger.info('Pruned snapshots', { removed, retained: maxCount });\n }\n return removed;\n}\n\nexport async function loadSnapshots(): Promise<BrewSnapshot[]> {\n let entries: string[];\n try {\n entries = await readdir(SNAPSHOTS_DIR);\n } catch {\n return [];\n }\n\n const jsonFiles = entries.filter((f) => f.endsWith('.json'));\n const snapshots: BrewSnapshot[] = [];\n\n for (const filename of jsonFiles) {\n try {\n const raw = await readFile(join(SNAPSHOTS_DIR, filename), 'utf-8');\n const parsed: unknown = JSON.parse(raw);\n if (!isValidSnapshot(parsed)) {\n logger.warn('Skipping corrupt snapshot file', { filename });\n continue;\n }\n snapshots.push(parsed);\n } catch {\n logger.warn('Failed to read snapshot file', { filename });\n }\n }\n\n return snapshots.sort((a, b) => b.capturedAt.localeCompare(a.capturedAt));\n}\n\nexport async function deleteSnapshot(capturedAt: string): Promise<void> {\n let entries: string[];\n try {\n entries = await readdir(SNAPSHOTS_DIR);\n } catch {\n return;\n }\n\n for (const filename of entries.filter((f) => f.endsWith('.json'))) {\n try {\n const raw = await readFile(join(SNAPSHOTS_DIR, filename), 'utf-8');\n const parsed: unknown = JSON.parse(raw);\n if (isValidSnapshot(parsed) && parsed.capturedAt === capturedAt) {\n await unlink(join(SNAPSHOTS_DIR, filename));\n return;\n }\n } catch {\n // Skip unreadable files\n }\n }\n}\n\nexport async function getLatestSnapshot(): Promise<BrewSnapshot | null> {\n const all = await loadSnapshots();\n return all[0] ?? null;\n}\n","import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\n\nconst DEFAULT_TIMEOUT_MS = 30_000; // 30 seconds for instant commands\nconst STREAM_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes idle timeout for streaming\n\n// SEG-004: resolver `brew` a ruta absoluta una sola vez al cargar el modulo.\n// Sin esto, `spawn('brew', ...)` hereda PATH del usuario y un PATH hijack\n// (dotfile comprometido, malware) podria suplantar el binario. Probamos las\n// dos rutas canonicas en macOS (Apple Silicon y Intel) y caemos al\n// resolutor del shell solo si ninguna existe.\nfunction resolveBrewPath(): string {\n if (existsSync('/opt/homebrew/bin/brew')) return '/opt/homebrew/bin/brew';\n if (existsSync('/usr/local/bin/brew')) return '/usr/local/bin/brew';\n return 'brew';\n}\n\nexport const BREW_BIN = resolveBrewPath();\n\nexport async function execBrew(args: string[], timeoutMs = DEFAULT_TIMEOUT_MS): Promise<string> {\n return new Promise((resolve, reject) => {\n const proc = spawn(BREW_BIN, args, { env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: '1' } });\n let stdout = '';\n let stderr = '';\n let killed = false;\n\n // EP-012: Timeout with AbortController pattern\n const timer = setTimeout(() => {\n killed = true;\n proc.kill();\n reject(new Error(`brew ${args.join(' ')} timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n\n proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });\n proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });\n proc.on('close', (code) => {\n clearTimeout(timer);\n if (killed) return;\n if (code === 0) {\n resolve(stdout);\n } else {\n reject(new Error(stderr.trim() || `brew ${args.join(' ')} exited with code ${code}`));\n }\n });\n proc.on('error', (err) => {\n clearTimeout(timer);\n if (killed) return;\n reject(new Error(`Failed to run brew: ${err.message}`));\n });\n });\n}\n\nexport async function* streamBrew(args: string[]): AsyncGenerator<string> {\n const proc = spawn(BREW_BIN, args, {\n env: { ...process.env, HOMEBREW_NO_AUTO_UPDATE: '1' },\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n let buffer = '';\n const lines: string[] = [];\n let done = false;\n let exitError: string | null = null;\n let lastOutputAt = Date.now();\n\n // PERF-002: replace the 100 ms sleep loop with a Promise that resolves on\n // every stdout/stderr chunk or exit. The previous implementation woke the\n // event loop ten times per second and added up to 100 ms latency per line.\n let waker: (() => void) | null = null;\n const wake = () => {\n if (waker) { const w = waker; waker = null; w(); }\n };\n const wait = () => new Promise<void>((resolve) => { waker = resolve; });\n\n const push = (chunk: Buffer) => {\n lastOutputAt = Date.now();\n buffer += chunk.toString();\n const parts = buffer.split('\\n');\n buffer = parts.pop() ?? '';\n for (const line of parts) {\n if (line.trim()) lines.push(line);\n }\n wake();\n };\n\n proc.stdout.on('data', push);\n proc.stderr.on('data', push);\n\n proc.on('close', (code) => {\n if (buffer.trim()) lines.push(buffer.trim());\n done = true;\n if (code !== 0) {\n exitError = `brew ${args.join(' ')} exited with code ${code}`;\n }\n wake();\n });\n\n proc.on('error', (err) => {\n done = true;\n exitError = err.message;\n wake();\n });\n\n try {\n while (!done || lines.length > 0) {\n if (lines.length > 0) {\n yield lines.shift()!;\n } else if (!done) {\n // EP-012: kill process if idle for too long. The idle check still runs\n // periodically through a guard timer because the wake path only fires\n // on output — a hung child without any output would never wake us.\n const guard = setTimeout(wake, 1_000);\n await wait();\n clearTimeout(guard);\n if (Date.now() - lastOutputAt > STREAM_IDLE_TIMEOUT_MS) {\n proc.kill();\n throw new Error(`brew ${args.join(' ')} timed out: no output for ${STREAM_IDLE_TIMEOUT_MS / 1000}s`);\n }\n }\n }\n } finally {\n if (!done) {\n proc.kill();\n }\n }\n\n // Throw after all lines have been yielded so the consumer sees\n // brew's stderr output in the stream before the error surfaces.\n if (exitError) {\n throw new Error(exitError);\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  captureSnapshot
3
- } from "./chunk-BRXZG7ZL.js";
3
+ } from "./chunk-CMIC4N74.js";
4
4
  import {
5
5
  DATA_DIR,
6
6
  LICENSE_PATH,
@@ -8,9 +8,11 @@ import {
8
8
  getMachineId
9
9
  } from "./chunk-IGDHDXUH.js";
10
10
  import {
11
- fetchWithRetry,
11
+ fetchWithRetry
12
+ } from "./chunk-NRRQECXA.js";
13
+ import {
12
14
  t
13
- } from "./chunk-WDRT6G63.js";
15
+ } from "./chunk-WX7MPVPH.js";
14
16
  import {
15
17
  logger
16
18
  } from "./chunk-KDHEUNRI.js";
@@ -146,7 +148,7 @@ async function readSyncEnvelope() {
146
148
  }
147
149
  }
148
150
  async function writeSyncEnvelope(envelope) {
149
- await mkdir(ICLOUD_SYNC_DIR, { recursive: true });
151
+ await mkdir(ICLOUD_SYNC_DIR, { recursive: true, mode: 448 });
150
152
  const tmpPath = ICLOUD_SYNC_PATH + ".tmp";
151
153
  await writeFile(tmpPath, JSON.stringify(envelope, null, 2), {
152
154
  encoding: "utf-8",
@@ -160,6 +162,10 @@ import { readFile as readFile2, writeFile as writeFile2, rename as rename2, rm }
160
162
  import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes2, scryptSync as scryptSync2, hkdfSync as hkdfSync2 } from "crypto";
161
163
 
162
164
  // src/lib/license/polar-api.ts
165
+ import { createHash } from "crypto";
166
+ function hashMachineLabel(machineId) {
167
+ return createHash("sha256").update(machineId).digest("hex").slice(0, 32);
168
+ }
163
169
  var BASE_URL = "https://api.polar.sh/v1/customer-portal/license-keys";
164
170
  var POLAR_ORGANIZATION_ID = "b8f245c0-d116-4457-92fb-1bda47139f82";
165
171
  function validateApiUrl(url) {
@@ -172,7 +178,7 @@ function validateApiUrl(url) {
172
178
  }
173
179
  }
174
180
  async function post(endpoint, body, expectEmpty = false) {
175
- const url = `${BASE_URL}/${endpoint}`;
181
+ const url = `${BASE_URL}/${endpoint}/`;
176
182
  validateApiUrl(url);
177
183
  const res = await fetchWithRetry(url, {
178
184
  method: "POST",
@@ -198,8 +204,9 @@ async function activateLicense(key) {
198
204
  const activation = await post("activate", {
199
205
  key,
200
206
  organization_id: POLAR_ORGANIZATION_ID,
201
- label: machineId
202
- // SEG-004: Use machine UUID instead of hostname
207
+ // SEG-004 + BK-009: identificador estable por equipo, hasheado para
208
+ // que el UUID en claro no aparezca en logs de Polar.
209
+ label: hashMachineLabel(machineId)
203
210
  });
204
211
  if (!activation || typeof activation.id !== "string" || !activation.license_key) {
205
212
  throw new Error("Invalid activation response: missing required fields");
@@ -718,4 +725,4 @@ export {
718
725
  sync,
719
726
  applyConflictResolutions
720
727
  };
721
- //# sourceMappingURL=chunk-KDKXGXN2.js.map
728
+ //# sourceMappingURL=chunk-EDQPT5EF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/sync/sync-engine.ts","../src/lib/sync/crypto.ts","../src/lib/sync/types.ts","../src/lib/sync/backends/icloud-backend.ts","../src/lib/license/license-manager.ts","../src/lib/license/polar-api.ts","../src/lib/license/types.ts"],"sourcesContent":["import { readFile, writeFile, rename } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { hostname } from 'node:os';\nimport { encryptPayload, decryptPayload } from './crypto.js';\nimport {\n readSyncEnvelope,\n writeSyncEnvelope,\n isICloudAvailable,\n} from './backends/icloud-backend.js';\nimport { captureSnapshot } from '../state-snapshot/snapshot.js';\nimport { DATA_DIR, getMachineId } from '../data-dir.js';\nimport { loadLicense } from '../license/license-manager.js';\nimport { logger } from '../../utils/logger.js';\nimport type {\n SyncConfig,\n SyncPayload,\n SyncConflict,\n SyncResult,\n MachineState,\n SyncEnvelope,\n} from './types.js';\nimport type { BrewfileSchema } from '../brewfile/types.js';\n\nconst SYNC_CONFIG_PATH = join(DATA_DIR, 'sync-config.json');\n\n// ── Config I/O ──────────────────────────────────────────────────────────────\n\nexport async function loadSyncConfig(): Promise<SyncConfig | null> {\n try {\n const raw = await readFile(SYNC_CONFIG_PATH, 'utf-8');\n return JSON.parse(raw) as SyncConfig;\n } catch {\n return null;\n }\n}\n\nexport async function saveSyncConfig(config: SyncConfig): Promise<void> {\n const tmpPath = SYNC_CONFIG_PATH + '.tmp';\n await writeFile(tmpPath, JSON.stringify(config, null, 2), {\n encoding: 'utf-8',\n mode: 0o600,\n });\n await rename(tmpPath, SYNC_CONFIG_PATH);\n}\n\n// ── Machine ID ───────────────────────────────────────────────────────────────\n// Single canonical implementation lives in data-dir.ts. The previous fallback\n// to os.hostname() here meant two different machines with the same hostname\n// (common on freshly-imaged corporate fleets) collided in sync state.\n\nexport { getMachineId };\n\n// ── Conflict detection ───────────────────────────────────────────────────────\n\nfunction detectConflicts(\n localSnapshot: { formulae: Array<{ name: string; version: string }>; casks: Array<{ name: string; version: string }> },\n otherMachines: MachineState[],\n localMachineId: string,\n): SyncConflict[] {\n const conflicts: SyncConflict[] = [];\n\n const localFormulaMap = new Map(localSnapshot.formulae.map((f) => [f.name, f.version]));\n const localCaskMap = new Map(localSnapshot.casks.map((c) => [c.name, c.version]));\n\n for (const machine of otherMachines) {\n if (machine.machineId === localMachineId) continue;\n\n // Check formula conflicts: same package, different version on both machines\n for (const remoteFormula of machine.snapshot.formulae) {\n const localVersion = localFormulaMap.get(remoteFormula.name);\n if (localVersion !== undefined && localVersion !== remoteFormula.version) {\n conflicts.push({\n packageName: remoteFormula.name,\n packageType: 'formula',\n localVersion,\n remoteMachine: machine.machineName,\n remoteVersion: remoteFormula.version,\n });\n }\n }\n\n // Check cask conflicts\n for (const remoteCask of machine.snapshot.casks) {\n const localVersion = localCaskMap.get(remoteCask.name);\n if (localVersion !== undefined && localVersion !== remoteCask.version) {\n conflicts.push({\n packageName: remoteCask.name,\n packageType: 'cask',\n localVersion,\n remoteMachine: machine.machineName,\n remoteVersion: remoteCask.version,\n });\n }\n }\n }\n\n return conflicts;\n}\n\n// ── Merge ────────────────────────────────────────────────────────────────────\n\nasync function writeEnvelope(payload: SyncPayload, licenseKey: string): Promise<string> {\n const now = new Date().toISOString();\n const { encrypted, iv, tag } = encryptPayload(payload, licenseKey);\n const envelope: SyncEnvelope = {\n schemaVersion: 1,\n encrypted,\n iv,\n tag,\n updatedAt: now,\n };\n await writeSyncEnvelope(envelope);\n return now;\n}\n\nasync function loadLicenseKeyOrThrow(): Promise<string> {\n // Sync requires Pro, and Pro requires a license. Read it lazily so\n // sync-store callers don't have to plumb the key through every call.\n const license = await loadLicense();\n if (!license || !license.key) {\n throw new Error('Sync requires an active license');\n }\n return license.key;\n}\n\nfunction mergePayload(existing: SyncPayload, localState: MachineState): SyncPayload {\n return {\n machines: {\n ...existing.machines,\n [localState.machineId]: localState,\n },\n };\n}\n\n// ── Main sync function ───────────────────────────────────────────────────────\n\nexport async function sync(\n isPro: boolean,\n currentBrewfile?: BrewfileSchema,\n): Promise<SyncResult> {\n if (!isPro) {\n throw new Error('Pro license required');\n }\n\n const available = await isICloudAvailable();\n if (!available) {\n return {\n success: false,\n conflicts: [],\n resolvedCount: 0,\n error: 'iCloud Drive not available',\n };\n }\n\n const licenseKey = await loadLicenseKeyOrThrow();\n\n let existingPayload: SyncPayload | null = null;\n\n try {\n const envelope = await readSyncEnvelope();\n if (envelope) {\n existingPayload = decryptPayload(envelope.encrypted, envelope.iv, envelope.tag, licenseKey);\n }\n } catch (err) {\n logger.warn('sync: could not decrypt existing payload, starting fresh', { error: String(err) });\n existingPayload = null;\n }\n\n // Capture current local state\n const snapshot = await captureSnapshot();\n const machineId = await getMachineId();\n const machineName = hostname();\n\n const localState: MachineState = {\n machineId,\n machineName,\n updatedAt: new Date().toISOString(),\n snapshot,\n ...(currentBrewfile ? { brewfile: currentBrewfile } : {}),\n };\n\n // Detect conflicts against other machines in the payload\n const otherMachines = existingPayload\n ? Object.values(existingPayload.machines).filter((m) => m.machineId !== machineId)\n : [];\n\n const conflicts = detectConflicts(snapshot, otherMachines, machineId);\n\n // Always write the local machine state to the payload, even when conflicts\n // exist, so that applyConflictResolutions() has a local entry to update.\n // Without this, the iCloud envelope keeps only remote machines, and\n // resolution updates are silently dropped (they require localMachine to exist).\n const basePayload: SyncPayload = existingPayload ?? { machines: {} };\n const mergedPayload = mergePayload(basePayload, localState);\n\n if (conflicts.length > 0) {\n // Persist local state, then surface conflicts so the user can resolve them.\n await writeEnvelope(mergedPayload, licenseKey);\n return {\n success: false,\n conflicts,\n resolvedCount: 0,\n };\n }\n\n const now = await writeEnvelope(mergedPayload, licenseKey);\n\n // Update local sync config\n const existingConfig = await loadSyncConfig();\n await saveSyncConfig({\n enabled: true,\n machineId,\n machineName,\n ...(existingConfig ?? {}),\n lastSync: now,\n });\n\n logger.info('sync: completed successfully', { machineId, machines: Object.keys(mergedPayload.machines).length });\n\n return {\n success: true,\n conflicts: [],\n resolvedCount: 0,\n };\n}\n\n// ── Conflict resolution ──────────────────────────────────────────────────────\n\nexport async function applyConflictResolutions(\n payload: SyncPayload,\n resolutions: Array<{ conflict: SyncConflict; resolution: 'use-local' | 'use-remote' }>,\n localMachineId: string,\n): Promise<void> {\n // Work on a mutable copy\n const updatedPayload: SyncPayload = {\n machines: { ...payload.machines },\n };\n\n for (const { conflict, resolution } of resolutions) {\n if (resolution !== 'use-remote') continue;\n // Re-read latest local machine on every iteration so consecutive resolutions\n // build on top of each other instead of overwriting prior changes.\n const localMachine = updatedPayload.machines[localMachineId];\n if (!localMachine) {\n logger.warn('sync: cannot apply resolution, local machine missing in payload', { localMachineId });\n continue;\n }\n if (conflict.packageType === 'formula') {\n updatedPayload.machines[localMachineId] = {\n ...localMachine,\n snapshot: {\n ...localMachine.snapshot,\n formulae: localMachine.snapshot.formulae.map((f) =>\n f.name === conflict.packageName\n ? { ...f, version: conflict.remoteVersion }\n : f,\n ),\n },\n };\n } else {\n updatedPayload.machines[localMachineId] = {\n ...localMachine,\n snapshot: {\n ...localMachine.snapshot,\n casks: localMachine.snapshot.casks.map((c) =>\n c.name === conflict.packageName\n ? { ...c, version: conflict.remoteVersion }\n : c,\n ),\n },\n };\n }\n }\n\n const licenseKey = await loadLicenseKeyOrThrow();\n await writeEnvelope(updatedPayload, licenseKey);\n logger.info('sync: conflict resolutions applied', { count: resolutions.length });\n}\n","import { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from 'node:crypto';\nimport { isSyncPayload, type SyncPayload } from './types.js';\n\n// SEG-003: Cross-machine sync encryption.\n// The two constants below are public (compiled into the npm bundle). The\n// per-user secret factor is the Polar license key, which only the user's\n// own machines hold and which Polar issues — so any two of the user's\n// machines derive the same key, but bundle + iCloud snoop is no longer\n// enough to decrypt: the attacker also needs the license key.\n//\n// HKDF-SHA256 over scrypt: the license key is high-entropy by construction\n// (Polar issues UUID-style keys), so the cost-hardening of scrypt isn't\n// what's protecting the key — the secrecy of the license key is. HKDF is\n// also faster, so machines don't pay scrypt's CPU tax on every sync.\nconst ENCRYPTION_SECRET = 'brew-tui-sync-aes256gcm-v1';\nconst HKDF_SALT = 'brew-tui-sync-salt-v1';\n\nconst keyCache = new Map<string, Buffer>();\nlet _legacyKey: Buffer | null = null;\n\nfunction deriveEncryptionKey(licenseKey: string): Buffer {\n const cached = keyCache.get(licenseKey);\n if (cached) return cached;\n const derived = Buffer.from(hkdfSync('sha256', ENCRYPTION_SECRET, HKDF_SALT, licenseKey, 32));\n keyCache.set(licenseKey, derived);\n return derived;\n}\n\n// Legacy key — scrypt(SECRET, SALT), no license-key factor. Used as a\n// decryption fallback for envelopes written by 0.6.2 and earlier.\n// TODO(SEG-003, 0.6.3): remove `_legacyKey` after telemetry confirms zero\n// fallback decrypts in the wild.\nfunction deriveLegacyKey(): Buffer {\n if (!_legacyKey) {\n _legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32, { N: 16384, r: 8, p: 1 });\n }\n return _legacyKey;\n}\n\nexport function encryptPayload(data: SyncPayload, licenseKey: string): { encrypted: string; iv: string; tag: string } {\n const key = deriveEncryptionKey(licenseKey);\n const iv = randomBytes(12); // 96-bit IV for GCM\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n\n const plaintext = JSON.stringify(data);\n const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n return {\n encrypted: ciphertext.toString('base64'),\n iv: iv.toString('base64'),\n tag: tag.toString('base64'),\n };\n}\n\nexport function decryptPayload(encrypted: string, iv: string, tag: string, licenseKey: string): SyncPayload {\n const ivBuf = Buffer.from(iv, 'base64');\n const tagBuf = Buffer.from(tag, 'base64');\n const ciphertext = Buffer.from(encrypted, 'base64');\n\n // Try the licenseKey-bound key first; fall back to the legacy bundle-only\n // key for envelopes written by 0.6.2 and earlier. Re-encryption happens\n // automatically on the next sync write because writeEnvelope always uses\n // the current key.\n for (const key of [deriveEncryptionKey(licenseKey), deriveLegacyKey()]) {\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, ivBuf);\n decipher.setAuthTag(tagBuf);\n const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n const parsed: unknown = JSON.parse(plaintext.toString('utf-8'));\n if (!isSyncPayload(parsed)) throw new Error('Invalid sync payload shape');\n return parsed;\n } catch { /* try next */ }\n }\n throw new Error('Failed to decrypt sync payload');\n}\n","import type { BrewSnapshot } from '../state-snapshot/snapshot.js';\nimport type { BrewfileSchema } from '../brewfile/types.js';\n\nexport interface SyncConfig {\n enabled: boolean;\n machineId: string;\n machineName: string;\n lastSync?: string; // ISO 8601\n}\n\nexport interface MachineState {\n machineId: string;\n machineName: string;\n updatedAt: string; // ISO 8601\n snapshot: BrewSnapshot;\n brewfile?: BrewfileSchema;\n}\n\nexport interface SyncPayload {\n machines: Record<string, MachineState>;\n}\n\n// BK-008: type guard for sync envelopes after AES-GCM decrypt. Defends against\n// truncated or migrated payloads landing as undefined accesses downstream.\nexport function isSyncPayload(value: unknown): value is SyncPayload {\n if (typeof value !== 'object' || value === null) return false;\n const machines = (value as Record<string, unknown>).machines;\n if (typeof machines !== 'object' || machines === null || Array.isArray(machines)) return false;\n for (const m of Object.values(machines as Record<string, unknown>)) {\n if (typeof m !== 'object' || m === null) return false;\n const state = m as Record<string, unknown>;\n if (\n typeof state.machineId !== 'string' ||\n typeof state.machineName !== 'string' ||\n typeof state.updatedAt !== 'string' ||\n typeof state.snapshot !== 'object'\n ) {\n return false;\n }\n }\n return true;\n}\n\nexport interface SyncEnvelope {\n schemaVersion: 1;\n encrypted: string;\n iv: string;\n tag: string;\n updatedAt: string; // ISO 8601 — plaintext for BrewBar monitoring\n}\n\n// BK-006: 'merge-union' aparecia en este union pero applyConflictResolutions()\n// nunca implemento la rama; cualquier caller que lo pasara veia el conflicto\n// descartado silenciosamente. Eliminado hasta que exista la logica de merge.\nexport type ConflictResolution = 'use-local' | 'use-remote';\n\nexport interface SyncConflict {\n packageName: string;\n packageType: 'formula' | 'cask';\n localVersion: string;\n remoteMachine: string;\n remoteVersion: string;\n}\n\nexport interface SyncResult {\n success: boolean;\n conflicts: SyncConflict[];\n resolvedCount: number;\n error?: string;\n}\n","import { readFile, writeFile, rename, mkdir, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { logger } from '../../../utils/logger.js';\nimport type { SyncEnvelope } from '../types.js';\n\nconst ICLOUD_BASE = join(\n homedir(),\n 'Library', 'Mobile Documents', 'com~apple~CloudDocs',\n);\nexport const ICLOUD_SYNC_DIR = join(ICLOUD_BASE, 'BrewTUI');\nexport const ICLOUD_SYNC_PATH = join(ICLOUD_SYNC_DIR, 'sync.json');\n\nexport async function isICloudAvailable(): Promise<boolean> {\n try {\n await stat(ICLOUD_BASE);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction isValidEnvelope(v: unknown): v is SyncEnvelope {\n if (!v || typeof v !== 'object') return false;\n const obj = v as Record<string, unknown>;\n return (\n obj['schemaVersion'] === 1 &&\n typeof obj['encrypted'] === 'string' &&\n typeof obj['iv'] === 'string' &&\n typeof obj['tag'] === 'string' &&\n typeof obj['updatedAt'] === 'string'\n );\n}\n\nexport async function readSyncEnvelope(): Promise<SyncEnvelope | null> {\n // BK-012: iCloud may leave an undownloaded placeholder at the path. Reading\n // returns 0 bytes (or ENOENT for the file but a sibling .icloud entry).\n // Treat empty / missing-but-pending as \"not yet ready\" without surfacing\n // a misleading \"no remote state\" to the caller.\n try {\n const info = await stat(ICLOUD_SYNC_PATH);\n if (info.size === 0) {\n logger.warn('sync: iCloud envelope exists but is empty (placeholder?)');\n return null;\n }\n } catch (err: unknown) {\n if (err instanceof Error && (err as NodeJS.ErrnoException).code === 'ENOENT') {\n // First-sync case OR pending download — check for the placeholder sibling.\n try {\n const placeholder = ICLOUD_SYNC_PATH.replace(/sync\\.json$/, '.sync.json.icloud');\n await stat(placeholder);\n logger.warn('sync: iCloud placeholder present, file not yet downloaded');\n } catch { /* genuinely absent */ }\n return null;\n }\n logger.warn('sync: could not stat iCloud envelope', { error: String(err) });\n return null;\n }\n\n try {\n const raw = await readFile(ICLOUD_SYNC_PATH, 'utf-8');\n const parsed: unknown = JSON.parse(raw);\n if (!isValidEnvelope(parsed)) {\n logger.warn('sync: invalid envelope structure in iCloud file');\n return null;\n }\n return parsed;\n } catch (err: unknown) {\n logger.warn('sync: could not read iCloud envelope', { error: String(err) });\n return null;\n }\n}\n\nexport async function writeSyncEnvelope(envelope: SyncEnvelope): Promise<void> {\n // BK-007: explicito 0o700 — iCloud Drive hereda permisos del sistema y por\n // defecto pueden ser 0o755. Aunque el contenido del envelope va cifrado, el\n // listado del directorio no deberia ser legible por otros usuarios locales.\n await mkdir(ICLOUD_SYNC_DIR, { recursive: true, mode: 0o700 });\n const tmpPath = ICLOUD_SYNC_PATH + '.tmp';\n await writeFile(tmpPath, JSON.stringify(envelope, null, 2), {\n encoding: 'utf-8',\n mode: 0o600,\n });\n await rename(tmpPath, ICLOUD_SYNC_PATH);\n}\n","import { readFile, writeFile, rename, rm } from 'node:fs/promises';\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from 'node:crypto';\nimport { LICENSE_PATH, ensureDataDirs, getMachineId } from '../data-dir.js';\nimport { activateLicense as apiActivate, validateLicense as apiValidate, deactivateLicense as apiDeactivate } from './polar-api.js';\nimport { t } from '../../i18n/index.js';\nimport { isLicenseData, type LicenseData, type LicenseFile } from './types.js';\n\n// SEG-009 guard: previously a hardcoded map bypassed Polar entirely. The\n// function is kept as an always-null export so a regression test can pin\n// the behaviour and the import site in license-store stays stable.\nexport function getBuiltinAccountType(_email: string): 'pro' | 'team' | 'free' | null {\n return null;\n}\n\nconst REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h\nconst GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; // 7 days\n\n// ── Layer 18: Client-side rate limiting on activations ──\nconst ACTIVATION_COOLDOWN_MS = 30_000; // 30 seconds between attempts\nconst MAX_ATTEMPTS = 5;\nconst LOCKOUT_MS = 15 * 60 * 1000; // 15 min lockout after max attempts\n\ninterface ActivationTracker {\n attempts: number;\n lastAttempt: number;\n lockedUntil: number;\n}\n\n// UX-004: rate-limit state is intentionally in-memory only. It is a first\n// filter to slow down brute force inside one TUI session — the authoritative\n// activation throttle lives in the Polar backend, which sees attempts across\n// process restarts. Persisting this client-side would invite users to delete\n// the file and reset themselves; the trade-off is documented here on purpose.\nconst tracker: ActivationTracker = {\n attempts: 0,\n lastAttempt: 0,\n lockedUntil: 0,\n};\n\nfunction checkRateLimit(): void {\n const now = Date.now();\n\n // Check lockout\n if (now < tracker.lockedUntil) {\n const remaining = Math.ceil((tracker.lockedUntil - now) / 60000);\n throw new Error(t('cli_rateLimited', { minutes: remaining }));\n }\n\n // Check cooldown\n if (now - tracker.lastAttempt < ACTIVATION_COOLDOWN_MS) {\n throw new Error(t('cli_cooldown'));\n }\n}\n\nfunction recordAttempt(success: boolean): void {\n const now = Date.now();\n tracker.lastAttempt = now;\n\n if (success) {\n tracker.attempts = 0;\n return;\n }\n\n tracker.attempts++;\n if (tracker.attempts >= MAX_ATTEMPTS) {\n tracker.lockedUntil = now + LOCKOUT_MS;\n tracker.attempts = 0;\n }\n}\n\n// SECURITY (SEG-002): the bundle-only constants below USED to be the entire\n// derivation input — anyone with the npm bundle could decrypt any user's\n// license.json. Now the per-user machineId is mixed into the HKDF info, so\n// the bundle alone is no longer sufficient: an attacker also needs the\n// target's ~/.brew-tui/machine-id. The two constants stay published; what's\n// secret is the user's local machineId, which never leaves the machine.\n//\n// HKDF-SHA256 was chosen over scrypt because Swift's CryptoKit (used by\n// BrewBar to read the same license.json) ships HKDF natively but not scrypt.\n// machineId is a UUIDv4 with 122 bits of entropy, so the cost-hardening of\n// scrypt is not what's protecting the key — the secrecy of the machineId is.\nconst ENCRYPTION_SECRET = 'brew-tui-license-aes256gcm-v1';\nconst HKDF_SALT = 'brew-tui-salt-v1';\n\nlet _derivedKey: Buffer | null = null;\nlet _legacyKey: Buffer | null = null;\nlet _decryptedWithLegacyKey = false;\n\nasync function deriveEncryptionKey(): Promise<Buffer> {\n if (_derivedKey) return _derivedKey;\n const machineId = await getMachineId();\n // HKDF: ikm = SECRET, salt = HKDF_SALT, info = machineId, len = 32\n const derived = hkdfSync('sha256', ENCRYPTION_SECRET, HKDF_SALT, machineId, 32);\n _derivedKey = Buffer.from(derived);\n return _derivedKey;\n}\n\n// Legacy key — scrypt(SECRET, SALT) with no machineId. Pre-existing\n// license.json files written by 0.6.2 and earlier are ciphered with this.\n// decryptLicenseData falls back to it; the next saveLicense re-ciphers\n// using the HKDF key. TODO(SEG-003, 0.6.3): remove `_legacyKey` after\n// telemetry confirms zero fallback decrypts in the wild.\nfunction deriveLegacyKey(): Buffer {\n if (!_legacyKey) _legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32);\n return _legacyKey;\n}\n\nasync function encryptLicenseData(data: LicenseData): Promise<{ encrypted: string; iv: string; tag: string }> {\n const key = await deriveEncryptionKey();\n const iv = randomBytes(12); // 96-bit IV for GCM\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n\n const plaintext = JSON.stringify(data);\n const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n return {\n encrypted: ciphertext.toString('base64'),\n iv: iv.toString('base64'),\n tag: tag.toString('base64'),\n };\n}\n\nasync function decryptLicenseData(encrypted: string, iv: string, tag: string): Promise<LicenseData> {\n const ivBuf = Buffer.from(iv, 'base64');\n const tagBuf = Buffer.from(tag, 'base64');\n const ciphertext = Buffer.from(encrypted, 'base64');\n\n // Try the current (machine-bound) key first; fall back to the legacy\n // (bundle-only) key for upgrade compatibility.\n const candidates: Array<[Buffer, boolean]> = [\n [await deriveEncryptionKey(), false],\n [deriveLegacyKey(), true],\n ];\n let lastErr: unknown;\n for (const [key, isLegacy] of candidates) {\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, ivBuf);\n decipher.setAuthTag(tagBuf);\n const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n const parsed: unknown = JSON.parse(plaintext.toString('utf-8'));\n if (!isLicenseData(parsed)) {\n throw new Error('Decrypted license payload failed shape validation');\n }\n _decryptedWithLegacyKey = isLegacy;\n return parsed;\n } catch (err) { lastErr = err; }\n }\n throw lastErr instanceof Error ? lastErr : new Error('Failed to decrypt license');\n}\n\n// BK-003: Type guard for license data format\nfunction isLicenseFile(obj: unknown): obj is LicenseFile {\n return typeof obj === 'object' && obj !== null && (obj as Record<string, unknown>).version === 1;\n}\n\nfunction isEncryptedLicenseFile(obj: unknown): obj is LicenseFile & { encrypted: string; iv: string; tag: string } {\n if (!isLicenseFile(obj)) return false;\n const record = obj as unknown as Record<string, unknown>;\n return typeof record.encrypted === 'string'\n && typeof record.iv === 'string'\n && typeof record.tag === 'string';\n}\n\nexport async function loadLicense(): Promise<LicenseData | null> {\n try {\n const raw = await readFile(LICENSE_PATH, 'utf-8');\n const parsed: unknown = JSON.parse(raw);\n\n // BK-003: Validate parsed data\n if (!isLicenseFile(parsed)) {\n throw new Error('Invalid license data format');\n }\n\n const file = parsed as LicenseFile;\n\n if (file.version !== 1) {\n // Future: add migration logic here\n throw new Error('Unsupported data version');\n }\n\n // New encrypted format\n if (isEncryptedLicenseFile(file)) {\n const data = await decryptLicenseData(file.encrypted!, file.iv!, file.tag!);\n\n // SEG-002: Check machine ID if stored in the envelope.\n // getMachineId() now always resolves a value — if the user's machine-id\n // file was wiped, a new UUID is created and this check rejects the\n // license, prompting reactivation. Same behaviour the polar-api flow\n // already had on save.\n const fileRecord = file as unknown as Record<string, unknown>;\n if (fileRecord.machineId) {\n const currentMachineId = await getMachineId();\n if (fileRecord.machineId !== currentMachineId) {\n throw new Error('License was activated on a different machine');\n }\n }\n\n // If we fell back to the legacy bundle-only key, re-cipher with the\n // current machine-bound key so future reads use the strong path.\n if (_decryptedWithLegacyKey) {\n _decryptedWithLegacyKey = false;\n try { await saveLicense(data); } catch { /* best effort */ }\n }\n\n return data;\n }\n\n // Legacy unencrypted format — migrate to encrypted on read\n if (file.license) {\n const data = file.license;\n // Re-save in encrypted format\n await saveLicense(data);\n return data;\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\nexport async function saveLicense(data: LicenseData): Promise<void> {\n await ensureDataDirs();\n const { encrypted, iv, tag } = await encryptLicenseData(data);\n // SEG-002: Include machineId in the envelope for portability detection\n const machineId = await getMachineId();\n const file: Record<string, unknown> = { version: 1, encrypted, iv, tag, machineId };\n const tmpPath = LICENSE_PATH + '.tmp';\n await writeFile(tmpPath, JSON.stringify(file, null, 2), { encoding: 'utf-8', mode: 0o600 });\n await rename(tmpPath, LICENSE_PATH);\n}\n\nexport async function clearLicense(): Promise<void> {\n try {\n await rm(LICENSE_PATH);\n } catch { /* file may not exist */ }\n}\n\nexport function isExpired(license: LicenseData): boolean {\n if (!license.expiresAt) return false;\n const expiry = new Date(license.expiresAt).getTime();\n // Fail closed on corrupted/unparseable dates: NaN comparisons are always\n // false, so the previous version treated a garbage expiresAt as \"never\n // expires\", which is exploitable.\n if (isNaN(expiry)) return true;\n return expiry < Date.now();\n}\n\nexport function needsRevalidation(license: LicenseData): boolean {\n const lastValidated = new Date(license.lastValidatedAt).getTime();\n if (isNaN(lastValidated)) return true; // corrupted date → force revalidation\n return Date.now() - lastValidated > REVALIDATION_INTERVAL_MS;\n}\n\nexport function isWithinGracePeriod(license: LicenseData): boolean {\n const lastValidated = new Date(license.lastValidatedAt).getTime();\n if (isNaN(lastValidated)) return false; // corrupted date → no grace\n return Date.now() - lastValidated < GRACE_PERIOD_MS;\n}\n\n// ── Layer 15: Gradual degradation after extended offline ──\n\nexport type DegradationLevel = 'none' | 'warning' | 'limited' | 'expired';\nexport type RevalidationResult = 'valid' | 'grace' | 'expired';\n\n/**\n * Returns the degradation level based on time since last server validation.\n * - 0-7 days: none (full access)\n * - 7-14 days: warning (shows a notice but still works)\n * - 14-30 days: limited (some features disabled)\n * - 30+ days: expired (all Pro features disabled)\n */\nexport function getDegradationLevel(license: LicenseData): DegradationLevel {\n const lastValidated = new Date(license.lastValidatedAt).getTime();\n if (isNaN(lastValidated)) return 'expired'; // corrupted date → deny access\n const elapsed = Date.now() - lastValidated;\n if (elapsed < 0) return 'none'; // clock skew: future timestamp → treat as fresh\n const days = elapsed / (24 * 60 * 60 * 1000);\n\n if (days <= 7) return 'none';\n if (days <= 14) return 'warning';\n if (days <= 30) return 'limited';\n return 'expired';\n}\n\n// Layer 10: License key format validation\nfunction validateLicenseKey(key: string): void {\n // Polar keys are UUID-like: 8-4-4-4-12 hex chars or similar\n // Reject obviously invalid keys to avoid unnecessary API calls\n if (key.length < 10 || key.length > 100) {\n throw new Error('Invalid license key format');\n }\n // Only allow alphanumeric, hyphens, underscores\n if (!/^[\\w-]+$/.test(key)) {\n throw new Error('Invalid license key format');\n }\n}\n\n// Polar license-key benefits use distinct prefixes per tier:\n// Pro Monthly/Yearly → \"BTUI-...\"\n// Team Monthly/Yearly → \"BTUI-T-...\"\n// We detect the tier from the prefix instead of looking up the productId,\n// because Polar's customer-portal license endpoints don't echo product info\n// in the activation response.\nfunction detectPlan(key: string): 'pro' | 'team' {\n const upper = key.toUpperCase();\n return upper.startsWith('BTUI-T-') || upper.startsWith('BTUI-T_') ? 'team' : 'pro';\n}\n\nexport async function activate(key: string): Promise<LicenseData> {\n validateLicenseKey(key);\n checkRateLimit();\n\n let success = false;\n try {\n const res = await apiActivate(key);\n\n if (!res.activated) {\n throw new Error(res.error ?? 'Activation failed');\n }\n\n const license: LicenseData = {\n key,\n instanceId: res.instance.id,\n status: 'active',\n customerEmail: res.meta.customer_email,\n customerName: res.meta.customer_name,\n plan: detectPlan(key),\n activatedAt: new Date().toISOString(),\n expiresAt: res.license_key.expires_at,\n lastValidatedAt: new Date().toISOString(),\n };\n\n await saveLicense(license);\n success = true;\n return license;\n } finally {\n recordAttempt(success);\n }\n}\n\n/**\n * Revalidate the license against the server.\n * This also serves as Layer 19 (telemetry): each validation call\n * allows Polar to track activation count, last-seen timestamp,\n * and detect if the activation limit is exceeded (license sharing).\n */\n// EP-006: Detect if an error is a network error vs validation/contract error\nfunction isNetworkError(err: unknown): boolean {\n const msg = err instanceof Error ? err.message : String(err);\n return /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network|timeout|abort/i.test(msg);\n}\n\nexport async function revalidate(license: LicenseData): Promise<RevalidationResult> {\n try {\n const res = await apiValidate(license.key, license.instanceId);\n\n if (res.valid) {\n const updated: LicenseData = {\n ...license,\n lastValidatedAt: new Date().toISOString(),\n status: 'active',\n expiresAt: res.license_key.expires_at,\n };\n await saveLicense(updated);\n return 'valid';\n }\n\n await saveLicense({ ...license, status: 'expired' });\n return 'expired';\n } catch (err) {\n // EP-006: Network errors trigger grace period; validation/contract errors mean expired\n if (isNetworkError(err)) {\n return isWithinGracePeriod(license) ? 'grace' : 'expired';\n }\n // Unexpected response or contract violation — treat as expired\n await saveLicense({ ...license, status: 'expired' });\n return 'expired';\n }\n}\n\nexport async function deactivate(license: LicenseData): Promise<{ remoteSuccess: boolean }> {\n // EP-001: apiDeactivate already wraps fetchWithRetry (3 attempts). The\n // outer loop multiplied that into 9 POSTs — Polar would count each as a\n // separate request and a flaky network would amplify load 3×.\n let remoteSuccess = false;\n try {\n await apiDeactivate(license.key, license.instanceId);\n remoteSuccess = true;\n } catch { /* local clear still happens below */ }\n await clearLicense();\n return { remoteSuccess };\n}\n","import { createHash } from 'node:crypto';\nimport type { PolarActivateResponse, PolarValidateResponse } from './types.js';\nimport { fetchWithRetry } from '../fetch-timeout.js';\nimport { getMachineId } from '../data-dir.js';\n\n// BK-009: hash truncado SHA-256 del machineId — opacidad adicional frente a\n// correlacion en logs de Polar. El servidor solo necesita un identificador\n// estable por equipo; no requiere el UUID en claro.\nfunction hashMachineLabel(machineId: string): string {\n return createHash('sha256').update(machineId).digest('hex').slice(0, 32);\n}\n\nconst BASE_URL = 'https://api.polar.sh/v1/customer-portal/license-keys';\n\n// ── GOV-004: Public organization ID (not a secret) ──\n// This is the public Polar organization identifier used for license key operations.\n// Found at: polar.sh/dashboard -> Settings -> General\nexport const POLAR_ORGANIZATION_ID = 'b8f245c0-d116-4457-92fb-1bda47139f82';\n\n// Polar product IDs (public, not secret) — useful for analytics, support, and\n// future server-side validation that wants to confirm what the customer bought.\nexport const POLAR_PRODUCT_IDS = {\n proMonthly: 'b925b882-464c-40c1-9ffd-b088ab31d9a3',\n proYearly: '8f97bb81-b950-4bc3-97c5-8133dd817d0b',\n teamMonthly: '7cf3fcb2-560d-4fbb-9936-15efac511b23',\n teamYearly: 'd096914d-902d-47b0-8d62-5c7e6fc4e087',\n} as const;\n\n// Public checkout URLs surfaced from the landing page and the CLI upgrade prompt.\n// Team links carry ?quantity=3 because Polar has no native min-seats enforcement\n// and the Team tier is sold from 3 seats up.\nexport const POLAR_CHECKOUT_URLS = {\n proMonthly: 'https://buy.polar.sh/polar_cl_QW1ZJ9887bU74drGr7JfujQfm3RKYnn1fuvc53DqD6D',\n proYearly: 'https://buy.polar.sh/polar_cl_yQsiUeDelyyEQznbWffD1j77JAyP24ra7iEVQ22PA4h',\n teamMonthly: 'https://buy.polar.sh/polar_cl_CO6xqSzKgFiQJwXnhZYGqisOP04Wspi0KKZSn38NjFZ?quantity=3',\n teamYearly: 'https://buy.polar.sh/polar_cl_BZowqmtaKwWEkRJNtBcashWg7oZOH6OhnnsJ204opNA?quantity=3',\n} as const;\n\n// Layer 11: API URL validation\nfunction validateApiUrl(url: string): void {\n const parsed = new URL(url);\n if (parsed.protocol !== 'https:') {\n throw new Error('HTTPS required for license API');\n }\n if (!parsed.hostname.endsWith('polar.sh')) {\n throw new Error('Invalid API host');\n }\n}\n\n// Raw Polar response shapes\ninterface PolarActivation {\n id: string; // activation_id\n license_key: {\n status: string;\n expires_at: string | null;\n };\n}\n\ninterface PolarValidated {\n id: string;\n status: string; // 'granted' | 'revoked' | 'disabled'\n expires_at: string | null;\n customer: {\n email: string | null;\n name: string | null;\n };\n activation: { id: string } | null;\n}\n\nasync function post<T>(endpoint: string, body: Record<string, unknown>, expectEmpty = false): Promise<T> {\n // BK-008: Polar requiere trailing slash en sus rutas. Sin la barra final el\n // servidor responde 307 y `fetch` con redirect followed pierde la cabecera\n // Authorization → 405. Aseguramos la barra final aqui para que el caller\n // pueda seguir usando rutas semanticas sin recordar la convencion.\n const url = `${BASE_URL}/${endpoint}/`;\n validateApiUrl(url);\n\n const res = await fetchWithRetry(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }, 15_000);\n\n if (!res.ok) {\n let message = `Request failed with status ${res.status}`;\n try {\n const errBody = await res.json() as { detail?: string; error?: string; message?: string };\n if (typeof errBody.detail === 'string') message = errBody.detail;\n else if (typeof errBody.error === 'string') message = errBody.error;\n else if (typeof errBody.message === 'string') message = errBody.message;\n } catch {\n // non-JSON error body — use generic message above\n }\n throw new Error(message);\n }\n\n if (expectEmpty || res.status === 204) return undefined as T;\n return res.json() as Promise<T>;\n}\n\nexport async function activateLicense(key: string): Promise<PolarActivateResponse> {\n const machineId = await getMachineId();\n\n const activation = await post<PolarActivation>('activate', {\n key,\n organization_id: POLAR_ORGANIZATION_ID,\n // SEG-004 + BK-009: identificador estable por equipo, hasheado para\n // que el UUID en claro no aparezca en logs de Polar.\n label: hashMachineLabel(machineId),\n });\n\n // EP-001: Runtime validation of activation response\n if (!activation || typeof activation.id !== 'string' || !activation.license_key) {\n throw new Error('Invalid activation response: missing required fields');\n }\n\n // Polar's activate response doesn't include customer info — fetch it via validate\n let customerEmail = '';\n let customerName = '';\n try {\n const validated = await post<PolarValidated>('validate', {\n key,\n organization_id: POLAR_ORGANIZATION_ID,\n activation_id: activation.id,\n });\n customerEmail = validated.customer?.email ?? '';\n customerName = validated.customer?.name ?? '';\n } catch {\n // customer info is non-critical — activation still succeeds\n }\n\n return {\n activated: true,\n error: null,\n instance: { id: activation.id },\n license_key: {\n id: 0,\n status: activation.license_key.status,\n key,\n activation_limit: 0,\n activations_count: 0,\n expires_at: activation.license_key.expires_at,\n },\n meta: { customer_email: customerEmail, customer_name: customerName },\n };\n}\n\nexport async function validateLicense(key: string, instanceId: string): Promise<PolarValidateResponse> {\n const res = await post<PolarValidated>('validate', {\n key,\n organization_id: POLAR_ORGANIZATION_ID,\n activation_id: instanceId,\n });\n\n // EP-002: Runtime validation of validate response\n if (!res || typeof res.id !== 'string' || typeof res.status !== 'string' || !res.customer) {\n throw new Error('Invalid validation response: missing required fields');\n }\n\n const notExpired = res.expires_at === null || new Date(res.expires_at) > new Date();\n const valid = res.status === 'granted' && notExpired;\n\n return {\n valid,\n error: valid ? null : `License ${res.status}`,\n license_key: {\n id: 0,\n status: res.status,\n key,\n expires_at: res.expires_at,\n },\n instance: { id: instanceId },\n };\n}\n\nexport async function deactivateLicense(key: string, instanceId: string): Promise<void> {\n await post<void>(\n 'deactivate',\n { key, organization_id: POLAR_ORGANIZATION_ID, activation_id: instanceId },\n true,\n );\n}\n","export interface LicenseData {\n key: string;\n instanceId: string;\n status: 'active' | 'expired' | 'inactive';\n customerEmail: string;\n customerName: string;\n plan: 'pro' | 'team';\n activatedAt: string;\n expiresAt: string | null;\n lastValidatedAt: string;\n}\n\n// BK-006: type guard for license payload after AES-GCM decrypt. A corrupt or\n// migrated file could JSON.parse to anything — refuse instead of crashing on\n// undefined accesses downstream.\nexport function isLicenseData(value: unknown): value is LicenseData {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.key === 'string' &&\n typeof v.instanceId === 'string' &&\n (v.status === 'active' || v.status === 'expired' || v.status === 'inactive') &&\n typeof v.customerEmail === 'string' &&\n typeof v.customerName === 'string' &&\n (v.plan === 'pro' || v.plan === 'team') &&\n typeof v.activatedAt === 'string' &&\n (v.expiresAt === null || typeof v.expiresAt === 'string') &&\n typeof v.lastValidatedAt === 'string'\n );\n}\n\nexport interface LicenseFile {\n version: 1;\n license?: LicenseData | null; // legacy unencrypted\n hmac?: string; // legacy\n encrypted?: string; // AES-256-GCM encrypted license JSON\n iv?: string;\n tag?: string;\n}\n\nexport type LicenseStatus = 'free' | 'pro' | 'team' | 'expired' | 'validating';\n\nexport interface PolarActivateResponse {\n activated: boolean;\n error: string | null;\n license_key: {\n id: number;\n status: string;\n key: string;\n activation_limit: number;\n activations_count: number;\n expires_at: string | null;\n };\n instance: { id: string };\n meta: { customer_name: string; customer_email: string };\n}\n\nexport interface PolarValidateResponse {\n valid: boolean;\n error: string | null;\n license_key: {\n id: number;\n status: string;\n key: string;\n expires_at: string | null;\n };\n instance: { id: string };\n}\n\nexport type ProFeatureId =\n | 'profiles'\n | 'smart-cleanup'\n | 'history'\n | 'security-audit'\n | 'rollback'\n | 'brewfile'\n | 'sync'\n | 'impact-analysis';\n\nexport type TeamFeatureId = 'compliance';\n"],"mappings":"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","names":["readFile","writeFile","rename","join","readFile","writeFile","rename","createCipheriv","createDecipheriv","randomBytes","scryptSync","hkdfSync","ENCRYPTION_SECRET","HKDF_SALT","_legacyKey","deriveEncryptionKey","hkdfSync","deriveLegacyKey","scryptSync","randomBytes","createCipheriv","createDecipheriv","readFile","writeFile","rename","join","readFile","writeFile","rename"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  captureSnapshot
3
- } from "./chunk-BRXZG7ZL.js";
3
+ } from "./chunk-CMIC4N74.js";
4
4
 
5
5
  // src/lib/compliance/compliance-checker.ts
6
6
  import { hostname } from "os";
@@ -120,4 +120,4 @@ export {
120
120
  versionAtLeast,
121
121
  checkCompliance
122
122
  };
123
- //# sourceMappingURL=chunk-J6HCX7RG.js.map
123
+ //# sourceMappingURL=chunk-JNEIP2LJ.js.map
@@ -0,0 +1,63 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-KDHEUNRI.js";
4
+
5
+ // src/lib/fetch-timeout.ts
6
+ function fetchWithTimeout(url, options = {}, timeoutMs = 15e3) {
7
+ return fetch(url, { ...options, signal: AbortSignal.timeout(timeoutMs) });
8
+ }
9
+ var DEFAULT_RETRY = {
10
+ attempts: 3,
11
+ baseDelayMs: 500,
12
+ maxDelayMs: 4e3,
13
+ // BK-004: 429 (rate limit) tambien es transitorio. Reintentamos respetando
14
+ // `Retry-After` cuando esta presente (se gestiona en el bucle inferior).
15
+ retryOn: (res) => res.status === 429 || res.status >= 500 && res.status < 600
16
+ };
17
+ function parseRetryAfter(res) {
18
+ const raw = res.headers.get("retry-after");
19
+ if (!raw) return null;
20
+ const seconds = Number(raw);
21
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.floor(seconds * 1e3);
22
+ const ts = Date.parse(raw);
23
+ if (Number.isFinite(ts)) {
24
+ const delta = ts - Date.now();
25
+ return delta > 0 ? delta : null;
26
+ }
27
+ return null;
28
+ }
29
+ function isTransientNetworkError(err) {
30
+ const msg = err instanceof Error ? err.message : String(err);
31
+ return /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network|timeout|abort|EAI_AGAIN/i.test(msg);
32
+ }
33
+ async function fetchWithRetry(url, options = {}, timeoutMs = 15e3, retry = {}) {
34
+ const cfg = { ...DEFAULT_RETRY, ...retry };
35
+ let lastError;
36
+ for (let attempt = 1; attempt <= cfg.attempts; attempt++) {
37
+ try {
38
+ const res = await fetchWithTimeout(url, options, timeoutMs);
39
+ if (attempt < cfg.attempts && cfg.retryOn?.(res)) {
40
+ const retryAfter = parseRetryAfter(res);
41
+ const backoff = Math.min(cfg.baseDelayMs * Math.pow(2, attempt - 1), cfg.maxDelayMs);
42
+ const delay = retryAfter !== null ? Math.min(retryAfter, cfg.maxDelayMs) : backoff;
43
+ logger.warn(`fetchWithRetry: ${url} returned ${res.status}, retry ${attempt}/${cfg.attempts - 1} in ${delay}ms`);
44
+ await new Promise((r) => setTimeout(r, delay));
45
+ continue;
46
+ }
47
+ return res;
48
+ } catch (err) {
49
+ lastError = err;
50
+ if (attempt >= cfg.attempts || !isTransientNetworkError(err)) throw err;
51
+ const delay = Math.min(cfg.baseDelayMs * Math.pow(2, attempt - 1), cfg.maxDelayMs);
52
+ logger.warn(`fetchWithRetry: ${url} threw transient error, retry ${attempt}/${cfg.attempts - 1} in ${delay}ms`, { error: String(err) });
53
+ await new Promise((r) => setTimeout(r, delay));
54
+ }
55
+ }
56
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
57
+ }
58
+
59
+ export {
60
+ fetchWithTimeout,
61
+ fetchWithRetry
62
+ };
63
+ //# sourceMappingURL=chunk-NRRQECXA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/fetch-timeout.ts"],"sourcesContent":["import { logger } from '../utils/logger.js';\n\nexport function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs = 15_000): Promise<Response> {\n return fetch(url, { ...options, signal: AbortSignal.timeout(timeoutMs) });\n}\n\n/**\n * Wrap an async function with debug-level latency logging.\n */\nexport function timed<T>(label: string, fn: () => Promise<T>): Promise<T> {\n const start = Date.now();\n return fn().finally(() => logger.debug(`${label} took ${Date.now() - start}ms`));\n}\n\ninterface RetryOptions {\n attempts?: number;\n baseDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (response: Response) => boolean;\n}\n\nconst DEFAULT_RETRY: Required<Omit<RetryOptions, 'retryOn'>> & Pick<RetryOptions, 'retryOn'> = {\n attempts: 3,\n baseDelayMs: 500,\n maxDelayMs: 4_000,\n // BK-004: 429 (rate limit) tambien es transitorio. Reintentamos respetando\n // `Retry-After` cuando esta presente (se gestiona en el bucle inferior).\n retryOn: (res) => res.status === 429 || (res.status >= 500 && res.status < 600),\n};\n\n// BK-004: parse del header `Retry-After`. RFC 7231: o bien segundos (entero) o\n// un HTTP-date. Devuelve milisegundos o null si no se puede interpretar.\nfunction parseRetryAfter(res: Response): number | null {\n const raw = res.headers.get('retry-after');\n if (!raw) return null;\n const seconds = Number(raw);\n if (Number.isFinite(seconds) && seconds >= 0) return Math.floor(seconds * 1000);\n const ts = Date.parse(raw);\n if (Number.isFinite(ts)) {\n const delta = ts - Date.now();\n return delta > 0 ? delta : null;\n }\n return null;\n}\n\nfunction isTransientNetworkError(err: unknown): boolean {\n const msg = err instanceof Error ? err.message : String(err);\n return /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network|timeout|abort|EAI_AGAIN/i.test(msg);\n}\n\n/**\n * fetchWithTimeout + retry with exponential backoff. Retries only on transient\n * network errors and on responses matching `retryOn` (default: 5xx). 4xx\n * responses are returned to the caller without retry.\n */\nexport async function fetchWithRetry(\n url: string,\n options: RequestInit = {},\n timeoutMs = 15_000,\n retry: RetryOptions = {},\n): Promise<Response> {\n const cfg = { ...DEFAULT_RETRY, ...retry };\n let lastError: unknown;\n\n for (let attempt = 1; attempt <= cfg.attempts; attempt++) {\n try {\n const res = await fetchWithTimeout(url, options, timeoutMs);\n if (attempt < cfg.attempts && cfg.retryOn?.(res)) {\n // BK-004: si el servidor indica `Retry-After` (tipico en 429), usar\n // ese hint en lugar del backoff exponencial. Cap al maxDelay para no\n // congelar al usuario indefinidamente si el servidor pide minutos.\n const retryAfter = parseRetryAfter(res);\n const backoff = Math.min(cfg.baseDelayMs * Math.pow(2, attempt - 1), cfg.maxDelayMs);\n const delay = retryAfter !== null ? Math.min(retryAfter, cfg.maxDelayMs) : backoff;\n logger.warn(`fetchWithRetry: ${url} returned ${res.status}, retry ${attempt}/${cfg.attempts - 1} in ${delay}ms`);\n await new Promise((r) => setTimeout(r, delay));\n continue;\n }\n return res;\n } catch (err) {\n lastError = err;\n if (attempt >= cfg.attempts || !isTransientNetworkError(err)) throw err;\n const delay = Math.min(cfg.baseDelayMs * Math.pow(2, attempt - 1), cfg.maxDelayMs);\n logger.warn(`fetchWithRetry: ${url} threw transient error, retry ${attempt}/${cfg.attempts - 1} in ${delay}ms`, { error: String(err) });\n await new Promise((r) => setTimeout(r, delay));\n }\n }\n\n throw lastError instanceof Error ? lastError : new Error(String(lastError));\n}\n"],"mappings":";;;;;AAEO,SAAS,iBAAiB,KAAa,UAAuB,CAAC,GAAG,YAAY,MAA2B;AAC9G,SAAO,MAAM,KAAK,EAAE,GAAG,SAAS,QAAQ,YAAY,QAAQ,SAAS,EAAE,CAAC;AAC1E;AAiBA,IAAM,gBAAyF;AAAA,EAC7F,UAAU;AAAA,EACV,aAAa;AAAA,EACb,YAAY;AAAA;AAAA;AAAA,EAGZ,SAAS,CAAC,QAAQ,IAAI,WAAW,OAAQ,IAAI,UAAU,OAAO,IAAI,SAAS;AAC7E;AAIA,SAAS,gBAAgB,KAA8B;AACrD,QAAM,MAAM,IAAI,QAAQ,IAAI,aAAa;AACzC,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,UAAU,OAAO,GAAG;AAC1B,MAAI,OAAO,SAAS,OAAO,KAAK,WAAW,EAAG,QAAO,KAAK,MAAM,UAAU,GAAI;AAC9E,QAAM,KAAK,KAAK,MAAM,GAAG;AACzB,MAAI,OAAO,SAAS,EAAE,GAAG;AACvB,UAAM,QAAQ,KAAK,KAAK,IAAI;AAC5B,WAAO,QAAQ,IAAI,QAAQ;AAAA,EAC7B;AACA,SAAO;AACT;AAEA,SAAS,wBAAwB,KAAuB;AACtD,QAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,SAAO,iFAAiF,KAAK,GAAG;AAClG;AAOA,eAAsB,eACpB,KACA,UAAuB,CAAC,GACxB,YAAY,MACZ,QAAsB,CAAC,GACJ;AACnB,QAAM,MAAM,EAAE,GAAG,eAAe,GAAG,MAAM;AACzC,MAAI;AAEJ,WAAS,UAAU,GAAG,WAAW,IAAI,UAAU,WAAW;AACxD,QAAI;AACF,YAAM,MAAM,MAAM,iBAAiB,KAAK,SAAS,SAAS;AAC1D,UAAI,UAAU,IAAI,YAAY,IAAI,UAAU,GAAG,GAAG;AAIhD,cAAM,aAAa,gBAAgB,GAAG;AACtC,cAAM,UAAU,KAAK,IAAI,IAAI,cAAc,KAAK,IAAI,GAAG,UAAU,CAAC,GAAG,IAAI,UAAU;AACnF,cAAM,QAAQ,eAAe,OAAO,KAAK,IAAI,YAAY,IAAI,UAAU,IAAI;AAC3E,eAAO,KAAK,mBAAmB,GAAG,aAAa,IAAI,MAAM,WAAW,OAAO,IAAI,IAAI,WAAW,CAAC,OAAO,KAAK,IAAI;AAC/G,cAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC;AAC7C;AAAA,MACF;AACA,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,kBAAY;AACZ,UAAI,WAAW,IAAI,YAAY,CAAC,wBAAwB,GAAG,EAAG,OAAM;AACpE,YAAM,QAAQ,KAAK,IAAI,IAAI,cAAc,KAAK,IAAI,GAAG,UAAU,CAAC,GAAG,IAAI,UAAU;AACjF,aAAO,KAAK,mBAAmB,GAAG,iCAAiC,OAAO,IAAI,IAAI,WAAW,CAAC,OAAO,KAAK,MAAM,EAAE,OAAO,OAAO,GAAG,EAAE,CAAC;AACtI,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,KAAK,CAAC;AAAA,IAC/C;AAAA,EACF;AAEA,QAAM,qBAAqB,QAAQ,YAAY,IAAI,MAAM,OAAO,SAAS,CAAC;AAC5E;","names":[]}
@@ -1,7 +1,3 @@
1
- import {
2
- logger
3
- } from "./chunk-KDHEUNRI.js";
4
-
5
1
  // src/i18n/index.ts
6
2
  import { create } from "zustand";
7
3
 
@@ -289,6 +285,7 @@ var en = {
289
285
  account_confirmDeactivate: "Deactivate your Pro license on this machine?",
290
286
  account_statusLabel: "Status:",
291
287
  account_pro: "[Pro]",
288
+ account_team: "[Team]",
292
289
  account_free: "[Free]",
293
290
  account_expired: "[Expired]",
294
291
  account_emailLabel: "Email:",
@@ -448,6 +445,7 @@ var en = {
448
445
  rollback_strategy_pin: "pin only (version not restorable)",
449
446
  rollback_strategy_unavailable: "cannot restore",
450
447
  rollback_executing: "Rolling back...",
448
+ rollback_executing_no_cancel: "Rollback in progress \u2014 cancellation is not safe and will be ignored.",
451
449
  rollback_success: "Rollback completed",
452
450
  rollback_error: "Rollback failed: {{error}}",
453
451
  rollback_item_downgrade: "{{name}}: {{from}} \u2192 {{to}}",
@@ -808,6 +806,7 @@ var es = {
808
806
  account_confirmDeactivate: "\xBFDesactivar tu licencia Pro en esta m\xE1quina?",
809
807
  account_statusLabel: "Estado:",
810
808
  account_pro: "[Pro]",
809
+ account_team: "[Team]",
811
810
  account_free: "[Gratis]",
812
811
  account_expired: "[Expirada]",
813
812
  account_emailLabel: "Email:",
@@ -968,6 +967,7 @@ var es = {
968
967
  rollback_strategy_pin: "solo fijar (versi\xF3n no restaurable)",
969
968
  rollback_strategy_unavailable: "no se puede restaurar",
970
969
  rollback_executing: "Revirtiendo...",
970
+ rollback_executing_no_cancel: "Rollback en curso \u2014 la cancelaci\xF3n no es segura y se ignorar\xE1.",
971
971
  rollback_success: "Rollback completado",
972
972
  rollback_error: "Rollback fallido: {{error}}",
973
973
  rollback_item_downgrade: "{{name}}: {{from}} \u2192 {{to}}",
@@ -1085,50 +1085,10 @@ function tp(baseKey, count, values) {
1085
1085
  return t(`${baseKey}${suffix}`, { count, ...values });
1086
1086
  }
1087
1087
 
1088
- // src/lib/fetch-timeout.ts
1089
- function fetchWithTimeout(url, options = {}, timeoutMs = 15e3) {
1090
- return fetch(url, { ...options, signal: AbortSignal.timeout(timeoutMs) });
1091
- }
1092
- var DEFAULT_RETRY = {
1093
- attempts: 3,
1094
- baseDelayMs: 500,
1095
- maxDelayMs: 4e3,
1096
- retryOn: (res) => res.status >= 500 && res.status < 600
1097
- };
1098
- function isTransientNetworkError(err) {
1099
- const msg = err instanceof Error ? err.message : String(err);
1100
- return /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network|timeout|abort|EAI_AGAIN/i.test(msg);
1101
- }
1102
- async function fetchWithRetry(url, options = {}, timeoutMs = 15e3, retry = {}) {
1103
- const cfg = { ...DEFAULT_RETRY, ...retry };
1104
- let lastError;
1105
- for (let attempt = 1; attempt <= cfg.attempts; attempt++) {
1106
- try {
1107
- const res = await fetchWithTimeout(url, options, timeoutMs);
1108
- if (attempt < cfg.attempts && cfg.retryOn?.(res)) {
1109
- const delay = Math.min(cfg.baseDelayMs * Math.pow(2, attempt - 1), cfg.maxDelayMs);
1110
- logger.warn(`fetchWithRetry: ${url} returned ${res.status}, retry ${attempt}/${cfg.attempts - 1} in ${delay}ms`);
1111
- await new Promise((r) => setTimeout(r, delay));
1112
- continue;
1113
- }
1114
- return res;
1115
- } catch (err) {
1116
- lastError = err;
1117
- if (attempt >= cfg.attempts || !isTransientNetworkError(err)) throw err;
1118
- const delay = Math.min(cfg.baseDelayMs * Math.pow(2, attempt - 1), cfg.maxDelayMs);
1119
- logger.warn(`fetchWithRetry: ${url} threw transient error, retry ${attempt}/${cfg.attempts - 1} in ${delay}ms`, { error: String(err) });
1120
- await new Promise((r) => setTimeout(r, delay));
1121
- }
1122
- }
1123
- throw lastError instanceof Error ? lastError : new Error(String(lastError));
1124
- }
1125
-
1126
1088
  export {
1127
1089
  useLocaleStore,
1128
1090
  getLocale,
1129
1091
  t,
1130
- tp,
1131
- fetchWithTimeout,
1132
- fetchWithRetry
1092
+ tp
1133
1093
  };
1134
- //# sourceMappingURL=chunk-WDRT6G63.js.map
1094
+ //# sourceMappingURL=chunk-WX7MPVPH.js.map