brew-tui 2.2.2 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/{brew-tui-bar-installer-GTV4OEZW.js → brew-tui-bar-installer-PCHNYMZL.js} +5 -3
- package/build/{brewfile-manager-HWTPBXPO.js → brewfile-manager-DNRM6CQ7.js} +3 -3
- package/build/chunk-7R4ME2NC.js +342 -0
- package/build/chunk-7R4ME2NC.js.map +1 -0
- package/build/chunk-CCAT52XY.js +138 -0
- package/build/chunk-CCAT52XY.js.map +1 -0
- package/build/{chunk-YUE5NRTE.js → chunk-I5VZR55J.js} +23 -3
- package/build/chunk-I5VZR55J.js.map +1 -0
- package/build/{version-check-QY3SQ6XI.js → chunk-KR6EAHEE.js} +6 -5
- package/build/{chunk-ZC23DNMK.js → chunk-PYDQHHI2.js} +22 -354
- package/build/chunk-PYDQHHI2.js.map +1 -0
- package/build/{chunk-F2S7TGCS.js → chunk-SDQYHY2L.js} +3 -1
- package/build/chunk-SDQYHY2L.js.map +1 -0
- package/build/{chunk-Y45AXONF.js → chunk-UZMGXQKF.js} +2 -2
- package/build/doctor-D56LDODR.js +133 -0
- package/build/doctor-D56LDODR.js.map +1 -0
- package/build/index.js +60 -162
- package/build/index.js.map +1 -1
- package/build/postinstall.js +10 -5
- package/build/postinstall.js.map +1 -1
- package/build/{sync-engine-G5ML7TJ5.js → sync-engine-KTH4K3NG.js} +4 -3
- package/build/version-check-UUJMLUK6.js +15 -0
- package/build/version-check-UUJMLUK6.js.map +1 -0
- package/package.json +1 -1
- package/build/chunk-F2S7TGCS.js.map +0 -1
- package/build/chunk-YUE5NRTE.js.map +0 -1
- package/build/chunk-ZC23DNMK.js.map +0 -1
- /package/build/{brew-tui-bar-installer-GTV4OEZW.js.map → brew-tui-bar-installer-PCHNYMZL.js.map} +0 -0
- /package/build/{brewfile-manager-HWTPBXPO.js.map → brewfile-manager-DNRM6CQ7.js.map} +0 -0
- /package/build/{version-check-QY3SQ6XI.js.map → chunk-KR6EAHEE.js.map} +0 -0
- /package/build/{chunk-Y45AXONF.js.map → chunk-UZMGXQKF.js.map} +0 -0
- /package/build/{sync-engine-G5ML7TJ5.js.map → sync-engine-KTH4K3NG.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/brew-tui-bar-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 BREWTUIBAR_APP_PATH = '/Applications/Brew-TUI-Bar.app';\nconst BREWTUIBAR_BUNDLE_ID = 'com.molinesdesigns.brewtuibar';\nconst BREWTUIBAR_PROCESS_NAME = 'Brew-TUI-Bar';\nconst LEGACY_APP_PATH = '/Applications/BrewBar.app';\nconst LEGACY_BUNDLE_ID = 'com.molinesdesigns.brewbar';\nconst LEGACY_PROCESS_NAME = 'BrewBar';\nconst DOWNLOAD_URL = 'https://github.com/MoLinesDesigns/Brew-TUI/releases/latest/download/Brew-TUI-Bar.app.zip';\nconst MAX_SIZE = 200 * 1024 * 1024; // 200 MB\n\nexport async function isBrewTUIBarInstalled(): Promise<boolean> {\n try {\n await access(BREWTUIBAR_APP_PATH);\n return true;\n } catch {\n return false;\n }\n}\n\n/// Reads CFBundleIdentifier of an installed .app bundle. Used to detect when\n/// another app has claimed a path we care about (e.g. a third-party clone at\n/// /Applications/Brew-TUI-Bar.app, or a foreign app sitting at the legacy\n/// /Applications/BrewBar.app path). Exported so `doctor` can surface the\n/// installed bundle ID in its diagnostic dump.\nexport async function bundleIdAt(appPath: string): Promise<string | null> {\n if (process.platform !== 'darwin') return null;\n try {\n const { stdout } = await execFileAsync('defaults', [\n 'read',\n `${appPath}/Contents/Info.plist`,\n 'CFBundleIdentifier',\n ]);\n return stdout.trim();\n } catch {\n return null;\n }\n}\n\nasync function installedBundleId(): Promise<string | null> {\n return bundleIdAt(BREWTUIBAR_APP_PATH);\n}\n\n/// Cleans up the legacy BrewBar.app bundle if present and owned by us. The\n/// cask transitional path handles this for `brew upgrade` users; this covers\n/// `brew-tui install-brew-tui-bar` and the npm cold-start auto-install. We\n/// only touch the bundle when its CFBundleIdentifier matches the legacy ID,\n/// so a foreign app at the same path is left alone.\nasync function removeLegacyBundleIfOurs(): Promise<void> {\n if (process.platform !== 'darwin') return;\n try {\n await access(LEGACY_APP_PATH);\n } catch {\n return;\n }\n\n const legacyId = await bundleIdAt(LEGACY_APP_PATH);\n if (legacyId !== LEGACY_BUNDLE_ID) return; // not ours, leave it\n\n // Quit the legacy process if it's running, then remove the bundle.\n try {\n const { stdout } = await execFileAsync('pgrep', ['-x', LEGACY_PROCESS_NAME]);\n if (stdout.trim().length > 0) {\n try {\n await execFileAsync('osascript', ['-e', `tell application \"${LEGACY_PROCESS_NAME}\" to quit`]);\n } catch { /* fall through to pkill */ }\n for (let i = 0; i < 15; i++) {\n try {\n const { stdout: s } = await execFileAsync('pgrep', ['-x', LEGACY_PROCESS_NAME]);\n if (s.trim().length === 0) break;\n } catch {\n break;\n }\n await new Promise((r) => setTimeout(r, 200));\n }\n try {\n await execFileAsync('pkill', ['-x', LEGACY_PROCESS_NAME]);\n } catch { /* nothing to kill */ }\n }\n } catch {\n /* pgrep exits 1 when no match — legacy app not running */\n }\n\n await rm(LEGACY_APP_PATH, { recursive: true, force: true });\n}\n\n/// Returns true if the Brew-TUI-Bar process is currently running.\n/// Used by the installer to decide whether to quit + relaunch after an update.\nexport async function isBrewTUIBarRunning(): Promise<boolean> {\n if (process.platform !== 'darwin') return false;\n try {\n const { stdout } = await execFileAsync('pgrep', ['-x', BREWTUIBAR_PROCESS_NAME]);\n return stdout.trim().length > 0;\n } catch {\n // pgrep exits 1 when no match; that means \"not running\", not a failure.\n return false;\n }\n}\n\n/// Asks Brew-TUI-Bar to quit gracefully (LSUIElement → no dialogs), then falls back\n/// to pkill if it hasn't exited within 3 s. Required before reemplazar el bundle:\n/// `ditto -xk` sobre una app en ejecución deja un bundle viejo con un Info.plist\n/// nuevo, lo cual confunde el monitor de last-action y los watchers FSEvents.\nasync function quitBrewTUIBar(): Promise<void> {\n if (process.platform !== 'darwin') return;\n try {\n await execFileAsync('osascript', ['-e', `tell application \"${BREWTUIBAR_PROCESS_NAME}\" to quit`]);\n } catch {\n /* osascript falla si la app no está registrada; pasamos a pkill */\n }\n for (let i = 0; i < 15; i++) {\n if (!(await isBrewTUIBarRunning())) return;\n await new Promise((r) => setTimeout(r, 200));\n }\n try {\n await execFileAsync('pkill', ['-x', BREWTUIBAR_PROCESS_NAME]);\n } catch {\n /* nada que matar */\n }\n}\n\n/// Install Brew-TUI-Bar. As of 2.1.0 we no longer gate on Pro: Free users get\n/// the bundle too and see the in-app upgrade prompt when they click the menu\n/// bar icon. The `_isPro` parameter is kept for backwards compatibility with\n/// existing call sites but is ignored.\nexport async function installBrewTUIBar(_isPro: boolean, force = false): Promise<void> {\n // macOS only\n if (process.platform !== 'darwin') {\n throw new Error(t('cli_brewtuibarMacOnly'));\n }\n\n // If an app already exists at our install path, verify it's ours before\n // we touch it. Defends against name collisions with third-party clones.\n if (await isBrewTUIBarInstalled()) {\n const id = await installedBundleId();\n if (id && id !== BREWTUIBAR_BUNDLE_ID) {\n throw new Error(t('cli_brewtuibarForeignBundle', { id }));\n }\n if (!force) {\n throw new Error(t('cli_brewtuibarAlreadyInstalled'));\n }\n }\n\n // EP-013: Use unique temp path\n const TMP_ZIP = join(tmpdir(), 'Brew-TUI-Bar-' + 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_brewtuibarDownloadFailed', { 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_brewtuibarDownloadFailed', { error: 'Download exceeds 200 MB size limit' }));\n }\n\n // EP-005: Track downloaded bytes during the stream\n let downloadedBytes = 0;\n\n // Progress reporting: TTYs get an in-place \\r-updated line; non-TTYs (e.g.\n // brew install captures stdout into a log) get a chunked one-line-per-25%\n // report so the log stays readable. Either way, callers see the install\n // is progressing, not hanging.\n const isTTY = process.stdout.isTTY ?? false;\n let lastReportedQuarter = -1;\n\n function reportProgress(): void {\n const totalMB = contentLength > 0 ? (contentLength / 1024 / 1024).toFixed(1) : null;\n const doneMB = (downloadedBytes / 1024 / 1024).toFixed(1);\n const pct = contentLength > 0 ? Math.floor((downloadedBytes / contentLength) * 100) : -1;\n\n if (isTTY) {\n // Pad to 60 chars so the previous (possibly longer) line is fully erased.\n const line = totalMB\n ? ` ${doneMB} MB / ${totalMB} MB (${pct}%)`\n : ` ${doneMB} MB`;\n process.stdout.write('\\r' + line.padEnd(60, ' '));\n } else if (pct >= 0) {\n const quarter = Math.floor(pct / 25);\n if (quarter > lastReportedQuarter) {\n lastReportedQuarter = quarter;\n console.log(` ${pct}% (${doneMB} MB / ${totalMB} MB)`);\n }\n }\n }\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 reportProgress();\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 // Newline so the next log line ('✔ installed…') starts clean after the \\r line.\n if (isTTY) process.stdout.write('\\n');\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_brewtuibarDownloadFailed', { 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_brewtuibarDownloadFailed', { error: 'SHA-256 checksum unavailable — cannot verify download integrity' }));\n }\n\n // Si Brew-TUI-Bar está corriendo, cerrarla antes de tocar el bundle. Sin esto\n // `ditto -xk` sobreescribe los recursos de un proceso vivo y la app queda\n // en estado degradado hasta el próximo lanzamiento.\n const wasRunning = await isBrewTUIBarRunning();\n if (wasRunning) {\n await quitBrewTUIBar();\n }\n\n // Clean up the legacy BrewBar.app bundle if it's ours. The cask transitional\n // path handles this on the brew upgrade side; this covers npm and cold-start.\n await removeLegacyBundleIfOurs();\n\n // Remove old app if force reinstall\n if (force && await isBrewTUIBarInstalled()) {\n await rm(BREWTUIBAR_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_brewtuibarDownloadFailed', { 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 // Si estaba corriendo antes de la actualización, relanzarla para que el\n // usuario vuelva a ver el ícono en la menubar sin pasos manuales.\n if (wasRunning) {\n await launchBrewTUIBar();\n }\n}\n\n/// Launches Brew-TUI-Bar 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 launchBrewTUIBar(): Promise<void> {\n if (process.platform !== 'darwin') return;\n if (!await isBrewTUIBarInstalled()) return;\n try {\n await execFileAsync('open', ['-g', '-a', BREWTUIBAR_APP_PATH]);\n } catch {\n // Non-fatal: may already be running, or LaunchServices may need a moment.\n }\n}\n\n/// One-shot \"install if missing, update if outdated, launch\" flow shared by\n/// the CLI cold-start (`ensureBrewTUIBarRunning`) and the npm postinstall.\n/// All errors are swallowed and logged as warnings — callers should never\n/// have their install/launch fail just because the menu bar app is unhappy.\nexport async function syncAndLaunchBrewTUIBar(): Promise<void> {\n if (process.platform !== 'darwin') return;\n\n const { checkBrewTUIBarVersion } = await import('./version-check.js');\n\n try {\n if (!(await isBrewTUIBarInstalled())) {\n console.log(t('cli_brewtuibarInstalling'));\n await installBrewTUIBar(false, false);\n console.log(t('cli_brewtuibarInstalled'));\n } else {\n // Reinstall in place when the installed bundle is older than the CLI.\n // Same contract enforced by `checkBrewTUIBarVersion`, so the menubar\n // app and CLI always agree on the license/IPC schema.\n const status = await checkBrewTUIBarVersion();\n if (status.kind === 'outdated') {\n console.log(t('cli_brewtuibarUpdating', { installed: status.installed, expected: status.expected }));\n await installBrewTUIBar(false, true);\n console.log(t('cli_brewtuibarInstalled'));\n }\n }\n await launchBrewTUIBar();\n } catch (err) {\n console.warn(t('cli_brewtuibarAutoFailed', { error: err instanceof Error ? err.message : String(err) }));\n }\n}\n\nexport async function uninstallBrewTUIBar(): Promise<void> {\n if (!await isBrewTUIBarInstalled()) {\n throw new Error(t('cli_brewtuibarNotInstalled'));\n }\n // Refuse to delete a foreign app that happens to live at the same path.\n const id = await installedBundleId();\n if (id && id !== BREWTUIBAR_BUNDLE_ID) {\n throw new Error(t('cli_brewtuibarForeignBundle', { id }));\n }\n\n await rm(BREWTUIBAR_APP_PATH, { recursive: true, force: true });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -6,7 +6,7 @@ var execFileAsync = promisify(execFile);
|
|
|
6
6
|
var BREWTUIBAR_INFO_PLIST = "/Applications/Brew-TUI-Bar.app/Contents/Info.plist";
|
|
7
7
|
var CONTRACT_VERSION = 1;
|
|
8
8
|
function expectedVersion() {
|
|
9
|
-
return "2.
|
|
9
|
+
return "2.3.1";
|
|
10
10
|
}
|
|
11
11
|
async function readBrewTUIBarVersion() {
|
|
12
12
|
try {
|
|
@@ -54,11 +54,12 @@ async function checkBrewTUIBarVersion() {
|
|
|
54
54
|
if (cmp < 0) return { kind: "outdated", installed, expected };
|
|
55
55
|
return { kind: "newer", installed, expected };
|
|
56
56
|
}
|
|
57
|
+
|
|
57
58
|
export {
|
|
58
59
|
CONTRACT_VERSION,
|
|
59
|
-
checkBrewTUIBarVersion,
|
|
60
|
-
compareSemver,
|
|
61
60
|
expectedVersion,
|
|
62
|
-
readBrewTUIBarVersion
|
|
61
|
+
readBrewTUIBarVersion,
|
|
62
|
+
compareSemver,
|
|
63
|
+
checkBrewTUIBarVersion
|
|
63
64
|
};
|
|
64
|
-
//# sourceMappingURL=
|
|
65
|
+
//# sourceMappingURL=chunk-KR6EAHEE.js.map
|
|
@@ -3,163 +3,16 @@ import {
|
|
|
3
3
|
} from "./chunk-NRRQECXA.js";
|
|
4
4
|
import {
|
|
5
5
|
t
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-SDQYHY2L.js";
|
|
7
7
|
import {
|
|
8
|
-
captureSnapshot
|
|
9
|
-
} from "./chunk-OXDZ4DCK.js";
|
|
10
|
-
import {
|
|
11
|
-
logger
|
|
12
|
-
} from "./chunk-KDHEUNRI.js";
|
|
13
|
-
import {
|
|
14
|
-
DATA_DIR,
|
|
15
8
|
LICENSE_PATH,
|
|
16
9
|
ensureDataDirs,
|
|
17
10
|
getMachineId
|
|
18
11
|
} from "./chunk-LFGDNAXH.js";
|
|
19
12
|
|
|
20
|
-
// src/lib/sync/sync-engine.ts
|
|
21
|
-
import { readFile as readFile3, writeFile as writeFile3, rename as rename3 } from "fs/promises";
|
|
22
|
-
import { join as join2 } from "path";
|
|
23
|
-
import { hostname } from "os";
|
|
24
|
-
|
|
25
|
-
// src/lib/sync/crypto.ts
|
|
26
|
-
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from "crypto";
|
|
27
|
-
|
|
28
|
-
// src/lib/sync/types.ts
|
|
29
|
-
function isSyncPayload(value) {
|
|
30
|
-
if (typeof value !== "object" || value === null) return false;
|
|
31
|
-
const machines = value.machines;
|
|
32
|
-
if (typeof machines !== "object" || machines === null || Array.isArray(machines)) return false;
|
|
33
|
-
for (const m of Object.values(machines)) {
|
|
34
|
-
if (typeof m !== "object" || m === null) return false;
|
|
35
|
-
const state = m;
|
|
36
|
-
if (typeof state.machineId !== "string" || typeof state.machineName !== "string" || typeof state.updatedAt !== "string" || typeof state.snapshot !== "object") {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// src/lib/sync/crypto.ts
|
|
44
|
-
var ENCRYPTION_SECRET = "brew-tui-sync-aes256gcm-v1";
|
|
45
|
-
var HKDF_SALT = "brew-tui-sync-salt-v1";
|
|
46
|
-
var keyCache = /* @__PURE__ */ new Map();
|
|
47
|
-
var _legacyKey = null;
|
|
48
|
-
function deriveEncryptionKey(licenseKey) {
|
|
49
|
-
const cached = keyCache.get(licenseKey);
|
|
50
|
-
if (cached) return cached;
|
|
51
|
-
const derived = Buffer.from(hkdfSync("sha256", ENCRYPTION_SECRET, HKDF_SALT, licenseKey, 32));
|
|
52
|
-
keyCache.set(licenseKey, derived);
|
|
53
|
-
return derived;
|
|
54
|
-
}
|
|
55
|
-
function deriveLegacyKey() {
|
|
56
|
-
if (!_legacyKey) {
|
|
57
|
-
_legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32, { N: 16384, r: 8, p: 1 });
|
|
58
|
-
}
|
|
59
|
-
return _legacyKey;
|
|
60
|
-
}
|
|
61
|
-
function encryptPayload(data, licenseKey) {
|
|
62
|
-
const key = deriveEncryptionKey(licenseKey);
|
|
63
|
-
const iv = randomBytes(12);
|
|
64
|
-
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
65
|
-
const plaintext = JSON.stringify(data);
|
|
66
|
-
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
67
|
-
const tag = cipher.getAuthTag();
|
|
68
|
-
return {
|
|
69
|
-
encrypted: ciphertext.toString("base64"),
|
|
70
|
-
iv: iv.toString("base64"),
|
|
71
|
-
tag: tag.toString("base64")
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
function decryptPayload(encrypted, iv, tag, licenseKey) {
|
|
75
|
-
const ivBuf = Buffer.from(iv, "base64");
|
|
76
|
-
const tagBuf = Buffer.from(tag, "base64");
|
|
77
|
-
const ciphertext = Buffer.from(encrypted, "base64");
|
|
78
|
-
for (const key of [deriveEncryptionKey(licenseKey), deriveLegacyKey()]) {
|
|
79
|
-
try {
|
|
80
|
-
const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
|
|
81
|
-
decipher.setAuthTag(tagBuf);
|
|
82
|
-
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
83
|
-
const parsed = JSON.parse(plaintext.toString("utf-8"));
|
|
84
|
-
if (!isSyncPayload(parsed)) throw new Error("Invalid sync payload shape");
|
|
85
|
-
return parsed;
|
|
86
|
-
} catch {
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
throw new Error("Failed to decrypt sync payload");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// src/lib/sync/backends/icloud-backend.ts
|
|
93
|
-
import { readFile, writeFile, rename, mkdir, stat } from "fs/promises";
|
|
94
|
-
import { homedir } from "os";
|
|
95
|
-
import { join } from "path";
|
|
96
|
-
var ICLOUD_BASE = join(
|
|
97
|
-
homedir(),
|
|
98
|
-
"Library",
|
|
99
|
-
"Mobile Documents",
|
|
100
|
-
"com~apple~CloudDocs"
|
|
101
|
-
);
|
|
102
|
-
var ICLOUD_SYNC_DIR = join(ICLOUD_BASE, "BrewTUI");
|
|
103
|
-
var ICLOUD_SYNC_PATH = join(ICLOUD_SYNC_DIR, "sync.json");
|
|
104
|
-
async function isICloudAvailable() {
|
|
105
|
-
try {
|
|
106
|
-
await stat(ICLOUD_BASE);
|
|
107
|
-
return true;
|
|
108
|
-
} catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function isValidEnvelope(v) {
|
|
113
|
-
if (!v || typeof v !== "object") return false;
|
|
114
|
-
const obj = v;
|
|
115
|
-
return obj["schemaVersion"] === 1 && typeof obj["encrypted"] === "string" && typeof obj["iv"] === "string" && typeof obj["tag"] === "string" && typeof obj["updatedAt"] === "string";
|
|
116
|
-
}
|
|
117
|
-
async function readSyncEnvelope() {
|
|
118
|
-
try {
|
|
119
|
-
const info = await stat(ICLOUD_SYNC_PATH);
|
|
120
|
-
if (info.size === 0) {
|
|
121
|
-
logger.warn("sync: iCloud envelope exists but is empty (placeholder?)");
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
} catch (err) {
|
|
125
|
-
if (err instanceof Error && err.code === "ENOENT") {
|
|
126
|
-
try {
|
|
127
|
-
const placeholder = ICLOUD_SYNC_PATH.replace(/sync\.json$/, ".sync.json.icloud");
|
|
128
|
-
await stat(placeholder);
|
|
129
|
-
logger.warn("sync: iCloud placeholder present, file not yet downloaded");
|
|
130
|
-
} catch {
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
logger.warn("sync: could not stat iCloud envelope", { error: String(err) });
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const raw = await readFile(ICLOUD_SYNC_PATH, "utf-8");
|
|
139
|
-
const parsed = JSON.parse(raw);
|
|
140
|
-
if (!isValidEnvelope(parsed)) {
|
|
141
|
-
logger.warn("sync: invalid envelope structure in iCloud file");
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
return parsed;
|
|
145
|
-
} catch (err) {
|
|
146
|
-
logger.warn("sync: could not read iCloud envelope", { error: String(err) });
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
async function writeSyncEnvelope(envelope) {
|
|
151
|
-
await mkdir(ICLOUD_SYNC_DIR, { recursive: true, mode: 448 });
|
|
152
|
-
const tmpPath = ICLOUD_SYNC_PATH + ".tmp";
|
|
153
|
-
await writeFile(tmpPath, JSON.stringify(envelope, null, 2), {
|
|
154
|
-
encoding: "utf-8",
|
|
155
|
-
mode: 384
|
|
156
|
-
});
|
|
157
|
-
await rename(tmpPath, ICLOUD_SYNC_PATH);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
13
|
// src/lib/license/license-manager.ts
|
|
161
|
-
import { readFile
|
|
162
|
-
import { createCipheriv
|
|
14
|
+
import { readFile, writeFile, rename, rm } from "fs/promises";
|
|
15
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from "crypto";
|
|
163
16
|
|
|
164
17
|
// src/lib/license/polar-api.ts
|
|
165
18
|
import { createHash } from "crypto";
|
|
@@ -310,26 +163,26 @@ function recordAttempt(success) {
|
|
|
310
163
|
tracker.attempts = 0;
|
|
311
164
|
}
|
|
312
165
|
}
|
|
313
|
-
var
|
|
314
|
-
var
|
|
166
|
+
var ENCRYPTION_SECRET = "brew-tui-license-aes256gcm-v1";
|
|
167
|
+
var HKDF_SALT = "brew-tui-salt-v1";
|
|
315
168
|
var _derivedKey = null;
|
|
316
|
-
var
|
|
169
|
+
var _legacyKey = null;
|
|
317
170
|
var _decryptedWithLegacyKey = false;
|
|
318
|
-
async function
|
|
171
|
+
async function deriveEncryptionKey() {
|
|
319
172
|
if (_derivedKey) return _derivedKey;
|
|
320
173
|
const machineId = await getMachineId();
|
|
321
|
-
const derived =
|
|
174
|
+
const derived = hkdfSync("sha256", ENCRYPTION_SECRET, HKDF_SALT, machineId, 32);
|
|
322
175
|
_derivedKey = Buffer.from(derived);
|
|
323
176
|
return _derivedKey;
|
|
324
177
|
}
|
|
325
|
-
function
|
|
326
|
-
if (!
|
|
327
|
-
return
|
|
178
|
+
function deriveLegacyKey() {
|
|
179
|
+
if (!_legacyKey) _legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32);
|
|
180
|
+
return _legacyKey;
|
|
328
181
|
}
|
|
329
182
|
async function encryptLicenseData(data) {
|
|
330
|
-
const key = await
|
|
331
|
-
const iv =
|
|
332
|
-
const cipher =
|
|
183
|
+
const key = await deriveEncryptionKey();
|
|
184
|
+
const iv = randomBytes(12);
|
|
185
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
333
186
|
const plaintext = JSON.stringify(data);
|
|
334
187
|
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
335
188
|
const tag = cipher.getAuthTag();
|
|
@@ -344,13 +197,13 @@ async function decryptLicenseData(encrypted, iv, tag) {
|
|
|
344
197
|
const tagBuf = Buffer.from(tag, "base64");
|
|
345
198
|
const ciphertext = Buffer.from(encrypted, "base64");
|
|
346
199
|
const candidates = [
|
|
347
|
-
[await
|
|
348
|
-
[
|
|
200
|
+
[await deriveEncryptionKey(), false],
|
|
201
|
+
[deriveLegacyKey(), true]
|
|
349
202
|
];
|
|
350
203
|
let lastErr;
|
|
351
204
|
for (const [key, isLegacy] of candidates) {
|
|
352
205
|
try {
|
|
353
|
-
const decipher =
|
|
206
|
+
const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
|
|
354
207
|
decipher.setAuthTag(tagBuf);
|
|
355
208
|
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
356
209
|
const parsed = JSON.parse(plaintext.toString("utf-8"));
|
|
@@ -375,7 +228,7 @@ function isEncryptedLicenseFile(obj) {
|
|
|
375
228
|
}
|
|
376
229
|
async function loadLicense() {
|
|
377
230
|
try {
|
|
378
|
-
const raw = await
|
|
231
|
+
const raw = await readFile(LICENSE_PATH, "utf-8");
|
|
379
232
|
const parsed = JSON.parse(raw);
|
|
380
233
|
if (!isLicenseFile(parsed)) {
|
|
381
234
|
throw new Error("Invalid license data format");
|
|
@@ -418,8 +271,8 @@ async function saveLicense(data) {
|
|
|
418
271
|
const machineId = await getMachineId();
|
|
419
272
|
const file = { version: 1, encrypted, iv, tag, machineId };
|
|
420
273
|
const tmpPath = LICENSE_PATH + ".tmp";
|
|
421
|
-
await
|
|
422
|
-
await
|
|
274
|
+
await writeFile(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
|
|
275
|
+
await rename(tmpPath, LICENSE_PATH);
|
|
423
276
|
}
|
|
424
277
|
async function clearLicense() {
|
|
425
278
|
try {
|
|
@@ -531,185 +384,6 @@ async function deactivate(license) {
|
|
|
531
384
|
return { remoteSuccess };
|
|
532
385
|
}
|
|
533
386
|
|
|
534
|
-
// src/lib/sync/sync-engine.ts
|
|
535
|
-
var SYNC_CONFIG_PATH = join2(DATA_DIR, "sync-config.json");
|
|
536
|
-
async function loadSyncConfig() {
|
|
537
|
-
try {
|
|
538
|
-
const raw = await readFile3(SYNC_CONFIG_PATH, "utf-8");
|
|
539
|
-
return JSON.parse(raw);
|
|
540
|
-
} catch {
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
async function saveSyncConfig(config) {
|
|
545
|
-
const tmpPath = SYNC_CONFIG_PATH + ".tmp";
|
|
546
|
-
await writeFile3(tmpPath, JSON.stringify(config, null, 2), {
|
|
547
|
-
encoding: "utf-8",
|
|
548
|
-
mode: 384
|
|
549
|
-
});
|
|
550
|
-
await rename3(tmpPath, SYNC_CONFIG_PATH);
|
|
551
|
-
}
|
|
552
|
-
function detectConflicts(localSnapshot, otherMachines, localMachineId) {
|
|
553
|
-
const conflicts = [];
|
|
554
|
-
const localFormulaMap = new Map(localSnapshot.formulae.map((f) => [f.name, f.version]));
|
|
555
|
-
const localCaskMap = new Map(localSnapshot.casks.map((c) => [c.name, c.version]));
|
|
556
|
-
for (const machine of otherMachines) {
|
|
557
|
-
if (machine.machineId === localMachineId) continue;
|
|
558
|
-
for (const remoteFormula of machine.snapshot.formulae) {
|
|
559
|
-
const localVersion = localFormulaMap.get(remoteFormula.name);
|
|
560
|
-
if (localVersion !== void 0 && localVersion !== remoteFormula.version) {
|
|
561
|
-
conflicts.push({
|
|
562
|
-
packageName: remoteFormula.name,
|
|
563
|
-
packageType: "formula",
|
|
564
|
-
localVersion,
|
|
565
|
-
remoteMachine: machine.machineName,
|
|
566
|
-
remoteVersion: remoteFormula.version
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
for (const remoteCask of machine.snapshot.casks) {
|
|
571
|
-
const localVersion = localCaskMap.get(remoteCask.name);
|
|
572
|
-
if (localVersion !== void 0 && localVersion !== remoteCask.version) {
|
|
573
|
-
conflicts.push({
|
|
574
|
-
packageName: remoteCask.name,
|
|
575
|
-
packageType: "cask",
|
|
576
|
-
localVersion,
|
|
577
|
-
remoteMachine: machine.machineName,
|
|
578
|
-
remoteVersion: remoteCask.version
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
return conflicts;
|
|
584
|
-
}
|
|
585
|
-
async function writeEnvelope(payload, licenseKey) {
|
|
586
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
587
|
-
const { encrypted, iv, tag } = encryptPayload(payload, licenseKey);
|
|
588
|
-
const envelope = {
|
|
589
|
-
schemaVersion: 1,
|
|
590
|
-
encrypted,
|
|
591
|
-
iv,
|
|
592
|
-
tag,
|
|
593
|
-
updatedAt: now
|
|
594
|
-
};
|
|
595
|
-
await writeSyncEnvelope(envelope);
|
|
596
|
-
return now;
|
|
597
|
-
}
|
|
598
|
-
async function loadLicenseKeyOrThrow() {
|
|
599
|
-
const license = await loadLicense();
|
|
600
|
-
if (!license || !license.key) {
|
|
601
|
-
throw new Error("Sync requires an active license");
|
|
602
|
-
}
|
|
603
|
-
return license.key;
|
|
604
|
-
}
|
|
605
|
-
function mergePayload(existing, localState) {
|
|
606
|
-
return {
|
|
607
|
-
machines: {
|
|
608
|
-
...existing.machines,
|
|
609
|
-
[localState.machineId]: localState
|
|
610
|
-
}
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
async function sync(isPro, currentBrewfile) {
|
|
614
|
-
if (!isPro) {
|
|
615
|
-
throw new Error("Pro license required");
|
|
616
|
-
}
|
|
617
|
-
const available = await isICloudAvailable();
|
|
618
|
-
if (!available) {
|
|
619
|
-
return {
|
|
620
|
-
success: false,
|
|
621
|
-
conflicts: [],
|
|
622
|
-
resolvedCount: 0,
|
|
623
|
-
error: "iCloud Drive not available"
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
const licenseKey = await loadLicenseKeyOrThrow();
|
|
627
|
-
let existingPayload = null;
|
|
628
|
-
try {
|
|
629
|
-
const envelope = await readSyncEnvelope();
|
|
630
|
-
if (envelope) {
|
|
631
|
-
existingPayload = decryptPayload(envelope.encrypted, envelope.iv, envelope.tag, licenseKey);
|
|
632
|
-
}
|
|
633
|
-
} catch (err) {
|
|
634
|
-
logger.warn("sync: could not decrypt existing payload, starting fresh", { error: String(err) });
|
|
635
|
-
existingPayload = null;
|
|
636
|
-
}
|
|
637
|
-
const snapshot = await captureSnapshot();
|
|
638
|
-
const machineId = await getMachineId();
|
|
639
|
-
const machineName = hostname();
|
|
640
|
-
const localState = {
|
|
641
|
-
machineId,
|
|
642
|
-
machineName,
|
|
643
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
644
|
-
snapshot,
|
|
645
|
-
...currentBrewfile ? { brewfile: currentBrewfile } : {}
|
|
646
|
-
};
|
|
647
|
-
const otherMachines = existingPayload ? Object.values(existingPayload.machines).filter((m) => m.machineId !== machineId) : [];
|
|
648
|
-
const conflicts = detectConflicts(snapshot, otherMachines, machineId);
|
|
649
|
-
const basePayload = existingPayload ?? { machines: {} };
|
|
650
|
-
const mergedPayload = mergePayload(basePayload, localState);
|
|
651
|
-
if (conflicts.length > 0) {
|
|
652
|
-
await writeEnvelope(mergedPayload, licenseKey);
|
|
653
|
-
return {
|
|
654
|
-
success: false,
|
|
655
|
-
conflicts,
|
|
656
|
-
resolvedCount: 0
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
const now = await writeEnvelope(mergedPayload, licenseKey);
|
|
660
|
-
const existingConfig = await loadSyncConfig();
|
|
661
|
-
await saveSyncConfig({
|
|
662
|
-
enabled: true,
|
|
663
|
-
machineId,
|
|
664
|
-
machineName,
|
|
665
|
-
...existingConfig ?? {},
|
|
666
|
-
lastSync: now
|
|
667
|
-
});
|
|
668
|
-
logger.info("sync: completed successfully", { machineId, machines: Object.keys(mergedPayload.machines).length });
|
|
669
|
-
return {
|
|
670
|
-
success: true,
|
|
671
|
-
conflicts: [],
|
|
672
|
-
resolvedCount: 0
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
async function applyConflictResolutions(payload, resolutions, localMachineId) {
|
|
676
|
-
const updatedPayload = {
|
|
677
|
-
machines: { ...payload.machines }
|
|
678
|
-
};
|
|
679
|
-
for (const { conflict, resolution } of resolutions) {
|
|
680
|
-
if (resolution !== "use-remote") continue;
|
|
681
|
-
const localMachine = updatedPayload.machines[localMachineId];
|
|
682
|
-
if (!localMachine) {
|
|
683
|
-
logger.warn("sync: cannot apply resolution, local machine missing in payload", { localMachineId });
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
if (conflict.packageType === "formula") {
|
|
687
|
-
updatedPayload.machines[localMachineId] = {
|
|
688
|
-
...localMachine,
|
|
689
|
-
snapshot: {
|
|
690
|
-
...localMachine.snapshot,
|
|
691
|
-
formulae: localMachine.snapshot.formulae.map(
|
|
692
|
-
(f) => f.name === conflict.packageName ? { ...f, version: conflict.remoteVersion } : f
|
|
693
|
-
)
|
|
694
|
-
}
|
|
695
|
-
};
|
|
696
|
-
} else {
|
|
697
|
-
updatedPayload.machines[localMachineId] = {
|
|
698
|
-
...localMachine,
|
|
699
|
-
snapshot: {
|
|
700
|
-
...localMachine.snapshot,
|
|
701
|
-
casks: localMachine.snapshot.casks.map(
|
|
702
|
-
(c) => c.name === conflict.packageName ? { ...c, version: conflict.remoteVersion } : c
|
|
703
|
-
)
|
|
704
|
-
}
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
const licenseKey = await loadLicenseKeyOrThrow();
|
|
709
|
-
await writeEnvelope(updatedPayload, licenseKey);
|
|
710
|
-
logger.info("sync: conflict resolutions applied", { count: resolutions.length });
|
|
711
|
-
}
|
|
712
|
-
|
|
713
387
|
export {
|
|
714
388
|
loadLicense,
|
|
715
389
|
isExpired,
|
|
@@ -717,12 +391,6 @@ export {
|
|
|
717
391
|
getDegradationLevel,
|
|
718
392
|
activate,
|
|
719
393
|
revalidate,
|
|
720
|
-
deactivate
|
|
721
|
-
decryptPayload,
|
|
722
|
-
readSyncEnvelope,
|
|
723
|
-
loadSyncConfig,
|
|
724
|
-
saveSyncConfig,
|
|
725
|
-
sync,
|
|
726
|
-
applyConflictResolutions
|
|
394
|
+
deactivate
|
|
727
395
|
};
|
|
728
|
-
//# sourceMappingURL=chunk-
|
|
396
|
+
//# sourceMappingURL=chunk-PYDQHHI2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/license/license-manager.ts","../src/lib/license/polar-api.ts","../src/lib/license/types.ts"],"sourcesContent":["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// Brew-TUI-Bar 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":[]}
|
|
@@ -378,6 +378,7 @@ var en = {
|
|
|
378
378
|
cli_brewtuibarForeignBundle: "\u2718 /Applications/Brew-TUI-Bar.app exists but its bundle ID is `{{id}}`, not com.molinesdesigns.brewtuibar. Refusing to touch a foreign app. Remove or rename it first.",
|
|
379
379
|
postinstall_skipped: "Note: Brew-TUI-Bar auto-install skipped: {{error}}",
|
|
380
380
|
postinstall_manualHint: "You can install it manually later with: brew-tui install-brew-tui-bar",
|
|
381
|
+
cli_versionMismatchWarning: "\u26A0 Brew-TUI-Bar {{installed}} is out of sync with this CLI ({{expected}}). It will be updated automatically the next time you run `brew-tui` or restart the app.",
|
|
381
382
|
cli_deactivateRemoteFailed: "\u26A0 Warning: Could not reach the server to deactivate remotely. The license was removed locally but may still count as active.",
|
|
382
383
|
// ── License degradation (Layer 15) ──
|
|
383
384
|
license_offlineWarning: "Your license has not been validated for {{days}} days. Please connect to the internet.",
|
|
@@ -901,6 +902,7 @@ var es = {
|
|
|
901
902
|
cli_brewtuibarForeignBundle: "\u2718 /Applications/Brew-TUI-Bar.app existe pero su bundle ID es `{{id}}`, no com.molinesdesigns.brewtuibar. No se tocar\xE1 una app ajena. Elim\xEDnala o ren\xF3mbrala primero.",
|
|
902
903
|
postinstall_skipped: "Nota: instalaci\xF3n autom\xE1tica de Brew-TUI-Bar saltada: {{error}}",
|
|
903
904
|
postinstall_manualHint: "Puedes instalarla manualmente m\xE1s tarde con: brew-tui install-brew-tui-bar",
|
|
905
|
+
cli_versionMismatchWarning: "\u26A0 Brew-TUI-Bar {{installed}} no coincide con esta CLI ({{expected}}). Se actualizar\xE1 autom\xE1ticamente la pr\xF3xima vez que ejecutes `brew-tui` o reinicies la app.",
|
|
904
906
|
cli_deactivateRemoteFailed: "\u26A0 Advertencia: No se pudo contactar al servidor para desactivar remotamente. La licencia se elimin\xF3 localmente pero puede seguir contando como activa.",
|
|
905
907
|
// ── License degradation (Layer 15) ──
|
|
906
908
|
license_offlineWarning: "Tu licencia no se ha validado en {{days}} d\xEDas. Por favor con\xE9ctate a internet.",
|
|
@@ -1095,4 +1097,4 @@ export {
|
|
|
1095
1097
|
t,
|
|
1096
1098
|
tp
|
|
1097
1099
|
};
|
|
1098
|
-
//# sourceMappingURL=chunk-
|
|
1100
|
+
//# sourceMappingURL=chunk-SDQYHY2L.js.map
|