brew-tui 3.4.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/{brew-tui-bar-installer-D75KPQ3Z.js → brew-tui-bar-installer-CGJFE7PQ.js} +2 -2
- package/build/{chunk-BP7CFABE.js → chunk-FFULOTXT.js} +2 -2
- package/build/{chunk-FQV2F47X.js → chunk-ILKWT7WR.js} +2 -2
- package/build/{chunk-D6DGYMXM.js → chunk-Q4PGJ4H2.js} +2 -2
- package/build/chunk-TA3XPHF4.js +284 -0
- package/build/chunk-TA3XPHF4.js.map +1 -0
- package/build/{chunk-63AUNN5U.js → chunk-UKMRSNGQ.js} +2 -2
- package/build/{doctor-V7IRCOIM.js → doctor-HNDEFZIR.js} +6 -6
- package/build/index.js +13 -13
- package/build/postinstall.js +1 -1
- package/build/{sync-engine-37NCGPBS.js → sync-engine-7M4WUHUW.js} +3 -3
- package/build/{version-check-2EYL5QFG.js → version-check-HAHQ336G.js} +2 -2
- package/package.json +1 -1
- package/build/chunk-4RPJM7O7.js +0 -371
- package/build/chunk-4RPJM7O7.js.map +0 -1
- /package/build/{brew-tui-bar-installer-D75KPQ3Z.js.map → brew-tui-bar-installer-CGJFE7PQ.js.map} +0 -0
- /package/build/{chunk-BP7CFABE.js.map → chunk-FFULOTXT.js.map} +0 -0
- /package/build/{chunk-FQV2F47X.js.map → chunk-ILKWT7WR.js.map} +0 -0
- /package/build/{chunk-D6DGYMXM.js.map → chunk-Q4PGJ4H2.js.map} +0 -0
- /package/build/{chunk-63AUNN5U.js.map → chunk-UKMRSNGQ.js.map} +0 -0
- /package/build/{doctor-V7IRCOIM.js.map → doctor-HNDEFZIR.js.map} +0 -0
- /package/build/{sync-engine-37NCGPBS.js.map → sync-engine-7M4WUHUW.js.map} +0 -0
- /package/build/{version-check-2EYL5QFG.js.map → version-check-HAHQ336G.js.map} +0 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
launchBrewTUIBar,
|
|
7
7
|
syncAndLaunchBrewTUIBar,
|
|
8
8
|
uninstallBrewTUIBar
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-UKMRSNGQ.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-
|
|
22
|
+
//# sourceMappingURL=brew-tui-bar-installer-CGJFE7PQ.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 "
|
|
9
|
+
return "4.0.1";
|
|
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-
|
|
65
|
+
//# sourceMappingURL=chunk-FFULOTXT.js.map
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
loadLicense,
|
|
7
7
|
needsRevalidation,
|
|
8
8
|
revalidate
|
|
9
|
-
} from "./chunk-
|
|
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-
|
|
138
|
+
//# sourceMappingURL=chunk-ILKWT7WR.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadLicense
|
|
3
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
189
|
+
const { checkBrewTUIBarVersion } = await import("./version-check-HAHQ336G.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-
|
|
228
|
+
//# sourceMappingURL=chunk-UKMRSNGQ.js.map
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
checkBrewTUIBarVersion,
|
|
3
3
|
readBrewTUIBarVersion
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FFULOTXT.js";
|
|
5
5
|
import {
|
|
6
6
|
useLicenseStore
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ILKWT7WR.js";
|
|
8
8
|
import {
|
|
9
9
|
bundleIdAt,
|
|
10
10
|
isBrewTUIBarInstalled,
|
|
11
11
|
isBrewTUIBarRunning
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-UKMRSNGQ.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 = "
|
|
56
|
+
const cliVersion = "4.0.1";
|
|
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-
|
|
133
|
+
//# sourceMappingURL=doctor-HNDEFZIR.js.map
|
package/build/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useLicenseStore,
|
|
3
3
|
verifyStoreIntegrity
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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: "
|
|
4565
|
+
t("app_version", { version: "4.0.1" })
|
|
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 ? "
|
|
6073
|
+
const version = true ? "4.0.1" : "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 ? "
|
|
6082
|
+
const version = true ? "4.0.1" : "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 = "
|
|
6097
|
+
const cliVersion = "4.0.1";
|
|
6098
6098
|
process.stdout.write(cliVersion + "\n");
|
|
6099
6099
|
if (process.platform === "darwin") {
|
|
6100
6100
|
try {
|
|
6101
|
-
const { readBrewTUIBarVersion } = await import("./version-check-
|
|
6101
|
+
const { readBrewTUIBarVersion } = await import("./version-check-HAHQ336G.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-
|
|
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-
|
|
6246
|
+
const { installBrewTUIBar } = await import("./brew-tui-bar-installer-CGJFE7PQ.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-
|
|
6258
|
+
const { uninstallBrewTUIBar } = await import("./brew-tui-bar-installer-CGJFE7PQ.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-
|
|
6269
|
+
const { runDoctor } = await import("./doctor-HNDEFZIR.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-
|
|
6293
|
+
const { syncAndLaunchBrewTUIBar } = await import("./brew-tui-bar-installer-CGJFE7PQ.js");
|
|
6294
6294
|
await syncAndLaunchBrewTUIBar();
|
|
6295
6295
|
}
|
|
6296
6296
|
runCli().catch((err) => {
|
package/build/postinstall.js
CHANGED
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
loadSyncConfig,
|
|
4
4
|
saveSyncConfig,
|
|
5
5
|
sync
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
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-
|
|
22
|
+
//# sourceMappingURL=sync-engine-7M4WUHUW.js.map
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
compareSemver,
|
|
5
5
|
expectedVersion,
|
|
6
6
|
readBrewTUIBarVersion
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FFULOTXT.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-
|
|
15
|
+
//# sourceMappingURL=version-check-HAHQ336G.js.map
|
package/package.json
CHANGED
package/build/chunk-4RPJM7O7.js
DELETED
|
@@ -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":[]}
|
/package/build/{brew-tui-bar-installer-D75KPQ3Z.js.map → brew-tui-bar-installer-CGJFE7PQ.js.map}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|