brew-tui 3.3.2 → 4.0.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-SY3CDC6Q.js";
9
+ } from "./chunk-X5I6BCKV.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-ZEILWDFQ.js.map
22
+ //# sourceMappingURL=brew-tui-bar-installer-4SARQO7K.js.map
@@ -6,7 +6,7 @@ import {
6
6
  loadLicense,
7
7
  needsRevalidation,
8
8
  revalidate
9
- } from "./chunk-4RPJM7O7.js";
9
+ } from "./chunk-TA3XPHF4.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-FQV2F47X.js.map
138
+ //# sourceMappingURL=chunk-ILKWT7WR.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadLicense
3
- } from "./chunk-4RPJM7O7.js";
3
+ } from "./chunk-TA3XPHF4.js";
4
4
  import {
5
5
  captureSnapshot
6
6
  } from "./chunk-OXDZ4DCK.js";
@@ -327,4 +327,4 @@ export {
327
327
  sync,
328
328
  applyConflictResolutions
329
329
  };
330
- //# sourceMappingURL=chunk-D6DGYMXM.js.map
330
+ //# sourceMappingURL=chunk-Q4PGJ4H2.js.map
@@ -0,0 +1,284 @@
1
+ import {
2
+ fetchWithRetry
3
+ } from "./chunk-NRRQECXA.js";
4
+ import {
5
+ t
6
+ } from "./chunk-A7U3NZYM.js";
7
+ import {
8
+ LICENSE_PATH,
9
+ ensureDataDirs,
10
+ getMachineId
11
+ } from "./chunk-LFGDNAXH.js";
12
+
13
+ // src/lib/license/license-manager.ts
14
+ import { readFile, writeFile, rename, rm } from "fs/promises";
15
+ import { verify, createPublicKey } from "crypto";
16
+
17
+ // src/lib/license/polar-api.ts
18
+ var BASE_URL = "https://api.molinesdesigns.com/api/license";
19
+ function validateApiUrl(url) {
20
+ const parsed = new URL(url);
21
+ if (parsed.protocol !== "https:") {
22
+ throw new Error("HTTPS required for license API");
23
+ }
24
+ if (parsed.hostname !== "molinesdesigns.com" && !parsed.hostname.endsWith(".molinesdesigns.com")) {
25
+ throw new Error("Invalid API host");
26
+ }
27
+ }
28
+ async function post(endpoint, body, expectEmpty = false) {
29
+ const url = `${BASE_URL}/${endpoint}`;
30
+ validateApiUrl(url);
31
+ const res = await fetchWithRetry(url, {
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json" },
34
+ body: JSON.stringify(body)
35
+ }, 15e3);
36
+ if (!res.ok) {
37
+ let message = `Request failed with status ${res.status}`;
38
+ try {
39
+ const errBody = await res.json();
40
+ if (typeof errBody.error === "string") message = errBody.error;
41
+ } catch {
42
+ }
43
+ throw new Error(message);
44
+ }
45
+ if (expectEmpty || res.status === 204) return void 0;
46
+ const wrapped = await res.json();
47
+ if (!wrapped.success || wrapped.data === void 0) {
48
+ throw new Error(wrapped.error ?? "Backend response missing data");
49
+ }
50
+ return wrapped.data;
51
+ }
52
+ function assertSigned(value) {
53
+ if (typeof value !== "object" || value === null) {
54
+ throw new Error("Invalid signed license: not an object");
55
+ }
56
+ const v = value;
57
+ if (typeof v.sig !== "string" || v.sig.length === 0) {
58
+ throw new Error("Invalid signed license: missing signature");
59
+ }
60
+ if (typeof v.license !== "object" || v.license === null) {
61
+ throw new Error("Invalid signed license: missing license payload");
62
+ }
63
+ }
64
+ async function activateLicense(key, machineId) {
65
+ const signed = await post("activate", { key, machineId });
66
+ assertSigned(signed);
67
+ return signed;
68
+ }
69
+ async function validateLicense(key, instanceId) {
70
+ const signed = await post("validate", { key, instanceId });
71
+ assertSigned(signed);
72
+ return signed;
73
+ }
74
+ async function deactivateLicense(key, instanceId) {
75
+ await post("deactivate", { key, instanceId });
76
+ }
77
+
78
+ // src/lib/license/types.ts
79
+ function isLicenseData(value) {
80
+ if (typeof value !== "object" || value === null) return false;
81
+ const v = value;
82
+ return typeof v.key === "string" && typeof v.instanceId === "string" && (v.status === "active" || v.status === "expired" || v.status === "inactive") && typeof v.customerEmail === "string" && typeof v.customerName === "string" && (v.plan === "pro" || v.plan === "team") && typeof v.activatedAt === "string" && (v.expiresAt === null || typeof v.expiresAt === "string") && typeof v.lastValidatedAt === "string";
83
+ }
84
+
85
+ // src/lib/license/license-manager.ts
86
+ var REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
87
+ var GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
88
+ var ACTIVATION_COOLDOWN_MS = 3e4;
89
+ var MAX_ATTEMPTS = 5;
90
+ var LOCKOUT_MS = 15 * 60 * 1e3;
91
+ var tracker = {
92
+ attempts: 0,
93
+ lastAttempt: 0,
94
+ lockedUntil: 0
95
+ };
96
+ function checkRateLimit() {
97
+ const now = Date.now();
98
+ if (now < tracker.lockedUntil) {
99
+ const remaining = Math.ceil((tracker.lockedUntil - now) / 6e4);
100
+ throw new Error(t("cli_rateLimited", { minutes: remaining }));
101
+ }
102
+ if (now - tracker.lastAttempt < ACTIVATION_COOLDOWN_MS) {
103
+ throw new Error(t("cli_cooldown"));
104
+ }
105
+ }
106
+ function recordAttempt(success) {
107
+ const now = Date.now();
108
+ tracker.lastAttempt = now;
109
+ if (success) {
110
+ tracker.attempts = 0;
111
+ return;
112
+ }
113
+ tracker.attempts++;
114
+ if (tracker.attempts >= MAX_ATTEMPTS) {
115
+ tracker.lockedUntil = now + LOCKOUT_MS;
116
+ tracker.attempts = 0;
117
+ }
118
+ }
119
+ var LICENSE_PUBLIC_KEY_B64 = "oHtzyU7ZACt8Eqga+U4PSagr0rSj1YLs3oVSpmjmwq0=";
120
+ var _cachedPublicKey = null;
121
+ function publicKey() {
122
+ if (_cachedPublicKey) return _cachedPublicKey;
123
+ const spkiPrefix = Buffer.from("302a300506032b6570032100", "hex");
124
+ const rawPub = Buffer.from(LICENSE_PUBLIC_KEY_B64, "base64");
125
+ const spki = Buffer.concat([spkiPrefix, rawPub]);
126
+ _cachedPublicKey = createPublicKey({ key: spki, format: "der", type: "spki" });
127
+ return _cachedPublicKey;
128
+ }
129
+ function canonicalJSON(value) {
130
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
131
+ if (Array.isArray(value)) {
132
+ return "[" + value.map(canonicalJSON).join(",") + "]";
133
+ }
134
+ const keys = Object.keys(value).sort();
135
+ const parts = keys.map(
136
+ (k) => JSON.stringify(k) + ":" + canonicalJSON(value[k])
137
+ );
138
+ return "{" + parts.join(",") + "}";
139
+ }
140
+ function verifySignedLicense(signed) {
141
+ try {
142
+ const sig = Buffer.from(signed.sig, "base64");
143
+ if (sig.length !== 64) return false;
144
+ const message = Buffer.from(canonicalJSON(signed.license), "utf8");
145
+ return verify(null, message, publicKey(), sig);
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+ function isLicenseFile(obj) {
151
+ if (typeof obj !== "object" || obj === null) return false;
152
+ const v = obj;
153
+ return v.version === 1 || v.version === 2;
154
+ }
155
+ async function loadLicense() {
156
+ try {
157
+ const raw = await readFile(LICENSE_PATH, "utf-8");
158
+ const parsed = JSON.parse(raw);
159
+ if (!isLicenseFile(parsed)) return null;
160
+ const file = parsed;
161
+ if (file.version === 2) {
162
+ if (!file.license || typeof file.sig !== "string") return null;
163
+ if (!isLicenseData(file.license)) return null;
164
+ const signed = { license: file.license, sig: file.sig };
165
+ if (!verifySignedLicense(signed)) return null;
166
+ return file.license;
167
+ }
168
+ if (file.version === 1) {
169
+ return null;
170
+ }
171
+ return null;
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+ async function persistSigned(signed) {
177
+ await ensureDataDirs();
178
+ const file = {
179
+ version: 2,
180
+ license: signed.license,
181
+ sig: signed.sig
182
+ };
183
+ const tmpPath = LICENSE_PATH + ".tmp";
184
+ await writeFile(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
185
+ await rename(tmpPath, LICENSE_PATH);
186
+ }
187
+ async function clearLicense() {
188
+ try {
189
+ await rm(LICENSE_PATH);
190
+ } catch {
191
+ }
192
+ }
193
+ function isExpired(license) {
194
+ if (!license.expiresAt) return false;
195
+ const expiry = new Date(license.expiresAt).getTime();
196
+ if (isNaN(expiry)) return true;
197
+ return expiry < Date.now();
198
+ }
199
+ function needsRevalidation(license) {
200
+ const lastValidated = new Date(license.lastValidatedAt).getTime();
201
+ if (isNaN(lastValidated)) return true;
202
+ return Date.now() - lastValidated > REVALIDATION_INTERVAL_MS;
203
+ }
204
+ function isWithinGracePeriod(license) {
205
+ const lastValidated = new Date(license.lastValidatedAt).getTime();
206
+ if (isNaN(lastValidated)) return false;
207
+ return Date.now() - lastValidated < GRACE_PERIOD_MS;
208
+ }
209
+ function getDegradationLevel(license) {
210
+ const lastValidated = new Date(license.lastValidatedAt).getTime();
211
+ if (isNaN(lastValidated)) return "expired";
212
+ const elapsed = Date.now() - lastValidated;
213
+ if (elapsed < 0) return "expired";
214
+ const days = elapsed / (24 * 60 * 60 * 1e3);
215
+ if (days <= 7) return "none";
216
+ if (days <= 14) return "warning";
217
+ if (days <= 30) return "limited";
218
+ return "expired";
219
+ }
220
+ function validateLicenseKey(key) {
221
+ if (key.length < 10 || key.length > 100) {
222
+ throw new Error("Invalid license key format");
223
+ }
224
+ if (!/^[\w-]+$/.test(key)) {
225
+ throw new Error("Invalid license key format");
226
+ }
227
+ }
228
+ async function activate(key) {
229
+ validateLicenseKey(key);
230
+ checkRateLimit();
231
+ let success = false;
232
+ try {
233
+ const machineId = await getMachineId();
234
+ const signed = await activateLicense(key, machineId);
235
+ if (!verifySignedLicense(signed)) {
236
+ throw new Error("Backend response failed signature verification");
237
+ }
238
+ await persistSigned(signed);
239
+ success = true;
240
+ return signed.license;
241
+ } finally {
242
+ recordAttempt(success);
243
+ }
244
+ }
245
+ function isNetworkError(err) {
246
+ const msg = err instanceof Error ? err.message : String(err);
247
+ return /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network|timeout|abort/i.test(msg);
248
+ }
249
+ async function revalidate(license) {
250
+ try {
251
+ const signed = await validateLicense(license.key, license.instanceId);
252
+ if (!verifySignedLicense(signed)) {
253
+ return "expired";
254
+ }
255
+ await persistSigned(signed);
256
+ return signed.license.status === "active" ? "valid" : "expired";
257
+ } catch (err) {
258
+ if (isNetworkError(err)) {
259
+ return isWithinGracePeriod(license) ? "grace" : "expired";
260
+ }
261
+ return "expired";
262
+ }
263
+ }
264
+ async function deactivate(license) {
265
+ let remoteSuccess = false;
266
+ try {
267
+ await deactivateLicense(license.key, license.instanceId);
268
+ remoteSuccess = true;
269
+ } catch {
270
+ }
271
+ await clearLicense();
272
+ return { remoteSuccess };
273
+ }
274
+
275
+ export {
276
+ loadLicense,
277
+ isExpired,
278
+ needsRevalidation,
279
+ getDegradationLevel,
280
+ activate,
281
+ revalidate,
282
+ deactivate
283
+ };
284
+ //# sourceMappingURL=chunk-TA3XPHF4.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 { verify, createPublicKey } from 'node:crypto';\nimport { LICENSE_PATH, ensureDataDirs, getMachineId } from '../data-dir.js';\nimport {\n activateLicense as apiActivate,\n validateLicense as apiValidate,\n deactivateLicense as apiDeactivate,\n type SignedLicense,\n} 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-009 v2): the signing key lives only on the brewtui-api NAS\n// (LICENSE_SIGNING_PRIVATE_KEY env var). The Ed25519 public counterpart is\n// embedded here AND in menubar/Brew-TUI-Bar/Sources/Services/LicenseChecker.swift —\n// both verify the signed envelope offline without round-tripping the network.\n// Exposing the public key is by design: a verifier needs it; forging signatures\n// without the private half is computationally infeasible.\n//\n// Cross-check vector for keeping JS and Swift in sync lives in\n// signature-cross-check.test.ts. Don't change the constant without rotating\n// the key on the backend AND bumping the schema version.\nconst LICENSE_PUBLIC_KEY_B64 = 'oHtzyU7ZACt8Eqga+U4PSagr0rSj1YLs3oVSpmjmwq0=';\n\nlet _cachedPublicKey: ReturnType<typeof createPublicKey> | null = null;\nfunction publicKey(): ReturnType<typeof createPublicKey> {\n if (_cachedPublicKey) return _cachedPublicKey;\n // SPKI wrapper for raw Ed25519 public keys (RFC 8410 §4):\n // SEQUENCE { AlgorithmIdentifier { 1.3.101.112 }, BIT STRING { rawKey } }\n // 12-byte prefix + 32 raw bytes = 44 bytes total. Same prefix the backend\n // uses in lib/signer.js to expose publicKeyBase64().\n const spkiPrefix = Buffer.from('302a300506032b6570032100', 'hex');\n const rawPub = Buffer.from(LICENSE_PUBLIC_KEY_B64, 'base64');\n const spki = Buffer.concat([spkiPrefix, rawPub]);\n _cachedPublicKey = createPublicKey({ key: spki, format: 'der', type: 'spki' });\n return _cachedPublicKey;\n}\n\n/**\n * Deterministic JSON serialisation: object keys sorted recursively, no\n * whitespace, JSON.stringify for primitives. Both the backend signer\n * (backend/lib/signer.js) and the Swift verifier (LicenseChecker.swift)\n * implement the same algorithm — that's how the bytes-signed match the\n * bytes-verified across three languages.\n */\nexport function canonicalJSON(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) {\n return '[' + value.map(canonicalJSON).join(',') + ']';\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const parts = keys.map((k) =>\n JSON.stringify(k) + ':' + canonicalJSON((value as Record<string, unknown>)[k]),\n );\n return '{' + parts.join(',') + '}';\n}\n\n/**\n * Crypto-verify the envelope returned by the backend. Returns false on any\n * failure — including malformed base64, wrong-length signatures or invalid\n * canonical encoding — so the caller has a single boolean to gate Pro access.\n */\nexport function verifySignedLicense(signed: SignedLicense): boolean {\n try {\n const sig = Buffer.from(signed.sig, 'base64');\n if (sig.length !== 64) return false;\n const message = Buffer.from(canonicalJSON(signed.license), 'utf8');\n return verify(null, message, publicKey(), sig);\n } catch {\n return false;\n }\n}\n\n// BK-003: Type guard for license data format\nfunction isLicenseFile(obj: unknown): obj is LicenseFile {\n if (typeof obj !== 'object' || obj === null) return false;\n const v = obj as Record<string, unknown>;\n return v.version === 1 || v.version === 2;\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 if (!isLicenseFile(parsed)) return null;\n const file = parsed as LicenseFile;\n\n // v2: signed envelope. The only path that grants Pro since 4.0.0.\n if (file.version === 2) {\n if (!file.license || typeof file.sig !== 'string') return null;\n if (!isLicenseData(file.license)) return null;\n const signed: SignedLicense = { license: file.license, sig: file.sig };\n if (!verifySignedLicense(signed)) return null;\n return file.license;\n }\n\n // v1: legacy AES-GCM envelope or unencrypted blob. Both are rejected —\n // the symmetric encryption key was shipped in the public bundle, so\n // accepting v1 would defeat the point of the signature migration. The\n // user just needs to run `brew-tui revalidate` once; activate() / the\n // periodic revalidation will overwrite the file with a v2 envelope on\n // the next successful round-trip to the backend.\n if (file.version === 1) {\n return null;\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Persists the signed envelope returned by the backend. Replaces the old\n * client-side AES-GCM saveLicense — the client no longer needs (or has) any\n * key material; it just writes the bytes the backend signed.\n *\n * Atomic write via tmp + rename: a crash mid-write leaves either the old\n * file intact or the new one fully written, never a torn JSON.\n */\nasync function persistSigned(signed: SignedLicense): Promise<void> {\n await ensureDataDirs();\n const file: LicenseFile = {\n version: 2,\n license: signed.license,\n sig: signed.sig,\n };\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\n/**\n * @deprecated Use `persistSigned` with a backend-issued SignedLicense. Kept\n * for tests that pre-date the v2 envelope. Sign here locally with a test\n * keypair would defeat the threat model, so this path is intentionally\n * non-functional in production: any LicenseData saved through here cannot\n * be loaded back (no signature → loadLicense returns null).\n */\nexport async function saveLicense(_data: LicenseData): Promise<void> {\n throw new Error('saveLicense is no longer supported; the backend issues signed envelopes (4.0.0).');\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\nexport async function activate(key: string): Promise<LicenseData> {\n validateLicenseKey(key);\n checkRateLimit();\n\n let success = false;\n try {\n const machineId = await getMachineId();\n const signed = await apiActivate(key, machineId);\n\n // Verify the envelope before trusting it. The backend should never emit\n // an unverifiable response, but a MITM injecting `{license, sig:\"\"}`\n // would otherwise pass straight through.\n if (!verifySignedLicense(signed)) {\n throw new Error('Backend response failed signature verification');\n }\n\n await persistSigned(signed);\n success = true;\n return signed.license;\n } finally {\n recordAttempt(success);\n }\n}\n\n/**\n * Revalidate the license against the server. Each call refreshes\n * lastValidatedAt and resets the offline-degradation timer.\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 signed = await apiValidate(license.key, license.instanceId);\n if (!verifySignedLicense(signed)) {\n // Treat a malformed signature as a hard failure — same posture as\n // an explicit \"expired\" from the backend.\n return 'expired';\n }\n await persistSigned(signed);\n // The backend stamps lastValidatedAt server-side, so the persisted\n // envelope is already fresh. Surface only the status to the caller.\n return signed.license.status === 'active' ? 'valid' : '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 — leave the existing file\n // alone (the user was Pro a moment ago; a transient API blip shouldn't\n // wipe the local state) but report expired so callers stop authorizing.\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 { fetchWithRetry } from '../fetch-timeout.js';\nimport type { LicenseData } from './types.js';\n\n// SEG-009/v2: licence operations no longer hit Polar directly. The brewtui-api\n// backend (NAS) proxies activate/validate/deactivate and returns an Ed25519-\n// signed envelope `{ license, sig }`. The client verifies that envelope\n// offline with the embedded public key (see license-manager.ts). The Polar\n// shared secret used to live in the published npm bundle, allowing anyone\n// with the bundle to forge a license; routing through the backend moves the\n// signing key off the client entirely.\nconst BASE_URL = 'https://api.molinesdesigns.com/api/license';\n\n/**\n * The shape returned by the backend's activate/validate endpoints. `license`\n * is the same LicenseData the rest of the codebase already consumes; `sig` is\n * base64 Ed25519 over canonical JSON of `license`.\n */\nexport interface SignedLicense {\n license: LicenseData;\n sig: string;\n}\n\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 (carried over from the old polar-api): only accept the exact\n // hostname or a true subdomain of molinesdesigns.com. `endsWith` alone\n // would let `evilmolinesdesigns.com` through.\n if (\n parsed.hostname !== 'molinesdesigns.com'\n && !parsed.hostname.endsWith('.molinesdesigns.com')\n ) {\n throw new Error('Invalid API host');\n }\n}\n\ninterface BackendEnvelope<T> {\n success: boolean;\n data?: T;\n error?: string;\n code?: string;\n}\n\nasync function post<T>(endpoint: string, body: Record<string, unknown>, expectEmpty = false): Promise<T> {\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 BackendEnvelope<unknown>;\n if (typeof errBody.error === 'string') message = errBody.error;\n } catch { /* non-JSON body — keep generic message */ }\n throw new Error(message);\n }\n\n if (expectEmpty || res.status === 204) return undefined as T;\n const wrapped = await res.json() as BackendEnvelope<T>;\n if (!wrapped.success || wrapped.data === undefined) {\n throw new Error(wrapped.error ?? 'Backend response missing data');\n }\n return wrapped.data;\n}\n\n/**\n * Validate the signed envelope structurally (the cryptographic verify happens\n * in license-manager.ts where the public key lives). Throws on any missing\n * field so callers can rely on the return type.\n */\nfunction assertSigned(value: unknown): asserts value is SignedLicense {\n if (typeof value !== 'object' || value === null) {\n throw new Error('Invalid signed license: not an object');\n }\n const v = value as Record<string, unknown>;\n if (typeof v.sig !== 'string' || v.sig.length === 0) {\n throw new Error('Invalid signed license: missing signature');\n }\n if (typeof v.license !== 'object' || v.license === null) {\n throw new Error('Invalid signed license: missing license payload');\n }\n}\n\nexport async function activateLicense(key: string, machineId: string): Promise<SignedLicense> {\n const signed = await post<SignedLicense>('activate', { key, machineId });\n assertSigned(signed);\n return signed;\n}\n\nexport async function validateLicense(key: string, instanceId: string): Promise<SignedLicense> {\n const signed = await post<SignedLicense>('validate', { key, instanceId });\n assertSigned(signed);\n return signed;\n}\n\nexport async function deactivateLicense(key: string, instanceId: string): Promise<void> {\n await post<{ deactivated: boolean }>('deactivate', { key, instanceId });\n}\n\n// Exposed for tests: lets us point the client at a local backend without\n// reaching for the network. The runtime caller never uses it.\nexport function _internalBaseUrl(): string {\n return BASE_URL;\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\n/**\n * v2 envelope: license payload + Ed25519 signature from the brewtui-api\n * backend. The client verifies the signature with the embedded public key\n * — see license-manager.ts. This replaces the v1 AES-GCM envelope where the\n * shared HKDF secret lived in the public npm bundle.\n *\n * v1 envelopes (encrypted: {iv,tag,encrypted}) and legacy unencrypted\n * envelopes ({license}) are still represented here so the loader can detect\n * and reject them with a helpful message (\"run brew-tui revalidate\").\n */\nexport interface LicenseFile {\n version: 1 | 2;\n // v2 fields\n license?: LicenseData | null;\n sig?: string;\n // v1 legacy fields — present only on files written by < 4.0.0\n hmac?: string;\n encrypted?: string;\n iv?: string;\n tag?: string;\n}\n\nexport type LicenseStatus = 'free' | 'pro' | 'team' | 'expired' | 'validating';\n\n// PolarActivateResponse / PolarValidateResponse used to mirror the shape of\n// the customer-portal API responses. Since 4.0.0 the client no longer talks\n// to Polar directly — see polar-api.ts (now a thin wrapper around the\n// brewtui-api backend). The signed envelope returned by the backend is\n// `SignedLicense` (exported from polar-api.ts).\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":[]}
@@ -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-5ODCMHRG.js");
189
+ const { checkBrewTUIBarVersion } = await import("./version-check-JKPPSCBU.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-SY3CDC6Q.js.map
228
+ //# sourceMappingURL=chunk-X5I6BCKV.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.3.2";
9
+ return "4.0.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-KGMDTVI6.js.map
65
+ //# sourceMappingURL=chunk-YZMSGOIV.js.map
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  checkBrewTUIBarVersion,
3
3
  readBrewTUIBarVersion
4
- } from "./chunk-KGMDTVI6.js";
4
+ } from "./chunk-YZMSGOIV.js";
5
5
  import {
6
6
  useLicenseStore
7
- } from "./chunk-FQV2F47X.js";
7
+ } from "./chunk-ILKWT7WR.js";
8
8
  import {
9
9
  bundleIdAt,
10
10
  isBrewTUIBarInstalled,
11
11
  isBrewTUIBarRunning
12
- } from "./chunk-SY3CDC6Q.js";
13
- import "./chunk-4RPJM7O7.js";
12
+ } from "./chunk-X5I6BCKV.js";
13
+ import "./chunk-TA3XPHF4.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.3.2";
56
+ const cliVersion = "4.0.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-ISRR4P33.js.map
133
+ //# sourceMappingURL=doctor-3IMAS27U.js.map
package/build/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  useLicenseStore,
3
3
  verifyStoreIntegrity
4
- } from "./chunk-FQV2F47X.js";
4
+ } from "./chunk-ILKWT7WR.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-D6DGYMXM.js";
38
+ } from "./chunk-Q4PGJ4H2.js";
39
39
  import {
40
40
  activate,
41
41
  deactivate,
42
42
  getDegradationLevel,
43
43
  loadLicense,
44
44
  revalidate
45
- } from "./chunk-4RPJM7O7.js";
45
+ } from "./chunk-TA3XPHF4.js";
46
46
  import {
47
47
  fetchWithRetry
48
48
  } from "./chunk-NRRQECXA.js";
