brew-tui 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/build/{brewbar-installer-BAHS6EEZ.js → brewbar-installer-WJZKSELD.js} +3 -3
- package/build/brewbar-installer-WJZKSELD.js.map +1 -0
- package/build/{brewfile-manager-3SERRYNC.js → brewfile-manager-OITSKEHY.js} +4 -4
- package/build/{chunk-U2DRWB7A.js → chunk-5MYWF5D7.js} +2 -2
- package/build/{chunk-MSXH66I2.js → chunk-6NA4INJS.js} +31 -3
- package/build/chunk-6NA4INJS.js.map +1 -0
- package/build/chunk-EHIBIFCB.js +54 -0
- package/build/chunk-EHIBIFCB.js.map +1 -0
- package/build/{chunk-LXF72RCD.js → chunk-KSIAKLE2.js} +3 -3
- package/build/{chunk-4I344KQX.js → chunk-PVSE6XO7.js} +42 -2
- package/build/chunk-PVSE6XO7.js.map +1 -0
- package/build/chunk-QPXROTAP.js +679 -0
- package/build/chunk-QPXROTAP.js.map +1 -0
- package/build/{chunk-KVCVIRWI.js → chunk-Z2VN4VYQ.js} +2 -2
- package/build/compliance-checker-7NMFKWTI.js +12 -0
- package/build/{history-logger-PBDOLKNJ.js → history-logger-ZGYRAFON.js} +3 -3
- package/build/index.js +449 -678
- package/build/index.js.map +1 -1
- package/build/{snapshot-RAPGMAJF.js → snapshot-YWIOFQ5H.js} +7 -3
- package/build/{sync-engine-CAFK4LHA.js → sync-engine-DIYXV66P.js} +7 -5
- package/package.json +4 -4
- package/build/brewbar-installer-BAHS6EEZ.js.map +0 -1
- package/build/chunk-4I344KQX.js.map +0 -1
- package/build/chunk-AIAZQJKL.js +0 -299
- package/build/chunk-AIAZQJKL.js.map +0 -1
- package/build/chunk-MSXH66I2.js.map +0 -1
- package/build/chunk-UWS4A4F5.js +0 -25
- package/build/chunk-UWS4A4F5.js.map +0 -1
- package/build/compliance-checker-X7P623UF.js +0 -12
- /package/build/{brewfile-manager-3SERRYNC.js.map → brewfile-manager-OITSKEHY.js.map} +0 -0
- /package/build/{chunk-U2DRWB7A.js.map → chunk-5MYWF5D7.js.map} +0 -0
- /package/build/{chunk-LXF72RCD.js.map → chunk-KSIAKLE2.js.map} +0 -0
- /package/build/{chunk-KVCVIRWI.js.map → chunk-Z2VN4VYQ.js.map} +0 -0
- /package/build/{compliance-checker-X7P623UF.js.map → compliance-checker-7NMFKWTI.js.map} +0 -0
- /package/build/{history-logger-PBDOLKNJ.js.map → history-logger-ZGYRAFON.js.map} +0 -0
- /package/build/{snapshot-RAPGMAJF.js.map → snapshot-YWIOFQ5H.js.map} +0 -0
- /package/build/{sync-engine-CAFK4LHA.js.map → sync-engine-DIYXV66P.js.map} +0 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureSnapshot
|
|
3
|
+
} from "./chunk-PVSE6XO7.js";
|
|
4
|
+
import {
|
|
5
|
+
DATA_DIR,
|
|
6
|
+
LICENSE_PATH,
|
|
7
|
+
ensureDataDirs,
|
|
8
|
+
getMachineId
|
|
9
|
+
} from "./chunk-EHIBIFCB.js";
|
|
10
|
+
import {
|
|
11
|
+
fetchWithRetry,
|
|
12
|
+
t
|
|
13
|
+
} from "./chunk-6NA4INJS.js";
|
|
14
|
+
import {
|
|
15
|
+
logger
|
|
16
|
+
} from "./chunk-KDHEUNRI.js";
|
|
17
|
+
|
|
18
|
+
// src/lib/sync/sync-engine.ts
|
|
19
|
+
import { readFile as readFile3, writeFile as writeFile3, rename as rename3 } from "fs/promises";
|
|
20
|
+
import { join as join2 } from "path";
|
|
21
|
+
import { hostname } from "os";
|
|
22
|
+
|
|
23
|
+
// src/lib/sync/crypto.ts
|
|
24
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from "crypto";
|
|
25
|
+
var ENCRYPTION_SECRET = "brew-tui-sync-aes256gcm-v1";
|
|
26
|
+
var HKDF_SALT = "brew-tui-sync-salt-v1";
|
|
27
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
28
|
+
var _legacyKey = null;
|
|
29
|
+
function deriveEncryptionKey(licenseKey) {
|
|
30
|
+
const cached = keyCache.get(licenseKey);
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
const derived = Buffer.from(hkdfSync("sha256", ENCRYPTION_SECRET, HKDF_SALT, licenseKey, 32));
|
|
33
|
+
keyCache.set(licenseKey, derived);
|
|
34
|
+
return derived;
|
|
35
|
+
}
|
|
36
|
+
function deriveLegacyKey() {
|
|
37
|
+
if (!_legacyKey) {
|
|
38
|
+
_legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32, { N: 16384, r: 8, p: 1 });
|
|
39
|
+
}
|
|
40
|
+
return _legacyKey;
|
|
41
|
+
}
|
|
42
|
+
function encryptPayload(data, licenseKey) {
|
|
43
|
+
const key = deriveEncryptionKey(licenseKey);
|
|
44
|
+
const iv = randomBytes(12);
|
|
45
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
46
|
+
const plaintext = JSON.stringify(data);
|
|
47
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
48
|
+
const tag = cipher.getAuthTag();
|
|
49
|
+
return {
|
|
50
|
+
encrypted: ciphertext.toString("base64"),
|
|
51
|
+
iv: iv.toString("base64"),
|
|
52
|
+
tag: tag.toString("base64")
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function decryptPayload(encrypted, iv, tag, licenseKey) {
|
|
56
|
+
const ivBuf = Buffer.from(iv, "base64");
|
|
57
|
+
const tagBuf = Buffer.from(tag, "base64");
|
|
58
|
+
const ciphertext = Buffer.from(encrypted, "base64");
|
|
59
|
+
for (const key of [deriveEncryptionKey(licenseKey), deriveLegacyKey()]) {
|
|
60
|
+
try {
|
|
61
|
+
const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
|
|
62
|
+
decipher.setAuthTag(tagBuf);
|
|
63
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
64
|
+
return JSON.parse(plaintext.toString("utf-8"));
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw new Error("Failed to decrypt sync payload");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/lib/sync/backends/icloud-backend.ts
|
|
72
|
+
import { readFile, writeFile, rename, mkdir, stat } from "fs/promises";
|
|
73
|
+
import { homedir } from "os";
|
|
74
|
+
import { join } from "path";
|
|
75
|
+
var ICLOUD_BASE = join(
|
|
76
|
+
homedir(),
|
|
77
|
+
"Library",
|
|
78
|
+
"Mobile Documents",
|
|
79
|
+
"com~apple~CloudDocs"
|
|
80
|
+
);
|
|
81
|
+
var ICLOUD_SYNC_DIR = join(ICLOUD_BASE, "BrewTUI");
|
|
82
|
+
var ICLOUD_SYNC_PATH = join(ICLOUD_SYNC_DIR, "sync.json");
|
|
83
|
+
async function isICloudAvailable() {
|
|
84
|
+
try {
|
|
85
|
+
await stat(ICLOUD_BASE);
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isValidEnvelope(v) {
|
|
92
|
+
if (!v || typeof v !== "object") return false;
|
|
93
|
+
const obj = v;
|
|
94
|
+
return obj["schemaVersion"] === 1 && typeof obj["encrypted"] === "string" && typeof obj["iv"] === "string" && typeof obj["tag"] === "string" && typeof obj["updatedAt"] === "string";
|
|
95
|
+
}
|
|
96
|
+
async function readSyncEnvelope() {
|
|
97
|
+
try {
|
|
98
|
+
const raw = await readFile(ICLOUD_SYNC_PATH, "utf-8");
|
|
99
|
+
const parsed = JSON.parse(raw);
|
|
100
|
+
if (!isValidEnvelope(parsed)) {
|
|
101
|
+
logger.warn("sync: invalid envelope structure in iCloud file");
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return parsed;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
logger.warn("sync: could not read iCloud envelope", { error: String(err) });
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function writeSyncEnvelope(envelope) {
|
|
114
|
+
await mkdir(ICLOUD_SYNC_DIR, { recursive: true });
|
|
115
|
+
const tmpPath = ICLOUD_SYNC_PATH + ".tmp";
|
|
116
|
+
await writeFile(tmpPath, JSON.stringify(envelope, null, 2), {
|
|
117
|
+
encoding: "utf-8",
|
|
118
|
+
mode: 384
|
|
119
|
+
});
|
|
120
|
+
await rename(tmpPath, ICLOUD_SYNC_PATH);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/lib/license/license-manager.ts
|
|
124
|
+
import { readFile as readFile2, writeFile as writeFile2, rename as rename2, rm } from "fs/promises";
|
|
125
|
+
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes2, scryptSync as scryptSync2, hkdfSync as hkdfSync2 } from "crypto";
|
|
126
|
+
|
|
127
|
+
// src/lib/license/polar-api.ts
|
|
128
|
+
var BASE_URL = "https://api.polar.sh/v1/customer-portal/license-keys";
|
|
129
|
+
var POLAR_ORGANIZATION_ID = "b8f245c0-d116-4457-92fb-1bda47139f82";
|
|
130
|
+
function validateApiUrl(url) {
|
|
131
|
+
const parsed = new URL(url);
|
|
132
|
+
if (parsed.protocol !== "https:") {
|
|
133
|
+
throw new Error("HTTPS required for license API");
|
|
134
|
+
}
|
|
135
|
+
if (!parsed.hostname.endsWith("polar.sh")) {
|
|
136
|
+
throw new Error("Invalid API host");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function post(endpoint, body, expectEmpty = false) {
|
|
140
|
+
const url = `${BASE_URL}/${endpoint}`;
|
|
141
|
+
validateApiUrl(url);
|
|
142
|
+
const res = await fetchWithRetry(url, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify(body)
|
|
146
|
+
}, 15e3);
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
let message = `Request failed with status ${res.status}`;
|
|
149
|
+
try {
|
|
150
|
+
const errBody = await res.json();
|
|
151
|
+
if (typeof errBody.detail === "string") message = errBody.detail;
|
|
152
|
+
else if (typeof errBody.error === "string") message = errBody.error;
|
|
153
|
+
else if (typeof errBody.message === "string") message = errBody.message;
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
throw new Error(message);
|
|
157
|
+
}
|
|
158
|
+
if (expectEmpty || res.status === 204) return void 0;
|
|
159
|
+
return res.json();
|
|
160
|
+
}
|
|
161
|
+
async function activateLicense(key) {
|
|
162
|
+
const machineId = await getMachineId();
|
|
163
|
+
const activation = await post("activate", {
|
|
164
|
+
key,
|
|
165
|
+
organization_id: POLAR_ORGANIZATION_ID,
|
|
166
|
+
label: machineId
|
|
167
|
+
// SEG-004: Use machine UUID instead of hostname
|
|
168
|
+
});
|
|
169
|
+
if (!activation || typeof activation.id !== "string" || !activation.license_key) {
|
|
170
|
+
throw new Error("Invalid activation response: missing required fields");
|
|
171
|
+
}
|
|
172
|
+
let customerEmail = "";
|
|
173
|
+
let customerName = "";
|
|
174
|
+
try {
|
|
175
|
+
const validated = await post("validate", {
|
|
176
|
+
key,
|
|
177
|
+
organization_id: POLAR_ORGANIZATION_ID,
|
|
178
|
+
activation_id: activation.id
|
|
179
|
+
});
|
|
180
|
+
customerEmail = validated.customer?.email ?? "";
|
|
181
|
+
customerName = validated.customer?.name ?? "";
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
activated: true,
|
|
186
|
+
error: null,
|
|
187
|
+
instance: { id: activation.id },
|
|
188
|
+
license_key: {
|
|
189
|
+
id: 0,
|
|
190
|
+
status: activation.license_key.status,
|
|
191
|
+
key,
|
|
192
|
+
activation_limit: 0,
|
|
193
|
+
activations_count: 0,
|
|
194
|
+
expires_at: activation.license_key.expires_at
|
|
195
|
+
},
|
|
196
|
+
meta: { customer_email: customerEmail, customer_name: customerName }
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function validateLicense(key, instanceId) {
|
|
200
|
+
const res = await post("validate", {
|
|
201
|
+
key,
|
|
202
|
+
organization_id: POLAR_ORGANIZATION_ID,
|
|
203
|
+
activation_id: instanceId
|
|
204
|
+
});
|
|
205
|
+
if (!res || typeof res.id !== "string" || typeof res.status !== "string" || !res.customer) {
|
|
206
|
+
throw new Error("Invalid validation response: missing required fields");
|
|
207
|
+
}
|
|
208
|
+
const notExpired = res.expires_at === null || new Date(res.expires_at) > /* @__PURE__ */ new Date();
|
|
209
|
+
const valid = res.status === "granted" && notExpired;
|
|
210
|
+
return {
|
|
211
|
+
valid,
|
|
212
|
+
error: valid ? null : `License ${res.status}`,
|
|
213
|
+
license_key: {
|
|
214
|
+
id: 0,
|
|
215
|
+
status: res.status,
|
|
216
|
+
key,
|
|
217
|
+
expires_at: res.expires_at
|
|
218
|
+
},
|
|
219
|
+
instance: { id: instanceId }
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async function deactivateLicense(key, instanceId) {
|
|
223
|
+
await post(
|
|
224
|
+
"deactivate",
|
|
225
|
+
{ key, organization_id: POLAR_ORGANIZATION_ID, activation_id: instanceId },
|
|
226
|
+
true
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/lib/license/license-manager.ts
|
|
231
|
+
var REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
232
|
+
var GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
233
|
+
var ACTIVATION_COOLDOWN_MS = 3e4;
|
|
234
|
+
var MAX_ATTEMPTS = 5;
|
|
235
|
+
var LOCKOUT_MS = 15 * 60 * 1e3;
|
|
236
|
+
var tracker = {
|
|
237
|
+
attempts: 0,
|
|
238
|
+
lastAttempt: 0,
|
|
239
|
+
lockedUntil: 0
|
|
240
|
+
};
|
|
241
|
+
function checkRateLimit() {
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
if (now < tracker.lockedUntil) {
|
|
244
|
+
const remaining = Math.ceil((tracker.lockedUntil - now) / 6e4);
|
|
245
|
+
throw new Error(t("cli_rateLimited", { minutes: remaining }));
|
|
246
|
+
}
|
|
247
|
+
if (now - tracker.lastAttempt < ACTIVATION_COOLDOWN_MS) {
|
|
248
|
+
throw new Error(t("cli_cooldown"));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function recordAttempt(success) {
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
tracker.lastAttempt = now;
|
|
254
|
+
if (success) {
|
|
255
|
+
tracker.attempts = 0;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
tracker.attempts++;
|
|
259
|
+
if (tracker.attempts >= MAX_ATTEMPTS) {
|
|
260
|
+
tracker.lockedUntil = now + LOCKOUT_MS;
|
|
261
|
+
tracker.attempts = 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
var ENCRYPTION_SECRET2 = "brew-tui-license-aes256gcm-v1";
|
|
265
|
+
var HKDF_SALT2 = "brew-tui-salt-v1";
|
|
266
|
+
var _derivedKey = null;
|
|
267
|
+
var _legacyKey2 = null;
|
|
268
|
+
var _decryptedWithLegacyKey = false;
|
|
269
|
+
async function deriveEncryptionKey2() {
|
|
270
|
+
if (_derivedKey) return _derivedKey;
|
|
271
|
+
const machineId = await getMachineId();
|
|
272
|
+
const derived = hkdfSync2("sha256", ENCRYPTION_SECRET2, HKDF_SALT2, machineId, 32);
|
|
273
|
+
_derivedKey = Buffer.from(derived);
|
|
274
|
+
return _derivedKey;
|
|
275
|
+
}
|
|
276
|
+
function deriveLegacyKey2() {
|
|
277
|
+
if (!_legacyKey2) _legacyKey2 = scryptSync2(ENCRYPTION_SECRET2, HKDF_SALT2, 32);
|
|
278
|
+
return _legacyKey2;
|
|
279
|
+
}
|
|
280
|
+
async function encryptLicenseData(data) {
|
|
281
|
+
const key = await deriveEncryptionKey2();
|
|
282
|
+
const iv = randomBytes2(12);
|
|
283
|
+
const cipher = createCipheriv2("aes-256-gcm", key, iv);
|
|
284
|
+
const plaintext = JSON.stringify(data);
|
|
285
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
286
|
+
const tag = cipher.getAuthTag();
|
|
287
|
+
return {
|
|
288
|
+
encrypted: ciphertext.toString("base64"),
|
|
289
|
+
iv: iv.toString("base64"),
|
|
290
|
+
tag: tag.toString("base64")
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async function decryptLicenseData(encrypted, iv, tag) {
|
|
294
|
+
const ivBuf = Buffer.from(iv, "base64");
|
|
295
|
+
const tagBuf = Buffer.from(tag, "base64");
|
|
296
|
+
const ciphertext = Buffer.from(encrypted, "base64");
|
|
297
|
+
const candidates = [
|
|
298
|
+
[await deriveEncryptionKey2(), false],
|
|
299
|
+
[deriveLegacyKey2(), true]
|
|
300
|
+
];
|
|
301
|
+
let lastErr;
|
|
302
|
+
for (const [key, isLegacy] of candidates) {
|
|
303
|
+
try {
|
|
304
|
+
const decipher = createDecipheriv2("aes-256-gcm", key, ivBuf);
|
|
305
|
+
decipher.setAuthTag(tagBuf);
|
|
306
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
307
|
+
_decryptedWithLegacyKey = isLegacy;
|
|
308
|
+
return JSON.parse(plaintext.toString("utf-8"));
|
|
309
|
+
} catch (err) {
|
|
310
|
+
lastErr = err;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
throw lastErr instanceof Error ? lastErr : new Error("Failed to decrypt license");
|
|
314
|
+
}
|
|
315
|
+
function isLicenseFile(obj) {
|
|
316
|
+
return typeof obj === "object" && obj !== null && obj.version === 1;
|
|
317
|
+
}
|
|
318
|
+
function isEncryptedLicenseFile(obj) {
|
|
319
|
+
if (!isLicenseFile(obj)) return false;
|
|
320
|
+
const record = obj;
|
|
321
|
+
return typeof record.encrypted === "string" && typeof record.iv === "string" && typeof record.tag === "string";
|
|
322
|
+
}
|
|
323
|
+
async function loadLicense() {
|
|
324
|
+
try {
|
|
325
|
+
const raw = await readFile2(LICENSE_PATH, "utf-8");
|
|
326
|
+
const parsed = JSON.parse(raw);
|
|
327
|
+
if (!isLicenseFile(parsed)) {
|
|
328
|
+
throw new Error("Invalid license data format");
|
|
329
|
+
}
|
|
330
|
+
const file = parsed;
|
|
331
|
+
if (file.version !== 1) {
|
|
332
|
+
throw new Error("Unsupported data version");
|
|
333
|
+
}
|
|
334
|
+
if (isEncryptedLicenseFile(file)) {
|
|
335
|
+
const data = await decryptLicenseData(file.encrypted, file.iv, file.tag);
|
|
336
|
+
const fileRecord = file;
|
|
337
|
+
if (fileRecord.machineId) {
|
|
338
|
+
const currentMachineId = await getMachineId();
|
|
339
|
+
if (fileRecord.machineId !== currentMachineId) {
|
|
340
|
+
throw new Error("License was activated on a different machine");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (_decryptedWithLegacyKey) {
|
|
344
|
+
_decryptedWithLegacyKey = false;
|
|
345
|
+
try {
|
|
346
|
+
await saveLicense(data);
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return data;
|
|
351
|
+
}
|
|
352
|
+
if (file.license) {
|
|
353
|
+
const data = file.license;
|
|
354
|
+
await saveLicense(data);
|
|
355
|
+
return data;
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
} catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function saveLicense(data) {
|
|
363
|
+
await ensureDataDirs();
|
|
364
|
+
const { encrypted, iv, tag } = await encryptLicenseData(data);
|
|
365
|
+
const machineId = await getMachineId();
|
|
366
|
+
const file = { version: 1, encrypted, iv, tag, machineId };
|
|
367
|
+
const tmpPath = LICENSE_PATH + ".tmp";
|
|
368
|
+
await writeFile2(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
|
|
369
|
+
await rename2(tmpPath, LICENSE_PATH);
|
|
370
|
+
}
|
|
371
|
+
async function clearLicense() {
|
|
372
|
+
try {
|
|
373
|
+
await rm(LICENSE_PATH);
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function isExpired(license) {
|
|
378
|
+
if (!license.expiresAt) return false;
|
|
379
|
+
const expiry = new Date(license.expiresAt).getTime();
|
|
380
|
+
if (isNaN(expiry)) return true;
|
|
381
|
+
return expiry < Date.now();
|
|
382
|
+
}
|
|
383
|
+
function needsRevalidation(license) {
|
|
384
|
+
const lastValidated = new Date(license.lastValidatedAt).getTime();
|
|
385
|
+
if (isNaN(lastValidated)) return true;
|
|
386
|
+
return Date.now() - lastValidated > REVALIDATION_INTERVAL_MS;
|
|
387
|
+
}
|
|
388
|
+
function isWithinGracePeriod(license) {
|
|
389
|
+
const lastValidated = new Date(license.lastValidatedAt).getTime();
|
|
390
|
+
if (isNaN(lastValidated)) return false;
|
|
391
|
+
return Date.now() - lastValidated < GRACE_PERIOD_MS;
|
|
392
|
+
}
|
|
393
|
+
function getDegradationLevel(license) {
|
|
394
|
+
const lastValidated = new Date(license.lastValidatedAt).getTime();
|
|
395
|
+
if (isNaN(lastValidated)) return "expired";
|
|
396
|
+
const elapsed = Date.now() - lastValidated;
|
|
397
|
+
if (elapsed < 0) return "none";
|
|
398
|
+
const days = elapsed / (24 * 60 * 60 * 1e3);
|
|
399
|
+
if (days <= 7) return "none";
|
|
400
|
+
if (days <= 14) return "warning";
|
|
401
|
+
if (days <= 30) return "limited";
|
|
402
|
+
return "expired";
|
|
403
|
+
}
|
|
404
|
+
function validateLicenseKey(key) {
|
|
405
|
+
if (key.length < 10 || key.length > 100) {
|
|
406
|
+
throw new Error("Invalid license key format");
|
|
407
|
+
}
|
|
408
|
+
if (!/^[\w-]+$/.test(key)) {
|
|
409
|
+
throw new Error("Invalid license key format");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function detectPlan(key) {
|
|
413
|
+
const upper = key.toUpperCase();
|
|
414
|
+
return upper.startsWith("BTUI-T-") || upper.startsWith("BTUI-T_") ? "team" : "pro";
|
|
415
|
+
}
|
|
416
|
+
async function activate(key) {
|
|
417
|
+
validateLicenseKey(key);
|
|
418
|
+
checkRateLimit();
|
|
419
|
+
let success = false;
|
|
420
|
+
try {
|
|
421
|
+
const res = await activateLicense(key);
|
|
422
|
+
if (!res.activated) {
|
|
423
|
+
throw new Error(res.error ?? "Activation failed");
|
|
424
|
+
}
|
|
425
|
+
const license = {
|
|
426
|
+
key,
|
|
427
|
+
instanceId: res.instance.id,
|
|
428
|
+
status: "active",
|
|
429
|
+
customerEmail: res.meta.customer_email,
|
|
430
|
+
customerName: res.meta.customer_name,
|
|
431
|
+
plan: detectPlan(key),
|
|
432
|
+
activatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
433
|
+
expiresAt: res.license_key.expires_at,
|
|
434
|
+
lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
435
|
+
};
|
|
436
|
+
await saveLicense(license);
|
|
437
|
+
success = true;
|
|
438
|
+
return license;
|
|
439
|
+
} finally {
|
|
440
|
+
recordAttempt(success);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function isNetworkError(err) {
|
|
444
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
445
|
+
return /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network|timeout|abort/i.test(msg);
|
|
446
|
+
}
|
|
447
|
+
async function revalidate(license) {
|
|
448
|
+
try {
|
|
449
|
+
const res = await validateLicense(license.key, license.instanceId);
|
|
450
|
+
if (res.valid) {
|
|
451
|
+
const updated = {
|
|
452
|
+
...license,
|
|
453
|
+
lastValidatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
454
|
+
status: "active",
|
|
455
|
+
expiresAt: res.license_key.expires_at
|
|
456
|
+
};
|
|
457
|
+
await saveLicense(updated);
|
|
458
|
+
return "valid";
|
|
459
|
+
}
|
|
460
|
+
await saveLicense({ ...license, status: "expired" });
|
|
461
|
+
return "expired";
|
|
462
|
+
} catch (err) {
|
|
463
|
+
if (isNetworkError(err)) {
|
|
464
|
+
return isWithinGracePeriod(license) ? "grace" : "expired";
|
|
465
|
+
}
|
|
466
|
+
await saveLicense({ ...license, status: "expired" });
|
|
467
|
+
return "expired";
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
async function deactivate(license) {
|
|
471
|
+
let remoteSuccess = false;
|
|
472
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
473
|
+
try {
|
|
474
|
+
await deactivateLicense(license.key, license.instanceId);
|
|
475
|
+
remoteSuccess = true;
|
|
476
|
+
break;
|
|
477
|
+
} catch {
|
|
478
|
+
if (attempt < 2) await new Promise((r) => setTimeout(r, 1e3));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
await clearLicense();
|
|
482
|
+
return { remoteSuccess };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/lib/sync/sync-engine.ts
|
|
486
|
+
var SYNC_CONFIG_PATH = join2(DATA_DIR, "sync-config.json");
|
|
487
|
+
async function loadSyncConfig() {
|
|
488
|
+
try {
|
|
489
|
+
const raw = await readFile3(SYNC_CONFIG_PATH, "utf-8");
|
|
490
|
+
return JSON.parse(raw);
|
|
491
|
+
} catch {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function saveSyncConfig(config) {
|
|
496
|
+
const tmpPath = SYNC_CONFIG_PATH + ".tmp";
|
|
497
|
+
await writeFile3(tmpPath, JSON.stringify(config, null, 2), {
|
|
498
|
+
encoding: "utf-8",
|
|
499
|
+
mode: 384
|
|
500
|
+
});
|
|
501
|
+
await rename3(tmpPath, SYNC_CONFIG_PATH);
|
|
502
|
+
}
|
|
503
|
+
function detectConflicts(localSnapshot, otherMachines, localMachineId) {
|
|
504
|
+
const conflicts = [];
|
|
505
|
+
const localFormulaMap = new Map(localSnapshot.formulae.map((f) => [f.name, f.version]));
|
|
506
|
+
const localCaskMap = new Map(localSnapshot.casks.map((c) => [c.name, c.version]));
|
|
507
|
+
for (const machine of otherMachines) {
|
|
508
|
+
if (machine.machineId === localMachineId) continue;
|
|
509
|
+
for (const remoteFormula of machine.snapshot.formulae) {
|
|
510
|
+
const localVersion = localFormulaMap.get(remoteFormula.name);
|
|
511
|
+
if (localVersion !== void 0 && localVersion !== remoteFormula.version) {
|
|
512
|
+
conflicts.push({
|
|
513
|
+
packageName: remoteFormula.name,
|
|
514
|
+
packageType: "formula",
|
|
515
|
+
localVersion,
|
|
516
|
+
remoteMachine: machine.machineName,
|
|
517
|
+
remoteVersion: remoteFormula.version
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
for (const remoteCask of machine.snapshot.casks) {
|
|
522
|
+
const localVersion = localCaskMap.get(remoteCask.name);
|
|
523
|
+
if (localVersion !== void 0 && localVersion !== remoteCask.version) {
|
|
524
|
+
conflicts.push({
|
|
525
|
+
packageName: remoteCask.name,
|
|
526
|
+
packageType: "cask",
|
|
527
|
+
localVersion,
|
|
528
|
+
remoteMachine: machine.machineName,
|
|
529
|
+
remoteVersion: remoteCask.version
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return conflicts;
|
|
535
|
+
}
|
|
536
|
+
async function writeEnvelope(payload, licenseKey) {
|
|
537
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
538
|
+
const { encrypted, iv, tag } = encryptPayload(payload, licenseKey);
|
|
539
|
+
const envelope = {
|
|
540
|
+
schemaVersion: 1,
|
|
541
|
+
encrypted,
|
|
542
|
+
iv,
|
|
543
|
+
tag,
|
|
544
|
+
updatedAt: now
|
|
545
|
+
};
|
|
546
|
+
await writeSyncEnvelope(envelope);
|
|
547
|
+
return now;
|
|
548
|
+
}
|
|
549
|
+
async function loadLicenseKeyOrThrow() {
|
|
550
|
+
const license = await loadLicense();
|
|
551
|
+
if (!license || !license.key) {
|
|
552
|
+
throw new Error("Sync requires an active license");
|
|
553
|
+
}
|
|
554
|
+
return license.key;
|
|
555
|
+
}
|
|
556
|
+
function mergePayload(existing, localState) {
|
|
557
|
+
return {
|
|
558
|
+
machines: {
|
|
559
|
+
...existing.machines,
|
|
560
|
+
[localState.machineId]: localState
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
async function sync(isPro, currentBrewfile) {
|
|
565
|
+
if (!isPro) {
|
|
566
|
+
throw new Error("Pro license required");
|
|
567
|
+
}
|
|
568
|
+
const available = await isICloudAvailable();
|
|
569
|
+
if (!available) {
|
|
570
|
+
return {
|
|
571
|
+
success: false,
|
|
572
|
+
conflicts: [],
|
|
573
|
+
resolvedCount: 0,
|
|
574
|
+
error: "iCloud Drive not available"
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
const licenseKey = await loadLicenseKeyOrThrow();
|
|
578
|
+
let existingPayload = null;
|
|
579
|
+
try {
|
|
580
|
+
const envelope = await readSyncEnvelope();
|
|
581
|
+
if (envelope) {
|
|
582
|
+
existingPayload = decryptPayload(envelope.encrypted, envelope.iv, envelope.tag, licenseKey);
|
|
583
|
+
}
|
|
584
|
+
} catch (err) {
|
|
585
|
+
logger.warn("sync: could not decrypt existing payload, starting fresh", { error: String(err) });
|
|
586
|
+
existingPayload = null;
|
|
587
|
+
}
|
|
588
|
+
const snapshot = await captureSnapshot();
|
|
589
|
+
const machineId = await getMachineId();
|
|
590
|
+
const machineName = hostname();
|
|
591
|
+
const localState = {
|
|
592
|
+
machineId,
|
|
593
|
+
machineName,
|
|
594
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
595
|
+
snapshot,
|
|
596
|
+
...currentBrewfile ? { brewfile: currentBrewfile } : {}
|
|
597
|
+
};
|
|
598
|
+
const otherMachines = existingPayload ? Object.values(existingPayload.machines).filter((m) => m.machineId !== machineId) : [];
|
|
599
|
+
const conflicts = detectConflicts(snapshot, otherMachines, machineId);
|
|
600
|
+
const basePayload = existingPayload ?? { machines: {} };
|
|
601
|
+
const mergedPayload = mergePayload(basePayload, localState);
|
|
602
|
+
if (conflicts.length > 0) {
|
|
603
|
+
await writeEnvelope(mergedPayload, licenseKey);
|
|
604
|
+
return {
|
|
605
|
+
success: false,
|
|
606
|
+
conflicts,
|
|
607
|
+
resolvedCount: 0
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
const now = await writeEnvelope(mergedPayload, licenseKey);
|
|
611
|
+
const existingConfig = await loadSyncConfig();
|
|
612
|
+
await saveSyncConfig({
|
|
613
|
+
enabled: true,
|
|
614
|
+
machineId,
|
|
615
|
+
machineName,
|
|
616
|
+
...existingConfig ?? {},
|
|
617
|
+
lastSync: now
|
|
618
|
+
});
|
|
619
|
+
logger.info("sync: completed successfully", { machineId, machines: Object.keys(mergedPayload.machines).length });
|
|
620
|
+
return {
|
|
621
|
+
success: true,
|
|
622
|
+
conflicts: [],
|
|
623
|
+
resolvedCount: 0
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
async function applyConflictResolutions(payload, resolutions, localMachineId) {
|
|
627
|
+
const updatedPayload = {
|
|
628
|
+
machines: { ...payload.machines }
|
|
629
|
+
};
|
|
630
|
+
for (const { conflict, resolution } of resolutions) {
|
|
631
|
+
if (resolution !== "use-remote") continue;
|
|
632
|
+
const localMachine = updatedPayload.machines[localMachineId];
|
|
633
|
+
if (!localMachine) {
|
|
634
|
+
logger.warn("sync: cannot apply resolution, local machine missing in payload", { localMachineId });
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (conflict.packageType === "formula") {
|
|
638
|
+
updatedPayload.machines[localMachineId] = {
|
|
639
|
+
...localMachine,
|
|
640
|
+
snapshot: {
|
|
641
|
+
...localMachine.snapshot,
|
|
642
|
+
formulae: localMachine.snapshot.formulae.map(
|
|
643
|
+
(f) => f.name === conflict.packageName ? { ...f, version: conflict.remoteVersion } : f
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
} else {
|
|
648
|
+
updatedPayload.machines[localMachineId] = {
|
|
649
|
+
...localMachine,
|
|
650
|
+
snapshot: {
|
|
651
|
+
...localMachine.snapshot,
|
|
652
|
+
casks: localMachine.snapshot.casks.map(
|
|
653
|
+
(c) => c.name === conflict.packageName ? { ...c, version: conflict.remoteVersion } : c
|
|
654
|
+
)
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const licenseKey = await loadLicenseKeyOrThrow();
|
|
660
|
+
await writeEnvelope(updatedPayload, licenseKey);
|
|
661
|
+
logger.info("sync: conflict resolutions applied", { count: resolutions.length });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
export {
|
|
665
|
+
loadLicense,
|
|
666
|
+
isExpired,
|
|
667
|
+
needsRevalidation,
|
|
668
|
+
getDegradationLevel,
|
|
669
|
+
activate,
|
|
670
|
+
revalidate,
|
|
671
|
+
deactivate,
|
|
672
|
+
decryptPayload,
|
|
673
|
+
readSyncEnvelope,
|
|
674
|
+
loadSyncConfig,
|
|
675
|
+
saveSyncConfig,
|
|
676
|
+
sync,
|
|
677
|
+
applyConflictResolutions
|
|
678
|
+
};
|
|
679
|
+
//# sourceMappingURL=chunk-QPXROTAP.js.map
|