brew-tui 3.0.0 → 3.2.0

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.
@@ -6,7 +6,7 @@ import {
6
6
  launchBrewTUIBar,
7
7
  syncAndLaunchBrewTUIBar,
8
8
  uninstallBrewTUIBar
9
- } from "./chunk-Y3OFIGIC.js";
9
+ } from "./chunk-JMTQEDOT.js";
10
10
  import "./chunk-NRRQECXA.js";
11
11
  import "./chunk-A7U3NZYM.js";
12
12
  import "./chunk-KDHEUNRI.js";
@@ -19,4 +19,4 @@ export {
19
19
  syncAndLaunchBrewTUIBar,
20
20
  uninstallBrewTUIBar
21
21
  };
22
- //# sourceMappingURL=brew-tui-bar-installer-5JMU46HK.js.map
22
+ //# sourceMappingURL=brew-tui-bar-installer-XRMSUYEJ.js.map
@@ -12,7 +12,7 @@ import {
12
12
 
13
13
  // src/lib/license/license-manager.ts
14
14
  import { readFile, writeFile, rename, rm } from "fs/promises";
15
- import { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from "crypto";
15
+ import { createCipheriv, createDecipheriv, randomBytes, hkdfSync } from "crypto";
16
16
 
17
17
  // src/lib/license/polar-api.ts
18
18
  import { createHash } from "crypto";
@@ -26,7 +26,7 @@ function validateApiUrl(url) {
26
26
  if (parsed.protocol !== "https:") {
27
27
  throw new Error("HTTPS required for license API");
28
28
  }
29
- if (!parsed.hostname.endsWith("polar.sh")) {
29
+ if (parsed.hostname !== "polar.sh" && !parsed.hostname.endsWith(".polar.sh")) {
30
30
  throw new Error("Invalid API host");
31
31
  }
32
32
  }
@@ -166,8 +166,6 @@ function recordAttempt(success) {
166
166
  var ENCRYPTION_SECRET = "brew-tui-license-aes256gcm-v1";
167
167
  var HKDF_SALT = "brew-tui-salt-v1";
168
168
  var _derivedKey = null;
169
- var _legacyKey = null;
170
- var _decryptedWithLegacyKey = false;
171
169
  async function deriveEncryptionKey() {
172
170
  if (_derivedKey) return _derivedKey;
173
171
  const machineId = await getMachineId();
@@ -175,10 +173,6 @@ async function deriveEncryptionKey() {
175
173
  _derivedKey = Buffer.from(derived);
176
174
  return _derivedKey;
177
175
  }
178
- function deriveLegacyKey() {
179
- if (!_legacyKey) _legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32);
180
- return _legacyKey;
181
- }
182
176
  async function encryptLicenseData(data) {
183
177
  const key = await deriveEncryptionKey();
184
178
  const iv = randomBytes(12);
@@ -196,27 +190,15 @@ async function decryptLicenseData(encrypted, iv, tag) {
196
190
  const ivBuf = Buffer.from(iv, "base64");
197
191
  const tagBuf = Buffer.from(tag, "base64");
198
192
  const ciphertext = Buffer.from(encrypted, "base64");
199
- const candidates = [
200
- [await deriveEncryptionKey(), false],
201
- [deriveLegacyKey(), true]
202
- ];
203
- let lastErr;
204
- for (const [key, isLegacy] of candidates) {
205
- try {
206
- const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
207
- decipher.setAuthTag(tagBuf);
208
- const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
209
- const parsed = JSON.parse(plaintext.toString("utf-8"));
210
- if (!isLicenseData(parsed)) {
211
- throw new Error("Decrypted license payload failed shape validation");
212
- }
213
- _decryptedWithLegacyKey = isLegacy;
214
- return parsed;
215
- } catch (err) {
216
- lastErr = err;
217
- }
193
+ const key = await deriveEncryptionKey();
194
+ const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
195
+ decipher.setAuthTag(tagBuf);
196
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
197
+ const parsed = JSON.parse(plaintext.toString("utf-8"));
198
+ if (!isLicenseData(parsed)) {
199
+ throw new Error("Decrypted license payload failed shape validation");
218
200
  }
219
- throw lastErr instanceof Error ? lastErr : new Error("Failed to decrypt license");
201
+ return parsed;
220
202
  }
221
203
  function isLicenseFile(obj) {
222
204
  return typeof obj === "object" && obj !== null && obj.version === 1;
@@ -242,17 +224,10 @@ async function loadLicense() {
242
224
  const fileRecord = file;
243
225
  if (fileRecord.machineId) {
244
226
  const currentMachineId = await getMachineId();
245
- if (fileRecord.machineId !== currentMachineId) {
227
+ if (typeof fileRecord.machineId !== "string" || fileRecord.machineId.toLowerCase() !== currentMachineId.toLowerCase()) {
246
228
  throw new Error("License was activated on a different machine");
247
229
  }
248
230
  }
249
- if (_decryptedWithLegacyKey) {
250
- _decryptedWithLegacyKey = false;
251
- try {
252
- await saveLicense(data);
253
- } catch {
254
- }
255
- }
256
231
  return data;
257
232
  }
258
233
  if (file.license) {
@@ -300,7 +275,7 @@ function getDegradationLevel(license) {
300
275
  const lastValidated = new Date(license.lastValidatedAt).getTime();
301
276
  if (isNaN(lastValidated)) return "expired";
302
277
  const elapsed = Date.now() - lastValidated;
303
- if (elapsed < 0) return "none";
278
+ if (elapsed < 0) return "expired";
304
279
  const days = elapsed / (24 * 60 * 60 * 1e3);
305
280
  if (days <= 7) return "none";
306
281
  if (days <= 14) return "warning";
@@ -393,4 +368,4 @@ export {
393
368
  revalidate,
394
369
  deactivate
395
370
  };
396
- //# sourceMappingURL=chunk-KRVYMXTP.js.map
371
+ //# sourceMappingURL=chunk-4RPJM7O7.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, 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;\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\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 // Only the machine-bound HKDF key is accepted. The legacy bundle-only\n // scrypt fallback (in place from 0.6.3 through 2.x for license.json files\n // written by 0.6.2 and earlier) was retired in 3.1.0 — see SEG-M2 in the\n // security audit. Any envelope ciphered with the legacy key now fails\n // decryption, which loadLicense() surfaces as \"license not found\" so the\n // user re-activates.\n const key = await deriveEncryptionKey();\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 return parsed;\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 // SEC-L2: randomUUID() yields lowercase UUIDs, but a manually edited\n // machine-id or license.json could carry mixed case. Normalise both\n // sides before comparing so case alone is never the rejection reason.\n if (typeof fileRecord.machineId !== 'string'\n || fileRecord.machineId.toLowerCase() !== currentMachineId.toLowerCase()) {\n throw new Error('License was activated on a different machine');\n }\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 // SEC-L1: a negative elapsed means lastValidatedAt is in the future. This\n // is almost always a clock-skew exploit (set system clock forward to keep\n // Pro features forever). Fail-closed instead of granting full access. The\n // next revalidate() against Polar will reset things if the skew is benign.\n if (elapsed < 0) return 'expired';\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 // SEC-M1: `endsWith('polar.sh')` would let `evilpolar.sh` pass. Match the\n // exact apex OR a true subdomain via the leading-dot variant.\n if (parsed.hostname !== 'polar.sh' && !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":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadLicense
3
- } from "./chunk-KRVYMXTP.js";
3
+ } from "./chunk-4RPJM7O7.js";
4
4
  import {
5
5
  captureSnapshot
6
6
  } from "./chunk-OXDZ4DCK.js";
@@ -18,7 +18,7 @@ import { join as join2 } from "path";
18
18
  import { hostname } from "os";
19
19
 
20
20
  // src/lib/sync/crypto.ts
21
- import { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from "crypto";
21
+ import { createCipheriv, createDecipheriv, randomBytes, hkdfSync } from "crypto";
22
22
 
23
23
  // src/lib/sync/types.ts
24
24
  function isSyncPayload(value) {
@@ -39,7 +39,6 @@ function isSyncPayload(value) {
39
39
  var ENCRYPTION_SECRET = "brew-tui-sync-aes256gcm-v1";
40
40
  var HKDF_SALT = "brew-tui-sync-salt-v1";
41
41
  var keyCache = /* @__PURE__ */ new Map();
42
- var _legacyKey = null;
43
42
  function deriveEncryptionKey(licenseKey) {
44
43
  const cached = keyCache.get(licenseKey);
45
44
  if (cached) return cached;
@@ -47,12 +46,6 @@ function deriveEncryptionKey(licenseKey) {
47
46
  keyCache.set(licenseKey, derived);
48
47
  return derived;
49
48
  }
50
- function deriveLegacyKey() {
51
- if (!_legacyKey) {
52
- _legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32, { N: 16384, r: 8, p: 1 });
53
- }
54
- return _legacyKey;
55
- }
56
49
  function encryptPayload(data, licenseKey) {
57
50
  const key = deriveEncryptionKey(licenseKey);
58
51
  const iv = randomBytes(12);
@@ -70,18 +63,13 @@ function decryptPayload(encrypted, iv, tag, licenseKey) {
70
63
  const ivBuf = Buffer.from(iv, "base64");
71
64
  const tagBuf = Buffer.from(tag, "base64");
72
65
  const ciphertext = Buffer.from(encrypted, "base64");
73
- for (const key of [deriveEncryptionKey(licenseKey), deriveLegacyKey()]) {
74
- try {
75
- const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
76
- decipher.setAuthTag(tagBuf);
77
- const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
78
- const parsed = JSON.parse(plaintext.toString("utf-8"));
79
- if (!isSyncPayload(parsed)) throw new Error("Invalid sync payload shape");
80
- return parsed;
81
- } catch {
82
- }
83
- }
84
- throw new Error("Failed to decrypt sync payload");
66
+ const key = deriveEncryptionKey(licenseKey);
67
+ const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
68
+ decipher.setAuthTag(tagBuf);
69
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
70
+ const parsed = JSON.parse(plaintext.toString("utf-8"));
71
+ if (!isSyncPayload(parsed)) throw new Error("Invalid sync payload shape");
72
+ return parsed;
85
73
  }
86
74
 
87
75
  // src/lib/sync/backends/icloud-backend.ts
@@ -339,4 +327,4 @@ export {
339
327
  sync,
340
328
  applyConflictResolutions
341
329
  };
342
- //# sourceMappingURL=chunk-WBYR6CIO.js.map
330
+ //# sourceMappingURL=chunk-D6DGYMXM.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"],"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, 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>();\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\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 // Only the licenseKey-bound HKDF key is accepted. The legacy bundle-only\n // scrypt fallback (for envelopes written by 0.6.2 and earlier) was retired\n // in 3.1.0 — see SEG-M3 in the security audit.\n const key = deriveEncryptionKey(licenseKey);\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}\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 Brew-TUI-Bar 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"],"mappings":"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","names":["readFile","writeFile","rename","join","join","readFile","writeFile","rename"]}
@@ -6,7 +6,7 @@ import {
6
6
  loadLicense,
7
7
  needsRevalidation,
8
8
  revalidate
9
- } from "./chunk-KRVYMXTP.js";
9
+ } from "./chunk-4RPJM7O7.js";
10
10
  import {
11
11
  ensureDataDirs
12
12
  } from "./chunk-LFGDNAXH.js";
@@ -135,4 +135,4 @@ export {
135
135
  verifyStoreIntegrity,
136
136
  useLicenseStore
137
137
  };
138
- //# sourceMappingURL=chunk-J3O7LVP7.js.map
138
+ //# sourceMappingURL=chunk-FQV2F47X.js.map
@@ -186,7 +186,7 @@ async function launchBrewTUIBar() {
186
186
  }
187
187
  async function syncAndLaunchBrewTUIBar() {
188
188
  if (process.platform !== "darwin") return;
189
- const { checkBrewTUIBarVersion } = await import("./version-check-NMOGXFF6.js");
189
+ const { checkBrewTUIBarVersion } = await import("./version-check-4G7O7AIM.js");
190
190
  try {
191
191
  if (!await isBrewTUIBarInstalled()) {
192
192
  console.log(t("cli_brewtuibarInstalling"));
@@ -225,4 +225,4 @@ export {
225
225
  syncAndLaunchBrewTUIBar,
226
226
  uninstallBrewTUIBar
227
227
  };
228
- //# sourceMappingURL=chunk-Y3OFIGIC.js.map
228
+ //# sourceMappingURL=chunk-JMTQEDOT.js.map
@@ -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 "3.0.0";
9
+ return "3.2.0";
10
10
  }
11
11
  async function readBrewTUIBarVersion() {
12
12
  try {
@@ -62,4 +62,4 @@ export {
62
62
  compareSemver,
63
63
  checkBrewTUIBarVersion
64
64
  };
65
- //# sourceMappingURL=chunk-X3TEZUER.js.map
65
+ //# sourceMappingURL=chunk-OSBWA63A.js.map
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  checkBrewTUIBarVersion,
3
3
  readBrewTUIBarVersion
4
- } from "./chunk-X3TEZUER.js";
4
+ } from "./chunk-OSBWA63A.js";
5
5
  import {
6
6
  useLicenseStore
7
- } from "./chunk-J3O7LVP7.js";
7
+ } from "./chunk-FQV2F47X.js";
8
8
  import {
9
9
  bundleIdAt,
10
10
  isBrewTUIBarInstalled,
11
11
  isBrewTUIBarRunning
12
- } from "./chunk-Y3OFIGIC.js";
13
- import "./chunk-KRVYMXTP.js";
12
+ } from "./chunk-JMTQEDOT.js";
13
+ import "./chunk-4RPJM7O7.js";
14
14
  import "./chunk-NRRQECXA.js";
15
15
  import "./chunk-A7U3NZYM.js";
16
16
  import "./chunk-KDHEUNRI.js";
@@ -53,7 +53,7 @@ async function findBrewBinary() {
53
53
  }
54
54
  }
55
55
  async function runDoctor() {
56
- const cliVersion = "3.0.0";
56
+ const cliVersion = "3.2.0";
57
57
  console.log(format("Brew-TUI", [
58
58
  { label: "CLI version", value: cliVersion },
59
59
  { label: "Platform", value: `${process.platform} (${arch()})` },
@@ -130,4 +130,4 @@ async function runDoctor() {
130
130
  export {
131
131
  runDoctor
132
132
  };
133
- //# sourceMappingURL=doctor-3PQZV7OV.js.map
133
+ //# sourceMappingURL=doctor-QVD4L2HN.js.map
package/build/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  useLicenseStore,
3
3
  verifyStoreIntegrity
4
- } from "./chunk-J3O7LVP7.js";
4
+ } from "./chunk-FQV2F47X.js";
5
5
  import {
6
6
  brewUpdate,
7
7
  casksToListItems,
@@ -35,14 +35,14 @@ import {
35
35
  loadSyncConfig,
36
36
  readSyncEnvelope,
37
37
  sync
38
- } from "./chunk-WBYR6CIO.js";
38
+ } from "./chunk-D6DGYMXM.js";
39
39
  import {
40
40
  activate,
41
41
  deactivate,
42
42
  getDegradationLevel,
43
43
  loadLicense,
44
44
  revalidate
45
- } from "./chunk-KRVYMXTP.js";
45
+ } from "./chunk-4RPJM7O7.js";
46
46
  import {
47
47
  fetchWithRetry
48
48
  } from "./chunk-NRRQECXA.js";
@@ -4550,7 +4550,7 @@ function AccountView() {
4550
4550
  status === "pro" || status === "team" || status === "expired" ? `v ${t("hint_revalidate")} ` : "",
4551
4551
  revalidating ? t("account_revalidating") : "",
4552
4552
  " ",
4553
- t("app_version", { version: "3.0.0" })
4553
+ t("app_version", { version: "3.2.0" })
4554
4554
  ] }) })
4555
4555
  ] });
4556
4556
  }
@@ -6058,7 +6058,7 @@ async function reportError(err, context = {}) {
6058
6058
  const config = await resolveConfig();
6059
6059
  if (!config.enabled || !config.endpoint) return;
6060
6060
  const machineId = await getMachineId();
6061
- const version = true ? "3.0.0" : "unknown";
6061
+ const version = true ? "3.2.0" : "unknown";
6062
6062
  await postReport(buildReport("error", err, context, machineId, version), config);
6063
6063
  }
6064
6064
  async function installCrashReporter() {
@@ -6067,7 +6067,7 @@ async function installCrashReporter() {
6067
6067
  if (!config.enabled || !config.endpoint) return;
6068
6068
  _installed = true;
6069
6069
  const machineId = await getMachineId();
6070
- const version = true ? "3.0.0" : "unknown";
6070
+ const version = true ? "3.2.0" : "unknown";
6071
6071
  process.on("uncaughtException", (err) => {
6072
6072
  void postReport(buildReport("fatal", err, { kind: "uncaughtException" }, machineId, version), config);
6073
6073
  });
@@ -6082,11 +6082,11 @@ import { jsx as jsx39 } from "react/jsx-runtime";
6082
6082
  var [, , command, arg] = process.argv;
6083
6083
  async function runCli() {
6084
6084
  if (command === "--version" || command === "-v" || command === "version") {
6085
- const cliVersion = "3.0.0";
6085
+ const cliVersion = "3.2.0";
6086
6086
  process.stdout.write(cliVersion + "\n");
6087
6087
  if (process.platform === "darwin") {
6088
6088
  try {
6089
- const { readBrewTUIBarVersion } = await import("./version-check-NMOGXFF6.js");
6089
+ const { readBrewTUIBarVersion } = await import("./version-check-4G7O7AIM.js");
6090
6090
  const appVersion = await readBrewTUIBarVersion();
6091
6091
  if (appVersion && appVersion !== cliVersion) {
6092
6092
  process.stderr.write(t("cli_versionMismatchWarning", { installed: appVersion, expected: cliVersion }) + "\n");
@@ -6210,7 +6210,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6210
6210
  } catch {
6211
6211
  }
6212
6212
  try {
6213
- const { loadSyncConfig: loadSyncConfig2 } = await import("./sync-engine-6OL6VOVB.js");
6213
+ const { loadSyncConfig: loadSyncConfig2 } = await import("./sync-engine-37NCGPBS.js");
6214
6214
  const syncConfig = await loadSyncConfig2();
6215
6215
  if (syncConfig?.lastSync) {
6216
6216
  console.log(`Sync: last sync ${formatDate(syncConfig.lastSync)}`);
@@ -6231,7 +6231,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6231
6231
  return;
6232
6232
  }
6233
6233
  if (command === "install-brew-tui-bar") {
6234
- const { installBrewTUIBar } = await import("./brew-tui-bar-installer-5JMU46HK.js");
6234
+ const { installBrewTUIBar } = await import("./brew-tui-bar-installer-XRMSUYEJ.js");
6235
6235
  try {
6236
6236
  console.log(t("cli_brewtuibarInstalling"));
6237
6237
  await installBrewTUIBar(false, arg === "--force");
@@ -6243,7 +6243,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6243
6243
  return;
6244
6244
  }
6245
6245
  if (command === "uninstall-brew-tui-bar") {
6246
- const { uninstallBrewTUIBar } = await import("./brew-tui-bar-installer-5JMU46HK.js");
6246
+ const { uninstallBrewTUIBar } = await import("./brew-tui-bar-installer-XRMSUYEJ.js");
6247
6247
  try {
6248
6248
  await uninstallBrewTUIBar();
6249
6249
  console.log(t("cli_brewtuibarUninstalled"));
@@ -6254,7 +6254,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6254
6254
  return;
6255
6255
  }
6256
6256
  if (command === "doctor") {
6257
- const { runDoctor } = await import("./doctor-3PQZV7OV.js");
6257
+ const { runDoctor } = await import("./doctor-QVD4L2HN.js");
6258
6258
  await runDoctor();
6259
6259
  return;
6260
6260
  }
@@ -6278,7 +6278,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6278
6278
  async function ensureBrewTUIBarRunning() {
6279
6279
  if (process.platform !== "darwin") return;
6280
6280
  await useLicenseStore.getState().initialize();
6281
- const { syncAndLaunchBrewTUIBar } = await import("./brew-tui-bar-installer-5JMU46HK.js");
6281
+ const { syncAndLaunchBrewTUIBar } = await import("./brew-tui-bar-installer-XRMSUYEJ.js");
6282
6282
  await syncAndLaunchBrewTUIBar();
6283
6283
  }
6284
6284
  runCli().catch((err) => {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  syncAndLaunchBrewTUIBar
4
- } from "./chunk-Y3OFIGIC.js";
4
+ } from "./chunk-JMTQEDOT.js";
5
5
  import "./chunk-NRRQECXA.js";
6
6
  import {
7
7
  t
@@ -3,8 +3,8 @@ import {
3
3
  loadSyncConfig,
4
4
  saveSyncConfig,
5
5
  sync
6
- } from "./chunk-WBYR6CIO.js";
7
- import "./chunk-KRVYMXTP.js";
6
+ } from "./chunk-D6DGYMXM.js";
7
+ import "./chunk-4RPJM7O7.js";
8
8
  import "./chunk-NRRQECXA.js";
9
9
  import "./chunk-A7U3NZYM.js";
10
10
  import "./chunk-OXDZ4DCK.js";
@@ -19,4 +19,4 @@ export {
19
19
  saveSyncConfig,
20
20
  sync
21
21
  };
22
- //# sourceMappingURL=sync-engine-6OL6VOVB.js.map
22
+ //# sourceMappingURL=sync-engine-37NCGPBS.js.map
@@ -4,7 +4,7 @@ import {
4
4
  compareSemver,
5
5
  expectedVersion,
6
6
  readBrewTUIBarVersion
7
- } from "./chunk-X3TEZUER.js";
7
+ } from "./chunk-OSBWA63A.js";
8
8
  export {
9
9
  CONTRACT_VERSION,
10
10
  checkBrewTUIBarVersion,
@@ -12,4 +12,4 @@ export {
12
12
  expectedVersion,
13
13
  readBrewTUIBarVersion
14
14
  };
15
- //# sourceMappingURL=version-check-NMOGXFF6.js.map
15
+ //# sourceMappingURL=version-check-4G7O7AIM.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brew-tui",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "Brew-TUI — Visual TUI for Homebrew package management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,11 +26,12 @@
26
26
  "dev": "tsx src/index.tsx",
27
27
  "build": "tsup",
28
28
  "test": "vitest run",
29
+ "test:coverage": "vitest run --coverage --no-file-parallelism",
29
30
  "test:watch": "vitest",
30
31
  "test:serial": "vitest run --no-file-parallelism",
31
32
  "lint": "eslint src/",
32
33
  "typecheck": "tsc --noEmit",
33
- "validate": "npm run typecheck && npm run test:serial && npm run build && npm run lint",
34
+ "validate": "npm run typecheck && npm run test:coverage && npm run build && npm run lint",
34
35
  "check:brewtuibar-release": "node scripts/check-brewtuibar-release.mjs",
35
36
  "prepublishOnly": "npm run validate && npm run check:brewtuibar-release",
36
37
  "postinstall": "node build/postinstall.js",
@@ -61,6 +62,7 @@
61
62
  "@types/react": "^19.2.14",
62
63
  "@typescript-eslint/eslint-plugin": "^8.58.0",
63
64
  "@typescript-eslint/parser": "^8.58.0",
65
+ "@vitest/coverage-v8": "4.1.5",
64
66
  "eslint": "^10.2.1",
65
67
  "husky": "9.1.7",
66
68
  "ink-testing-library": "^4.0.0",
@@ -68,6 +70,6 @@
68
70
  "tsup": "^8.4.0",
69
71
  "tsx": "^4.19.0",
70
72
  "typescript": "~6.0.3",
71
- "vitest": "^4.1.5"
73
+ "vitest": "4.1.5"
72
74
  }
73
75
  }
@@ -1 +0,0 @@
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":[]}
@@ -1 +0,0 @@
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"],"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 Brew-TUI-Bar 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"],"mappings":"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","names":["readFile","writeFile","rename","join","join","readFile","writeFile","rename"]}