@@ -4562,7 +4562,7 @@ function AccountView() {
4562
4562
  status === "pro" || status === "team" || status === "expired" ? `v ${t("hint_revalidate")} ` : "",
4563
4563
  revalidating ? t("account_revalidating") : "",
4564
4564
  " ",
4565
- t("app_version", { version: "3.3.2" })
4565
+ t("app_version", { version: "4.0.0" })
4566
4566
  ] }) })
4567
4567
  ] });
4568
4568
  }
@@ -6070,7 +6070,7 @@ async function reportError(err, context = {}) {
6070
6070
  const config = await resolveConfig();
6071
6071
  if (!config.enabled || !config.endpoint) return;
6072
6072
  const machineId = await getMachineId();
6073
- const version = true ? "3.3.2" : "unknown";
6073
+ const version = true ? "4.0.0" : "unknown";
6074
6074
  await postReport(buildReport("error", err, context, machineId, version), config);
6075
6075
  }
6076
6076
  async function installCrashReporter() {
@@ -6079,7 +6079,7 @@ async function installCrashReporter() {
6079
6079
  if (!config.enabled || !config.endpoint) return;
6080
6080
  _installed = true;
6081
6081
  const machineId = await getMachineId();
6082
- const version = true ? "3.3.2" : "unknown";
6082
+ const version = true ? "4.0.0" : "unknown";
6083
6083
  process.on("uncaughtException", (err) => {
6084
6084
  void postReport(buildReport("fatal", err, { kind: "uncaughtException" }, machineId, version), config);
6085
6085
  });
@@ -6094,11 +6094,11 @@ import { jsx as jsx39 } from "react/jsx-runtime";
6094
6094
  var [, , command, arg] = process.argv;
6095
6095
  async function runCli() {
6096
6096
  if (command === "--version" || command === "-v" || command === "version") {
6097
- const cliVersion = "3.3.2";
6097
+ const cliVersion = "4.0.0";
6098
6098
  process.stdout.write(cliVersion + "\n");
6099
6099
  if (process.platform === "darwin") {
6100
6100
  try {
6101
- const { readBrewTUIBarVersion } = await import("./version-check-5ODCMHRG.js");
6101
+ const { readBrewTUIBarVersion } = await import("./version-check-JKPPSCBU.js");
6102
6102
  const appVersion = await readBrewTUIBarVersion();
6103
6103
  if (appVersion && appVersion !== cliVersion) {
6104
6104
  process.stderr.write(t("cli_versionMismatchWarning", { installed: appVersion, expected: cliVersion }) + "\n");
@@ -6222,7 +6222,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6222
6222
  } catch {
6223
6223
  }
6224
6224
  try {
6225
- const { loadSyncConfig: loadSyncConfig2 } = await import("./sync-engine-37NCGPBS.js");
6225
+ const { loadSyncConfig: loadSyncConfig2 } = await import("./sync-engine-7M4WUHUW.js");
6226
6226
  const syncConfig = await loadSyncConfig2();
6227
6227
  if (syncConfig?.lastSync) {
6228
6228
  console.log(`Sync: last sync ${formatDate(syncConfig.lastSync)}`);
@@ -6243,7 +6243,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6243
6243
  return;
6244
6244
  }
6245
6245
  if (command === "install-brew-tui-bar") {
6246
- const { installBrewTUIBar } = await import("./brew-tui-bar-installer-ZEILWDFQ.js");
6246
+ const { installBrewTUIBar } = await import("./brew-tui-bar-installer-4SARQO7K.js");
6247
6247
  try {
6248
6248
  console.log(t("cli_brewtuibarInstalling"));
6249
6249
  await installBrewTUIBar(false, arg === "--force");
@@ -6255,7 +6255,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6255
6255
  return;
6256
6256
  }
6257
6257
  if (command === "uninstall-brew-tui-bar") {
6258
- const { uninstallBrewTUIBar } = await import("./brew-tui-bar-installer-ZEILWDFQ.js");
6258
+ const { uninstallBrewTUIBar } = await import("./brew-tui-bar-installer-4SARQO7K.js");
6259
6259
  try {
6260
6260
  await uninstallBrewTUIBar();
6261
6261
  console.log(t("cli_brewtuibarUninstalled"));
@@ -6266,7 +6266,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6266
6266
  return;
6267
6267
  }
6268
6268
  if (command === "doctor") {
6269
- const { runDoctor } = await import("./doctor-ISRR4P33.js");
6269
+ const { runDoctor } = await import("./doctor-3IMAS27U.js");
6270
6270
  await runDoctor();
6271
6271
  return;
6272
6272
  }
@@ -6290,7 +6290,7 @@ Snapshots: ${snapshots.length} (latest: ${latest ? formatDate(latest.capturedAt)
6290
6290
  async function ensureBrewTUIBarRunning() {
6291
6291
  if (process.platform !== "darwin") return;
6292
6292
  await useLicenseStore.getState().initialize();
6293
- const { syncAndLaunchBrewTUIBar } = await import("./brew-tui-bar-installer-ZEILWDFQ.js");
6293
+ const { syncAndLaunchBrewTUIBar } = await import("./brew-tui-bar-installer-4SARQO7K.js");
6294
6294
  await syncAndLaunchBrewTUIBar();
6295
6295
  }
6296
6296
  runCli().catch((err) => {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  syncAndLaunchBrewTUIBar
4
- } from "./chunk-SY3CDC6Q.js";
4
+ } from "./chunk-X5I6BCKV.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-D6DGYMXM.js";
7
- import "./chunk-4RPJM7O7.js";
6
+ } from "./chunk-Q4PGJ4H2.js";
7
+ import "./chunk-TA3XPHF4.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-37NCGPBS.js.map
22
+ //# sourceMappingURL=sync-engine-7M4WUHUW.js.map
@@ -4,7 +4,7 @@ import {
4
4
  compareSemver,
5
5
  expectedVersion,
6
6
  readBrewTUIBarVersion
7
- } from "./chunk-KGMDTVI6.js";
7
+ } from "./chunk-YZMSGOIV.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-5ODCMHRG.js.map
15
+ //# sourceMappingURL=version-check-JKPPSCBU.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brew-tui",
3
- "version": "3.3.2",
3
+ "version": "4.0.0",
4
4
  "description": "Brew-TUI — Visual TUI for Homebrew package management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,371 +0,0 @@
1
- import {
2
- fetchWithRetry
3
- } from "./chunk-NRRQECXA.js";
4
- import {
5
- t
6
- } from "./chunk-A7U3NZYM.js";
7
- import {
8
- LICENSE_PATH,
9
- ensureDataDirs,
10
- getMachineId
11
- } from "./chunk-LFGDNAXH.js";
12
-
13
- // src/lib/license/license-manager.ts
14
- import { readFile, writeFile, rename, rm } from "fs/promises";
15
- import { createCipheriv, createDecipheriv, randomBytes, hkdfSync } from "crypto";
16
-
17
- // src/lib/license/polar-api.ts
18
- import { createHash } from "crypto";
19
- function hashMachineLabel(machineId) {
20
- return createHash("sha256").update(machineId).digest("hex").slice(0, 32);
21
- }
22
- var BASE_URL = "https://api.polar.sh/v1/customer-portal/license-keys";
23
- var POLAR_ORGANIZATION_ID = "b8f245c0-d116-4457-92fb-1bda47139f82";
24
- function validateApiUrl(url) {
25
- const parsed = new URL(url);
26
- if (parsed.protocol !== "https:") {
27
- throw new Error("HTTPS required for license API");
28
- }
29
- if (parsed.hostname !== "polar.sh" && !parsed.hostname.endsWith(".polar.sh")) {
30
- throw new Error("Invalid API host");
31
- }
32
- }
33
- async function post(endpoint, body, expectEmpty = false) {
34
- const url = `${BASE_URL}/${endpoint}/`;
35
- validateApiUrl(url);
36
- const res = await fetchWithRetry(url, {
37
- method: "POST",
38
- headers: { "Content-Type": "application/json" },
39
- body: JSON.stringify(body)
40
- }, 15e3);
41
- if (!res.ok) {
42
- let message = `Request failed with status ${res.status}`;
43
- try {
44
- const errBody = await res.json();
45
- if (typeof errBody.detail === "string") message = errBody.detail;
46
- else if (typeof errBody.error === "string") message = errBody.error;
47
- else if (typeof errBody.message === "string") message = errBody.message;
48
- } catch {
49
- }
50
- throw new Error(message);
51
- }
52
- if (expectEmpty || res.status === 204) return void 0;
53
- return res.json();
54
- }
55
- async function activateLicense(key) {
56
- const machineId = await getMachineId();
57
- const activation = await post("activate", {
58
- key,
59
- organization_id: POLAR_ORGANIZATION_ID,
60
- // SEG-004 + BK-009: identificador estable por equipo, hasheado para
61
- // que el UUID en claro no aparezca en logs de Polar.
62
- label: hashMachineLabel(machineId)
63
- });
64
- if (!activation || typeof activation.id !== "string" || !activation.license_key) {
65
- throw new Error("Invalid activation response: missing required fields");
66
- }
67
- let customerEmail = "";
68
- let customerName = "";
69
- try {
70
- const validated = await post("validate", {
71
- key,
72
- organization_id: POLAR_ORGANIZATION_ID,
73
- activation_id: activation.id
74
- });
75
- customerEmail = validated.customer?.email ?? "";
76
- customerName = validated.customer?.name ?? "";
77
- } catch {
78
- }
79
- return {
80
- activated: true,
81
- error: null,
82
- instance: { id: activation.id },
83
- license_key: {
84
- id: 0,
85
- status: activation.license_key.status,
86
- key,
87
- activation_limit: 0,
88
- activations_count: 0,
89
- expires_at: activation.license_key.expires_at
90
- },
91
- meta: { customer_email: customerEmail, customer_name: customerName }
92
- };
93
- }
94
- async function validateLicense(key, instanceId) {
95
- const res = await post("validate", {
96
- key,
97
- organization_id: POLAR_ORGANIZATION_ID,
98
- activation_id: instanceId
99
- });
100
- if (!res || typeof res.id !== "string" || typeof res.status !== "string" || !res.customer) {
101
- throw new Error("Invalid validation response: missing required fields");
102
- }
103
- const notExpired = res.expires_at === null || new Date(res.expires_at) > /* @__PURE__ */ new Date();
104
- const valid = res.status === "granted" && notExpired;
105
- return {
106
- valid,
107
- error: valid ? null : `License ${res.status}`,
108
- license_key: {
109
- id: 0,
110
- status: res.status,
111
- key,
112
- expires_at: res.expires_at
113
- },
114
- instance: { id: instanceId }
115
- };
116
- }
117
- async function deactivateLicense(key, instanceId) {
118
- await post(
119
- "deactivate",
120
- { key, organization_id: POLAR_ORGANIZATION_ID, activation_id: instanceId },
121
- true
122
- );
123
- }
124
-
125
- // src/lib/license/types.ts
126
- function isLicenseData(value) {
127
- if (typeof value !== "object" || value === null) return false;
128
- const v = value;
129
- return typeof v.key === "string" && typeof v.instanceId === "string" && (v.status === "active" || v.status === "expired" || v.status === "inactive") && typeof v.customerEmail === "string" && typeof v.customerName === "string" && (v.plan === "pro" || v.plan === "team") && typeof v.activatedAt === "string" && (v.expiresAt === null || typeof v.expiresAt === "string") && typeof v.lastValidatedAt === "string";
130
- }
131
-
132
- // src/lib/license/license-manager.ts
133
- var REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
134
- var GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
135
- var ACTIVATION_COOLDOWN_MS = 3e4;
136
- var MAX_ATTEMPTS = 5;
137
- var LOCKOUT_MS = 15 * 60 * 1e3;
138
- var tracker = {
139
- attempts: 0,
140
- lastAttempt: 0,
141
- lockedUntil: 0
142
- };
143
- function checkRateLimit() {
144
- const now = Date.now();
145
- if (now < tracker.lockedUntil) {
146
- const remaining = Math.ceil((tracker.lockedUntil - now) / 6e4);
147
- throw new Error(t("cli_rateLimited", { minutes: remaining }));
148
- }
149
- if (now - tracker.lastAttempt < ACTIVATION_COOLDOWN_MS) {
150
- throw new Error(t("cli_cooldown"));
151
- }
152
- }
153
- function recordAttempt(success) {
154
- const now = Date.now();
155
- tracker.lastAttempt = now;
156
- if (success) {
157
- tracker.attempts = 0;
158
- return;
159
- }
160
- tracker.attempts++;
161
- if (tracker.attempts >= MAX_ATTEMPTS) {
162
- tracker.lockedUntil = now + LOCKOUT_MS;
163
- tracker.attempts = 0;
164
- }
165
- }
166
- var ENCRYPTION_SECRET = "brew-tui-license-aes256gcm-v1";
167
- var HKDF_SALT = "brew-tui-salt-v1";
168
- var _derivedKey = null;
169
- async function deriveEncryptionKey() {
170
- if (_derivedKey) return _derivedKey;
171
- const machineId = await getMachineId();
172
- const derived = hkdfSync("sha256", ENCRYPTION_SECRET, HKDF_SALT, machineId, 32);
173
- _derivedKey = Buffer.from(derived);
174
- return _derivedKey;
175
- }
176
- async function encryptLicenseData(data) {
177
- const key = await deriveEncryptionKey();
178
- const iv = randomBytes(12);
179
- const cipher = createCipheriv("aes-256-gcm", key, iv);
180
- const plaintext = JSON.stringify(data);
181
- const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
182
- const tag = cipher.getAuthTag();
183
- return {
184
- encrypted: ciphertext.toString("base64"),
185
- iv: iv.toString("base64"),
186
- tag: tag.toString("base64")
187
- };
188
- }
189
- async function decryptLicenseData(encrypted, iv, tag) {
190
- const ivBuf = Buffer.from(iv, "base64");
191
- const tagBuf = Buffer.from(tag, "base64");
192
- const ciphertext = Buffer.from(encrypted, "base64");
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");
200
- }
201
- return parsed;
202
- }
203
- function isLicenseFile(obj) {
204
- return typeof obj === "object" && obj !== null && obj.version === 1;
205
- }
206
- function isEncryptedLicenseFile(obj) {
207
- if (!isLicenseFile(obj)) return false;
208
- const record = obj;
209
- return typeof record.encrypted === "string" && typeof record.iv === "string" && typeof record.tag === "string";
210
- }
211
- async function loadLicense() {
212
- try {
213
- const raw = await readFile(LICENSE_PATH, "utf-8");
214
- const parsed = JSON.parse(raw);
215
- if (!isLicenseFile(parsed)) {
216
- throw new Error("Invalid license data format");
217
- }
218
- const file = parsed;
219
- if (file.version !== 1) {
220
- throw new Error("Unsupported data version");
221
- }
222
- if (isEncryptedLicenseFile(file)) {
223
- const data = await decryptLicenseData(file.encrypted, file.iv, file.tag);
224
- const fileRecord = file;
225
- if (fileRecord.machineId) {
226
- const currentMachineId = await getMachineId();
227
- if (typeof fileRecord.machineId !== "string" || fileRecord.machineId.toLowerCase() !== currentMachineId.toLowerCase()) {
228
- throw new Error("License was activated on a different machine");
229
- }
230
- }
231
- return data;
232
- }
233
- if (file.license) {
234
- const data = file.license;
235
- await saveLicense(data);
236
- return data;
237
- }
238
- return null;
239
- } catch {
240
- return null;
241
- }
242
- }
243
- async function saveLicense(data) {
244
- await ensureDataDirs();
245
- const { encrypted, iv, tag } = await encryptLicenseData(data);
246
- const machineId = await getMachineId();
247
- const file = { version: 1, encrypted, iv, tag, machineId };
248
- const tmpPath = LICENSE_PATH + ".tmp";
249
- await writeFile(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
250
- await rename(tmpPath, LICENSE_PATH);
251
- }
252
- async function clearLicense() {
253
- try {
254
- await rm(LICENSE_PATH);
255
- } catch {
256
- }
257
- }
258
- function isExpired(license) {
259
- if (!license.expiresAt) return false;
260
- const expiry = new Date(license.expiresAt).getTime();
261
- if (isNaN(expiry)) return true;
262
- return expiry < Date.now();
263
- }
264
- function needsRevalidation(license) {
265
- const lastValidated = new Date(license.lastValidatedAt).getTime();
266
- if (isNaN(lastValidated)) return true;
267
- return Date.now() - lastValidated > REVALIDATION_INTERVAL_MS;
268
- }
269
- function isWithinGracePeriod(license) {
270
- const lastValidated = new Date(license.lastValidatedAt).getTime();
271
- if (isNaN(lastValidated)) return false;
272
- return Date.now() - lastValidated < GRACE_PERIOD_MS;
273
- }
274
- function getDegradationLevel(license) {
275
- const lastValidated = new Date(license.lastValidatedAt).getTime();
276
- if (isNaN(lastValidated)) return "expired";
277
- const elapsed = Date.now() - lastValidated;
278
- if (elapsed < 0) return "expired";
279
- const days = elapsed / (24 * 60 * 60 * 1e3);
280
- if (days <= 7) return "none";
281
- if (days <= 14) return "warning";
282
- if (days <= 30) return "limited";
283
- return "expired";
284
- }
285
- function validateLicenseKey(key) {
286
- if (key.length < 10 || key.length > 100) {
287
- throw new Error("Invalid license key format");
288
- }
289
- if (!/^[\w-]+$/.test(key)) {
290
- throw new Error("Invalid license key format");
291
- }
292
- }
293
- function detectPlan(key) {
294
- const upper = key.toUpperCase();
295
- return upper.startsWith("BTUI-T-") || upper.startsWith("BTUI-T_") ? "team" : "pro";
296
- }
297
- async function activate(key) {
298
- validateLicenseKey(key);
299
- checkRateLimit();
300
- let success = false;
301
- try {
302
- const res = await activateLicense(key);
303
- if (!res.activated) {
304
- throw new Error(res.error ?? "Activation failed");
305
- }
306
- const license = {
307
- key,
308
- instanceId: res.instance.id,
309
- status: "active",
310
- customerEmail: res.meta.customer_email,
311
- customerName: res.meta.customer_name,
312
- plan: detectPlan(key),
313
- activatedAt: (/* @__PURE__ */ new Date()).toISOString(),
314
- expiresAt: res.license_key.expires_at,
315
- lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString()
316
- };
317
- await saveLicense(license);
318
- success = true;
319
- return license;
320
- } finally {
321
- recordAttempt(success);
322
- }
323
- }
324
- function isNetworkError(err) {
325
- const msg = err instanceof Error ? err.message : String(err);
326
- return /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network|timeout|abort/i.test(msg);
327
- }
328
- async function revalidate(license) {
329
- try {
330
- const res = await validateLicense(license.key, license.instanceId);
331
- if (res.valid) {
332
- const updated = {
333
- ...license,
334
- lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString(),
335
- status: "active",
336
- expiresAt: res.license_key.expires_at
337
- };
338
- await saveLicense(updated);
339
- return "valid";
340
- }
341
- await saveLicense({ ...license, status: "expired" });
342
- return "expired";
343
- } catch (err) {
344
- if (isNetworkError(err)) {
345
- return isWithinGracePeriod(license) ? "grace" : "expired";
346
- }
347
- await saveLicense({ ...license, status: "expired" });
348
- return "expired";
349
- }
350
- }
351
- async function deactivate(license) {
352
- let remoteSuccess = false;
353
- try {
354
- await deactivateLicense(license.key, license.instanceId);
355
- remoteSuccess = true;
356
- } catch {
357
- }
358
- await clearLicense();
359
- return { remoteSuccess };
360
- }
361
-
362
- export {
363
- loadLicense,
364
- isExpired,
365
- needsRevalidation,
366
- getDegradationLevel,
367
- activate,
368
- revalidate,
369
- deactivate
370
- };
371
- //# sourceMappingURL=chunk-4RPJM7O7.js.map
@@ -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, 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":[]}