brew-tui 2.2.2 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/{brew-tui-bar-installer-GTV4OEZW.js → brew-tui-bar-installer-PCHNYMZL.js} +5 -3
- package/build/{brewfile-manager-HWTPBXPO.js → brewfile-manager-DNRM6CQ7.js} +3 -3
- package/build/chunk-7R4ME2NC.js +342 -0
- package/build/chunk-7R4ME2NC.js.map +1 -0
- package/build/chunk-CCAT52XY.js +138 -0
- package/build/chunk-CCAT52XY.js.map +1 -0
- package/build/{chunk-YUE5NRTE.js → chunk-I5VZR55J.js} +23 -3
- package/build/chunk-I5VZR55J.js.map +1 -0
- package/build/{version-check-QY3SQ6XI.js → chunk-KR6EAHEE.js} +6 -5
- package/build/{chunk-ZC23DNMK.js → chunk-PYDQHHI2.js} +22 -354
- package/build/chunk-PYDQHHI2.js.map +1 -0
- package/build/{chunk-F2S7TGCS.js → chunk-SDQYHY2L.js} +3 -1
- package/build/chunk-SDQYHY2L.js.map +1 -0
- package/build/{chunk-Y45AXONF.js → chunk-UZMGXQKF.js} +2 -2
- package/build/doctor-D56LDODR.js +133 -0
- package/build/doctor-D56LDODR.js.map +1 -0
- package/build/index.js +60 -162
- package/build/index.js.map +1 -1
- package/build/postinstall.js +10 -5
- package/build/postinstall.js.map +1 -1
- package/build/{sync-engine-G5ML7TJ5.js → sync-engine-KTH4K3NG.js} +4 -3
- package/build/version-check-UUJMLUK6.js +15 -0
- package/build/version-check-UUJMLUK6.js.map +1 -0
- package/package.json +1 -1
- package/build/chunk-F2S7TGCS.js.map +0 -1
- package/build/chunk-YUE5NRTE.js.map +0 -1
- package/build/chunk-ZC23DNMK.js.map +0 -1
- /package/build/{brew-tui-bar-installer-GTV4OEZW.js.map → brew-tui-bar-installer-PCHNYMZL.js.map} +0 -0
- /package/build/{brewfile-manager-HWTPBXPO.js.map → brewfile-manager-DNRM6CQ7.js.map} +0 -0
- /package/build/{version-check-QY3SQ6XI.js.map → chunk-KR6EAHEE.js.map} +0 -0
- /package/build/{chunk-Y45AXONF.js.map → chunk-UZMGXQKF.js.map} +0 -0
- /package/build/{sync-engine-G5ML7TJ5.js.map → sync-engine-KTH4K3NG.js.map} +0 -0
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
+
bundleIdAt,
|
|
2
3
|
installBrewTUIBar,
|
|
3
4
|
isBrewTUIBarInstalled,
|
|
4
5
|
isBrewTUIBarRunning,
|
|
5
6
|
launchBrewTUIBar,
|
|
6
7
|
syncAndLaunchBrewTUIBar,
|
|
7
8
|
uninstallBrewTUIBar
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-I5VZR55J.js";
|
|
9
10
|
import "./chunk-NRRQECXA.js";
|
|
10
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-SDQYHY2L.js";
|
|
11
12
|
import "./chunk-KDHEUNRI.js";
|
|
12
13
|
export {
|
|
14
|
+
bundleIdAt,
|
|
13
15
|
installBrewTUIBar,
|
|
14
16
|
isBrewTUIBarInstalled,
|
|
15
17
|
isBrewTUIBarRunning,
|
|
@@ -17,4 +19,4 @@ export {
|
|
|
17
19
|
syncAndLaunchBrewTUIBar,
|
|
18
20
|
uninstallBrewTUIBar
|
|
19
21
|
};
|
|
20
|
-
//# sourceMappingURL=brew-tui-bar-installer-
|
|
22
|
+
//# sourceMappingURL=brew-tui-bar-installer-PCHNYMZL.js.map
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
loadBrewfile,
|
|
6
6
|
reconcile,
|
|
7
7
|
saveBrewfile
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-UZMGXQKF.js";
|
|
9
|
+
import "./chunk-SDQYHY2L.js";
|
|
10
10
|
import "./chunk-OXDZ4DCK.js";
|
|
11
11
|
import "./chunk-KDHEUNRI.js";
|
|
12
12
|
import "./chunk-LFGDNAXH.js";
|
|
@@ -18,4 +18,4 @@ export {
|
|
|
18
18
|
reconcile,
|
|
19
19
|
saveBrewfile
|
|
20
20
|
};
|
|
21
|
-
//# sourceMappingURL=brewfile-manager-
|
|
21
|
+
//# sourceMappingURL=brewfile-manager-DNRM6CQ7.js.map
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadLicense
|
|
3
|
+
} from "./chunk-PYDQHHI2.js";
|
|
4
|
+
import {
|
|
5
|
+
captureSnapshot
|
|
6
|
+
} from "./chunk-OXDZ4DCK.js";
|
|
7
|
+
import {
|
|
8
|
+
logger
|
|
9
|
+
} from "./chunk-KDHEUNRI.js";
|
|
10
|
+
import {
|
|
11
|
+
DATA_DIR,
|
|
12
|
+
getMachineId
|
|
13
|
+
} from "./chunk-LFGDNAXH.js";
|
|
14
|
+
|
|
15
|
+
// src/lib/sync/sync-engine.ts
|
|
16
|
+
import { readFile as readFile2, writeFile as writeFile2, rename as rename2 } from "fs/promises";
|
|
17
|
+
import { join as join2 } from "path";
|
|
18
|
+
import { hostname } from "os";
|
|
19
|
+
|
|
20
|
+
// src/lib/sync/crypto.ts
|
|
21
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from "crypto";
|
|
22
|
+
|
|
23
|
+
// src/lib/sync/types.ts
|
|
24
|
+
function isSyncPayload(value) {
|
|
25
|
+
if (typeof value !== "object" || value === null) return false;
|
|
26
|
+
const machines = value.machines;
|
|
27
|
+
if (typeof machines !== "object" || machines === null || Array.isArray(machines)) return false;
|
|
28
|
+
for (const m of Object.values(machines)) {
|
|
29
|
+
if (typeof m !== "object" || m === null) return false;
|
|
30
|
+
const state = m;
|
|
31
|
+
if (typeof state.machineId !== "string" || typeof state.machineName !== "string" || typeof state.updatedAt !== "string" || typeof state.snapshot !== "object") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/lib/sync/crypto.ts
|
|
39
|
+
var ENCRYPTION_SECRET = "brew-tui-sync-aes256gcm-v1";
|
|
40
|
+
var HKDF_SALT = "brew-tui-sync-salt-v1";
|
|
41
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
42
|
+
var _legacyKey = null;
|
|
43
|
+
function deriveEncryptionKey(licenseKey) {
|
|
44
|
+
const cached = keyCache.get(licenseKey);
|
|
45
|
+
if (cached) return cached;
|
|
46
|
+
const derived = Buffer.from(hkdfSync("sha256", ENCRYPTION_SECRET, HKDF_SALT, licenseKey, 32));
|
|
47
|
+
keyCache.set(licenseKey, derived);
|
|
48
|
+
return derived;
|
|
49
|
+
}
|
|
50
|
+
function deriveLegacyKey() {
|
|
51
|
+
if (!_legacyKey) {
|
|
52
|
+
_legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32, { N: 16384, r: 8, p: 1 });
|
|
53
|
+
}
|
|
54
|
+
return _legacyKey;
|
|
55
|
+
}
|
|
56
|
+
function encryptPayload(data, licenseKey) {
|
|
57
|
+
const key = deriveEncryptionKey(licenseKey);
|
|
58
|
+
const iv = randomBytes(12);
|
|
59
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
60
|
+
const plaintext = JSON.stringify(data);
|
|
61
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
62
|
+
const tag = cipher.getAuthTag();
|
|
63
|
+
return {
|
|
64
|
+
encrypted: ciphertext.toString("base64"),
|
|
65
|
+
iv: iv.toString("base64"),
|
|
66
|
+
tag: tag.toString("base64")
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function decryptPayload(encrypted, iv, tag, licenseKey) {
|
|
70
|
+
const ivBuf = Buffer.from(iv, "base64");
|
|
71
|
+
const tagBuf = Buffer.from(tag, "base64");
|
|
72
|
+
const ciphertext = Buffer.from(encrypted, "base64");
|
|
73
|
+
for (const key of [deriveEncryptionKey(licenseKey), deriveLegacyKey()]) {
|
|
74
|
+
try {
|
|
75
|
+
const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
|
|
76
|
+
decipher.setAuthTag(tagBuf);
|
|
77
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
78
|
+
const parsed = JSON.parse(plaintext.toString("utf-8"));
|
|
79
|
+
if (!isSyncPayload(parsed)) throw new Error("Invalid sync payload shape");
|
|
80
|
+
return parsed;
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw new Error("Failed to decrypt sync payload");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/lib/sync/backends/icloud-backend.ts
|
|
88
|
+
import { readFile, writeFile, rename, mkdir, stat } from "fs/promises";
|
|
89
|
+
import { homedir } from "os";
|
|
90
|
+
import { join } from "path";
|
|
91
|
+
var ICLOUD_BASE = join(
|
|
92
|
+
homedir(),
|
|
93
|
+
"Library",
|
|
94
|
+
"Mobile Documents",
|
|
95
|
+
"com~apple~CloudDocs"
|
|
96
|
+
);
|
|
97
|
+
var ICLOUD_SYNC_DIR = join(ICLOUD_BASE, "BrewTUI");
|
|
98
|
+
var ICLOUD_SYNC_PATH = join(ICLOUD_SYNC_DIR, "sync.json");
|
|
99
|
+
async function isICloudAvailable() {
|
|
100
|
+
try {
|
|
101
|
+
await stat(ICLOUD_BASE);
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function isValidEnvelope(v) {
|
|
108
|
+
if (!v || typeof v !== "object") return false;
|
|
109
|
+
const obj = v;
|
|
110
|
+
return obj["schemaVersion"] === 1 && typeof obj["encrypted"] === "string" && typeof obj["iv"] === "string" && typeof obj["tag"] === "string" && typeof obj["updatedAt"] === "string";
|
|
111
|
+
}
|
|
112
|
+
async function readSyncEnvelope() {
|
|
113
|
+
try {
|
|
114
|
+
const info = await stat(ICLOUD_SYNC_PATH);
|
|
115
|
+
if (info.size === 0) {
|
|
116
|
+
logger.warn("sync: iCloud envelope exists but is empty (placeholder?)");
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
121
|
+
try {
|
|
122
|
+
const placeholder = ICLOUD_SYNC_PATH.replace(/sync\.json$/, ".sync.json.icloud");
|
|
123
|
+
await stat(placeholder);
|
|
124
|
+
logger.warn("sync: iCloud placeholder present, file not yet downloaded");
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
logger.warn("sync: could not stat iCloud envelope", { error: String(err) });
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const raw = await readFile(ICLOUD_SYNC_PATH, "utf-8");
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
if (!isValidEnvelope(parsed)) {
|
|
136
|
+
logger.warn("sync: invalid envelope structure in iCloud file");
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return parsed;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
logger.warn("sync: could not read iCloud envelope", { error: String(err) });
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function writeSyncEnvelope(envelope) {
|
|
146
|
+
await mkdir(ICLOUD_SYNC_DIR, { recursive: true, mode: 448 });
|
|
147
|
+
const tmpPath = ICLOUD_SYNC_PATH + ".tmp";
|
|
148
|
+
await writeFile(tmpPath, JSON.stringify(envelope, null, 2), {
|
|
149
|
+
encoding: "utf-8",
|
|
150
|
+
mode: 384
|
|
151
|
+
});
|
|
152
|
+
await rename(tmpPath, ICLOUD_SYNC_PATH);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/lib/sync/sync-engine.ts
|
|
156
|
+
var SYNC_CONFIG_PATH = join2(DATA_DIR, "sync-config.json");
|
|
157
|
+
async function loadSyncConfig() {
|
|
158
|
+
try {
|
|
159
|
+
const raw = await readFile2(SYNC_CONFIG_PATH, "utf-8");
|
|
160
|
+
return JSON.parse(raw);
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function saveSyncConfig(config) {
|
|
166
|
+
const tmpPath = SYNC_CONFIG_PATH + ".tmp";
|
|
167
|
+
await writeFile2(tmpPath, JSON.stringify(config, null, 2), {
|
|
168
|
+
encoding: "utf-8",
|
|
169
|
+
mode: 384
|
|
170
|
+
});
|
|
171
|
+
await rename2(tmpPath, SYNC_CONFIG_PATH);
|
|
172
|
+
}
|
|
173
|
+
function detectConflicts(localSnapshot, otherMachines, localMachineId) {
|
|
174
|
+
const conflicts = [];
|
|
175
|
+
const localFormulaMap = new Map(localSnapshot.formulae.map((f) => [f.name, f.version]));
|
|
176
|
+
const localCaskMap = new Map(localSnapshot.casks.map((c) => [c.name, c.version]));
|
|
177
|
+
for (const machine of otherMachines) {
|
|
178
|
+
if (machine.machineId === localMachineId) continue;
|
|
179
|
+
for (const remoteFormula of machine.snapshot.formulae) {
|
|
180
|
+
const localVersion = localFormulaMap.get(remoteFormula.name);
|
|
181
|
+
if (localVersion !== void 0 && localVersion !== remoteFormula.version) {
|
|
182
|
+
conflicts.push({
|
|
183
|
+
packageName: remoteFormula.name,
|
|
184
|
+
packageType: "formula",
|
|
185
|
+
localVersion,
|
|
186
|
+
remoteMachine: machine.machineName,
|
|
187
|
+
remoteVersion: remoteFormula.version
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const remoteCask of machine.snapshot.casks) {
|
|
192
|
+
const localVersion = localCaskMap.get(remoteCask.name);
|
|
193
|
+
if (localVersion !== void 0 && localVersion !== remoteCask.version) {
|
|
194
|
+
conflicts.push({
|
|
195
|
+
packageName: remoteCask.name,
|
|
196
|
+
packageType: "cask",
|
|
197
|
+
localVersion,
|
|
198
|
+
remoteMachine: machine.machineName,
|
|
199
|
+
remoteVersion: remoteCask.version
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return conflicts;
|
|
205
|
+
}
|
|
206
|
+
async function writeEnvelope(payload, licenseKey) {
|
|
207
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
208
|
+
const { encrypted, iv, tag } = encryptPayload(payload, licenseKey);
|
|
209
|
+
const envelope = {
|
|
210
|
+
schemaVersion: 1,
|
|
211
|
+
encrypted,
|
|
212
|
+
iv,
|
|
213
|
+
tag,
|
|
214
|
+
updatedAt: now
|
|
215
|
+
};
|
|
216
|
+
await writeSyncEnvelope(envelope);
|
|
217
|
+
return now;
|
|
218
|
+
}
|
|
219
|
+
async function loadLicenseKeyOrThrow() {
|
|
220
|
+
const license = await loadLicense();
|
|
221
|
+
if (!license || !license.key) {
|
|
222
|
+
throw new Error("Sync requires an active license");
|
|
223
|
+
}
|
|
224
|
+
return license.key;
|
|
225
|
+
}
|
|
226
|
+
function mergePayload(existing, localState) {
|
|
227
|
+
return {
|
|
228
|
+
machines: {
|
|
229
|
+
...existing.machines,
|
|
230
|
+
[localState.machineId]: localState
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function sync(isPro, currentBrewfile) {
|
|
235
|
+
if (!isPro) {
|
|
236
|
+
throw new Error("Pro license required");
|
|
237
|
+
}
|
|
238
|
+
const available = await isICloudAvailable();
|
|
239
|
+
if (!available) {
|
|
240
|
+
return {
|
|
241
|
+
success: false,
|
|
242
|
+
conflicts: [],
|
|
243
|
+
resolvedCount: 0,
|
|
244
|
+
error: "iCloud Drive not available"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const licenseKey = await loadLicenseKeyOrThrow();
|
|
248
|
+
let existingPayload = null;
|
|
249
|
+
try {
|
|
250
|
+
const envelope = await readSyncEnvelope();
|
|
251
|
+
if (envelope) {
|
|
252
|
+
existingPayload = decryptPayload(envelope.encrypted, envelope.iv, envelope.tag, licenseKey);
|
|
253
|
+
}
|
|
254
|
+
} catch (err) {
|
|
255
|
+
logger.warn("sync: could not decrypt existing payload, starting fresh", { error: String(err) });
|
|
256
|
+
existingPayload = null;
|
|
257
|
+
}
|
|
258
|
+
const snapshot = await captureSnapshot();
|
|
259
|
+
const machineId = await getMachineId();
|
|
260
|
+
const machineName = hostname();
|
|
261
|
+
const localState = {
|
|
262
|
+
machineId,
|
|
263
|
+
machineName,
|
|
264
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
265
|
+
snapshot,
|
|
266
|
+
...currentBrewfile ? { brewfile: currentBrewfile } : {}
|
|
267
|
+
};
|
|
268
|
+
const otherMachines = existingPayload ? Object.values(existingPayload.machines).filter((m) => m.machineId !== machineId) : [];
|
|
269
|
+
const conflicts = detectConflicts(snapshot, otherMachines, machineId);
|
|
270
|
+
const basePayload = existingPayload ?? { machines: {} };
|
|
271
|
+
const mergedPayload = mergePayload(basePayload, localState);
|
|
272
|
+
if (conflicts.length > 0) {
|
|
273
|
+
await writeEnvelope(mergedPayload, licenseKey);
|
|
274
|
+
return {
|
|
275
|
+
success: false,
|
|
276
|
+
conflicts,
|
|
277
|
+
resolvedCount: 0
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const now = await writeEnvelope(mergedPayload, licenseKey);
|
|
281
|
+
const existingConfig = await loadSyncConfig();
|
|
282
|
+
await saveSyncConfig({
|
|
283
|
+
enabled: true,
|
|
284
|
+
machineId,
|
|
285
|
+
machineName,
|
|
286
|
+
...existingConfig ?? {},
|
|
287
|
+
lastSync: now
|
|
288
|
+
});
|
|
289
|
+
logger.info("sync: completed successfully", { machineId, machines: Object.keys(mergedPayload.machines).length });
|
|
290
|
+
return {
|
|
291
|
+
success: true,
|
|
292
|
+
conflicts: [],
|
|
293
|
+
resolvedCount: 0
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
async function applyConflictResolutions(payload, resolutions, localMachineId) {
|
|
297
|
+
const updatedPayload = {
|
|
298
|
+
machines: { ...payload.machines }
|
|
299
|
+
};
|
|
300
|
+
for (const { conflict, resolution } of resolutions) {
|
|
301
|
+
if (resolution !== "use-remote") continue;
|
|
302
|
+
const localMachine = updatedPayload.machines[localMachineId];
|
|
303
|
+
if (!localMachine) {
|
|
304
|
+
logger.warn("sync: cannot apply resolution, local machine missing in payload", { localMachineId });
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (conflict.packageType === "formula") {
|
|
308
|
+
updatedPayload.machines[localMachineId] = {
|
|
309
|
+
...localMachine,
|
|
310
|
+
snapshot: {
|
|
311
|
+
...localMachine.snapshot,
|
|
312
|
+
formulae: localMachine.snapshot.formulae.map(
|
|
313
|
+
(f) => f.name === conflict.packageName ? { ...f, version: conflict.remoteVersion } : f
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
} else {
|
|
318
|
+
updatedPayload.machines[localMachineId] = {
|
|
319
|
+
...localMachine,
|
|
320
|
+
snapshot: {
|
|
321
|
+
...localMachine.snapshot,
|
|
322
|
+
casks: localMachine.snapshot.casks.map(
|
|
323
|
+
(c) => c.name === conflict.packageName ? { ...c, version: conflict.remoteVersion } : c
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const licenseKey = await loadLicenseKeyOrThrow();
|
|
330
|
+
await writeEnvelope(updatedPayload, licenseKey);
|
|
331
|
+
logger.info("sync: conflict resolutions applied", { count: resolutions.length });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export {
|
|
335
|
+
decryptPayload,
|
|
336
|
+
readSyncEnvelope,
|
|
337
|
+
loadSyncConfig,
|
|
338
|
+
saveSyncConfig,
|
|
339
|
+
sync,
|
|
340
|
+
applyConflictResolutions
|
|
341
|
+
};
|
|
342
|
+
//# sourceMappingURL=chunk-7R4ME2NC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/sync/sync-engine.ts","../src/lib/sync/crypto.ts","../src/lib/sync/types.ts","../src/lib/sync/backends/icloud-backend.ts"],"sourcesContent":["import { readFile, writeFile, rename } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { hostname } from 'node:os';\nimport { encryptPayload, decryptPayload } from './crypto.js';\nimport {\n readSyncEnvelope,\n writeSyncEnvelope,\n isICloudAvailable,\n} from './backends/icloud-backend.js';\nimport { captureSnapshot } from '../state-snapshot/snapshot.js';\nimport { DATA_DIR, getMachineId } from '../data-dir.js';\nimport { loadLicense } from '../license/license-manager.js';\nimport { logger } from '../../utils/logger.js';\nimport type {\n SyncConfig,\n SyncPayload,\n SyncConflict,\n SyncResult,\n MachineState,\n SyncEnvelope,\n} from './types.js';\nimport type { BrewfileSchema } from '../brewfile/types.js';\n\nconst SYNC_CONFIG_PATH = join(DATA_DIR, 'sync-config.json');\n\n// ── Config I/O ──────────────────────────────────────────────────────────────\n\nexport async function loadSyncConfig(): Promise<SyncConfig | null> {\n try {\n const raw = await readFile(SYNC_CONFIG_PATH, 'utf-8');\n return JSON.parse(raw) as SyncConfig;\n } catch {\n return null;\n }\n}\n\nexport async function saveSyncConfig(config: SyncConfig): Promise<void> {\n const tmpPath = SYNC_CONFIG_PATH + '.tmp';\n await writeFile(tmpPath, JSON.stringify(config, null, 2), {\n encoding: 'utf-8',\n mode: 0o600,\n });\n await rename(tmpPath, SYNC_CONFIG_PATH);\n}\n\n// ── Machine ID ───────────────────────────────────────────────────────────────\n// Single canonical implementation lives in data-dir.ts. The previous fallback\n// to os.hostname() here meant two different machines with the same hostname\n// (common on freshly-imaged corporate fleets) collided in sync state.\n\nexport { getMachineId };\n\n// ── Conflict detection ───────────────────────────────────────────────────────\n\nfunction detectConflicts(\n localSnapshot: { formulae: Array<{ name: string; version: string }>; casks: Array<{ name: string; version: string }> },\n otherMachines: MachineState[],\n localMachineId: string,\n): SyncConflict[] {\n const conflicts: SyncConflict[] = [];\n\n const localFormulaMap = new Map(localSnapshot.formulae.map((f) => [f.name, f.version]));\n const localCaskMap = new Map(localSnapshot.casks.map((c) => [c.name, c.version]));\n\n for (const machine of otherMachines) {\n if (machine.machineId === localMachineId) continue;\n\n // Check formula conflicts: same package, different version on both machines\n for (const remoteFormula of machine.snapshot.formulae) {\n const localVersion = localFormulaMap.get(remoteFormula.name);\n if (localVersion !== undefined && localVersion !== remoteFormula.version) {\n conflicts.push({\n packageName: remoteFormula.name,\n packageType: 'formula',\n localVersion,\n remoteMachine: machine.machineName,\n remoteVersion: remoteFormula.version,\n });\n }\n }\n\n // Check cask conflicts\n for (const remoteCask of machine.snapshot.casks) {\n const localVersion = localCaskMap.get(remoteCask.name);\n if (localVersion !== undefined && localVersion !== remoteCask.version) {\n conflicts.push({\n packageName: remoteCask.name,\n packageType: 'cask',\n localVersion,\n remoteMachine: machine.machineName,\n remoteVersion: remoteCask.version,\n });\n }\n }\n }\n\n return conflicts;\n}\n\n// ── Merge ────────────────────────────────────────────────────────────────────\n\nasync function writeEnvelope(payload: SyncPayload, licenseKey: string): Promise<string> {\n const now = new Date().toISOString();\n const { encrypted, iv, tag } = encryptPayload(payload, licenseKey);\n const envelope: SyncEnvelope = {\n schemaVersion: 1,\n encrypted,\n iv,\n tag,\n updatedAt: now,\n };\n await writeSyncEnvelope(envelope);\n return now;\n}\n\nasync function loadLicenseKeyOrThrow(): Promise<string> {\n // Sync requires Pro, and Pro requires a license. Read it lazily so\n // sync-store callers don't have to plumb the key through every call.\n const license = await loadLicense();\n if (!license || !license.key) {\n throw new Error('Sync requires an active license');\n }\n return license.key;\n}\n\nfunction mergePayload(existing: SyncPayload, localState: MachineState): SyncPayload {\n return {\n machines: {\n ...existing.machines,\n [localState.machineId]: localState,\n },\n };\n}\n\n// ── Main sync function ───────────────────────────────────────────────────────\n\nexport async function sync(\n isPro: boolean,\n currentBrewfile?: BrewfileSchema,\n): Promise<SyncResult> {\n if (!isPro) {\n throw new Error('Pro license required');\n }\n\n const available = await isICloudAvailable();\n if (!available) {\n return {\n success: false,\n conflicts: [],\n resolvedCount: 0,\n error: 'iCloud Drive not available',\n };\n }\n\n const licenseKey = await loadLicenseKeyOrThrow();\n\n let existingPayload: SyncPayload | null = null;\n\n try {\n const envelope = await readSyncEnvelope();\n if (envelope) {\n existingPayload = decryptPayload(envelope.encrypted, envelope.iv, envelope.tag, licenseKey);\n }\n } catch (err) {\n logger.warn('sync: could not decrypt existing payload, starting fresh', { error: String(err) });\n existingPayload = null;\n }\n\n // Capture current local state\n const snapshot = await captureSnapshot();\n const machineId = await getMachineId();\n const machineName = hostname();\n\n const localState: MachineState = {\n machineId,\n machineName,\n updatedAt: new Date().toISOString(),\n snapshot,\n ...(currentBrewfile ? { brewfile: currentBrewfile } : {}),\n };\n\n // Detect conflicts against other machines in the payload\n const otherMachines = existingPayload\n ? Object.values(existingPayload.machines).filter((m) => m.machineId !== machineId)\n : [];\n\n const conflicts = detectConflicts(snapshot, otherMachines, machineId);\n\n // Always write the local machine state to the payload, even when conflicts\n // exist, so that applyConflictResolutions() has a local entry to update.\n // Without this, the iCloud envelope keeps only remote machines, and\n // resolution updates are silently dropped (they require localMachine to exist).\n const basePayload: SyncPayload = existingPayload ?? { machines: {} };\n const mergedPayload = mergePayload(basePayload, localState);\n\n if (conflicts.length > 0) {\n // Persist local state, then surface conflicts so the user can resolve them.\n await writeEnvelope(mergedPayload, licenseKey);\n return {\n success: false,\n conflicts,\n resolvedCount: 0,\n };\n }\n\n const now = await writeEnvelope(mergedPayload, licenseKey);\n\n // Update local sync config\n const existingConfig = await loadSyncConfig();\n await saveSyncConfig({\n enabled: true,\n machineId,\n machineName,\n ...(existingConfig ?? {}),\n lastSync: now,\n });\n\n logger.info('sync: completed successfully', { machineId, machines: Object.keys(mergedPayload.machines).length });\n\n return {\n success: true,\n conflicts: [],\n resolvedCount: 0,\n };\n}\n\n// ── Conflict resolution ──────────────────────────────────────────────────────\n\nexport async function applyConflictResolutions(\n payload: SyncPayload,\n resolutions: Array<{ conflict: SyncConflict; resolution: 'use-local' | 'use-remote' }>,\n localMachineId: string,\n): Promise<void> {\n // Work on a mutable copy\n const updatedPayload: SyncPayload = {\n machines: { ...payload.machines },\n };\n\n for (const { conflict, resolution } of resolutions) {\n if (resolution !== 'use-remote') continue;\n // Re-read latest local machine on every iteration so consecutive resolutions\n // build on top of each other instead of overwriting prior changes.\n const localMachine = updatedPayload.machines[localMachineId];\n if (!localMachine) {\n logger.warn('sync: cannot apply resolution, local machine missing in payload', { localMachineId });\n continue;\n }\n if (conflict.packageType === 'formula') {\n updatedPayload.machines[localMachineId] = {\n ...localMachine,\n snapshot: {\n ...localMachine.snapshot,\n formulae: localMachine.snapshot.formulae.map((f) =>\n f.name === conflict.packageName\n ? { ...f, version: conflict.remoteVersion }\n : f,\n ),\n },\n };\n } else {\n updatedPayload.machines[localMachineId] = {\n ...localMachine,\n snapshot: {\n ...localMachine.snapshot,\n casks: localMachine.snapshot.casks.map((c) =>\n c.name === conflict.packageName\n ? { ...c, version: conflict.remoteVersion }\n : c,\n ),\n },\n };\n }\n }\n\n const licenseKey = await loadLicenseKeyOrThrow();\n await writeEnvelope(updatedPayload, licenseKey);\n logger.info('sync: conflict resolutions applied', { count: resolutions.length });\n}\n","import { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from 'node:crypto';\nimport { isSyncPayload, type SyncPayload } from './types.js';\n\n// SEG-003: Cross-machine sync encryption.\n// The two constants below are public (compiled into the npm bundle). The\n// per-user secret factor is the Polar license key, which only the user's\n// own machines hold and which Polar issues — so any two of the user's\n// machines derive the same key, but bundle + iCloud snoop is no longer\n// enough to decrypt: the attacker also needs the license key.\n//\n// HKDF-SHA256 over scrypt: the license key is high-entropy by construction\n// (Polar issues UUID-style keys), so the cost-hardening of scrypt isn't\n// what's protecting the key — the secrecy of the license key is. HKDF is\n// also faster, so machines don't pay scrypt's CPU tax on every sync.\nconst ENCRYPTION_SECRET = 'brew-tui-sync-aes256gcm-v1';\nconst HKDF_SALT = 'brew-tui-sync-salt-v1';\n\nconst keyCache = new Map<string, Buffer>();\nlet _legacyKey: Buffer | null = null;\n\nfunction deriveEncryptionKey(licenseKey: string): Buffer {\n const cached = keyCache.get(licenseKey);\n if (cached) return cached;\n const derived = Buffer.from(hkdfSync('sha256', ENCRYPTION_SECRET, HKDF_SALT, licenseKey, 32));\n keyCache.set(licenseKey, derived);\n return derived;\n}\n\n// Legacy key — scrypt(SECRET, SALT), no license-key factor. Used as a\n// decryption fallback for envelopes written by 0.6.2 and earlier.\n// TODO(SEG-003, 0.6.3): remove `_legacyKey` after telemetry confirms zero\n// fallback decrypts in the wild.\nfunction deriveLegacyKey(): Buffer {\n if (!_legacyKey) {\n _legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32, { N: 16384, r: 8, p: 1 });\n }\n return _legacyKey;\n}\n\nexport function encryptPayload(data: SyncPayload, licenseKey: string): { encrypted: string; iv: string; tag: string } {\n const key = deriveEncryptionKey(licenseKey);\n const iv = randomBytes(12); // 96-bit IV for GCM\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n\n const plaintext = JSON.stringify(data);\n const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n return {\n encrypted: ciphertext.toString('base64'),\n iv: iv.toString('base64'),\n tag: tag.toString('base64'),\n };\n}\n\nexport function decryptPayload(encrypted: string, iv: string, tag: string, licenseKey: string): SyncPayload {\n const ivBuf = Buffer.from(iv, 'base64');\n const tagBuf = Buffer.from(tag, 'base64');\n const ciphertext = Buffer.from(encrypted, 'base64');\n\n // Try the licenseKey-bound key first; fall back to the legacy bundle-only\n // key for envelopes written by 0.6.2 and earlier. Re-encryption happens\n // automatically on the next sync write because writeEnvelope always uses\n // the current key.\n for (const key of [deriveEncryptionKey(licenseKey), deriveLegacyKey()]) {\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, ivBuf);\n decipher.setAuthTag(tagBuf);\n const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n const parsed: unknown = JSON.parse(plaintext.toString('utf-8'));\n if (!isSyncPayload(parsed)) throw new Error('Invalid sync payload shape');\n return parsed;\n } catch { /* try next */ }\n }\n throw new Error('Failed to decrypt sync payload');\n}\n","import type { BrewSnapshot } from '../state-snapshot/snapshot.js';\nimport type { BrewfileSchema } from '../brewfile/types.js';\n\nexport interface SyncConfig {\n enabled: boolean;\n machineId: string;\n machineName: string;\n lastSync?: string; // ISO 8601\n}\n\nexport interface MachineState {\n machineId: string;\n machineName: string;\n updatedAt: string; // ISO 8601\n snapshot: BrewSnapshot;\n brewfile?: BrewfileSchema;\n}\n\nexport interface SyncPayload {\n machines: Record<string, MachineState>;\n}\n\n// BK-008: type guard for sync envelopes after AES-GCM decrypt. Defends against\n// truncated or migrated payloads landing as undefined accesses downstream.\nexport function isSyncPayload(value: unknown): value is SyncPayload {\n if (typeof value !== 'object' || value === null) return false;\n const machines = (value as Record<string, unknown>).machines;\n if (typeof machines !== 'object' || machines === null || Array.isArray(machines)) return false;\n for (const m of Object.values(machines as Record<string, unknown>)) {\n if (typeof m !== 'object' || m === null) return false;\n const state = m as Record<string, unknown>;\n if (\n typeof state.machineId !== 'string' ||\n typeof state.machineName !== 'string' ||\n typeof state.updatedAt !== 'string' ||\n typeof state.snapshot !== 'object'\n ) {\n return false;\n }\n }\n return true;\n}\n\nexport interface SyncEnvelope {\n schemaVersion: 1;\n encrypted: string;\n iv: string;\n tag: string;\n updatedAt: string; // ISO 8601 — plaintext for Brew-TUI-Bar monitoring\n}\n\n// BK-006: 'merge-union' aparecia en este union pero applyConflictResolutions()\n// nunca implemento la rama; cualquier caller que lo pasara veia el conflicto\n// descartado silenciosamente. Eliminado hasta que exista la logica de merge.\nexport type ConflictResolution = 'use-local' | 'use-remote';\n\nexport interface SyncConflict {\n packageName: string;\n packageType: 'formula' | 'cask';\n localVersion: string;\n remoteMachine: string;\n remoteVersion: string;\n}\n\nexport interface SyncResult {\n success: boolean;\n conflicts: SyncConflict[];\n resolvedCount: number;\n error?: string;\n}\n","import { readFile, writeFile, rename, mkdir, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { logger } from '../../../utils/logger.js';\nimport type { SyncEnvelope } from '../types.js';\n\nconst ICLOUD_BASE = join(\n homedir(),\n 'Library', 'Mobile Documents', 'com~apple~CloudDocs',\n);\nexport const ICLOUD_SYNC_DIR = join(ICLOUD_BASE, 'BrewTUI');\nexport const ICLOUD_SYNC_PATH = join(ICLOUD_SYNC_DIR, 'sync.json');\n\nexport async function isICloudAvailable(): Promise<boolean> {\n try {\n await stat(ICLOUD_BASE);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction isValidEnvelope(v: unknown): v is SyncEnvelope {\n if (!v || typeof v !== 'object') return false;\n const obj = v as Record<string, unknown>;\n return (\n obj['schemaVersion'] === 1 &&\n typeof obj['encrypted'] === 'string' &&\n typeof obj['iv'] === 'string' &&\n typeof obj['tag'] === 'string' &&\n typeof obj['updatedAt'] === 'string'\n );\n}\n\nexport async function readSyncEnvelope(): Promise<SyncEnvelope | null> {\n // BK-012: iCloud may leave an undownloaded placeholder at the path. Reading\n // returns 0 bytes (or ENOENT for the file but a sibling .icloud entry).\n // Treat empty / missing-but-pending as \"not yet ready\" without surfacing\n // a misleading \"no remote state\" to the caller.\n try {\n const info = await stat(ICLOUD_SYNC_PATH);\n if (info.size === 0) {\n logger.warn('sync: iCloud envelope exists but is empty (placeholder?)');\n return null;\n }\n } catch (err: unknown) {\n if (err instanceof Error && (err as NodeJS.ErrnoException).code === 'ENOENT') {\n // First-sync case OR pending download — check for the placeholder sibling.\n try {\n const placeholder = ICLOUD_SYNC_PATH.replace(/sync\\.json$/, '.sync.json.icloud');\n await stat(placeholder);\n logger.warn('sync: iCloud placeholder present, file not yet downloaded');\n } catch { /* genuinely absent */ }\n return null;\n }\n logger.warn('sync: could not stat iCloud envelope', { error: String(err) });\n return null;\n }\n\n try {\n const raw = await readFile(ICLOUD_SYNC_PATH, 'utf-8');\n const parsed: unknown = JSON.parse(raw);\n if (!isValidEnvelope(parsed)) {\n logger.warn('sync: invalid envelope structure in iCloud file');\n return null;\n }\n return parsed;\n } catch (err: unknown) {\n logger.warn('sync: could not read iCloud envelope', { error: String(err) });\n return null;\n }\n}\n\nexport async function writeSyncEnvelope(envelope: SyncEnvelope): Promise<void> {\n // BK-007: explicito 0o700 — iCloud Drive hereda permisos del sistema y por\n // defecto pueden ser 0o755. Aunque el contenido del envelope va cifrado, el\n // listado del directorio no deberia ser legible por otros usuarios locales.\n await mkdir(ICLOUD_SYNC_DIR, { recursive: true, mode: 0o700 });\n const tmpPath = ICLOUD_SYNC_PATH + '.tmp';\n await writeFile(tmpPath, JSON.stringify(envelope, null, 2), {\n encoding: 'utf-8',\n mode: 0o600,\n });\n await rename(tmpPath, ICLOUD_SYNC_PATH);\n}\n"],"mappings":"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","names":["readFile","writeFile","rename","join","join","readFile","writeFile","rename"]}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {
|
|
2
|
+
activate,
|
|
3
|
+
deactivate,
|
|
4
|
+
getDegradationLevel,
|
|
5
|
+
isExpired,
|
|
6
|
+
loadLicense,
|
|
7
|
+
needsRevalidation,
|
|
8
|
+
revalidate
|
|
9
|
+
} from "./chunk-PYDQHHI2.js";
|
|
10
|
+
import {
|
|
11
|
+
ensureDataDirs
|
|
12
|
+
} from "./chunk-LFGDNAXH.js";
|
|
13
|
+
|
|
14
|
+
// src/stores/license-store.ts
|
|
15
|
+
import { create } from "zustand";
|
|
16
|
+
|
|
17
|
+
// src/lib/license/anti-tamper.ts
|
|
18
|
+
var _originalIsPro = null;
|
|
19
|
+
var _originalGetState = null;
|
|
20
|
+
var _storeApi = null;
|
|
21
|
+
function initStoreIntegrity(store) {
|
|
22
|
+
_storeApi = store;
|
|
23
|
+
_originalIsPro = store.getState().isPro;
|
|
24
|
+
_originalGetState = store.getState;
|
|
25
|
+
}
|
|
26
|
+
function verifyStoreIntegrity() {
|
|
27
|
+
if (!_storeApi || !_originalIsPro || !_originalGetState) return false;
|
|
28
|
+
const state = _storeApi.getState();
|
|
29
|
+
if (state.isPro !== _originalIsPro) return false;
|
|
30
|
+
if (_storeApi.getState !== _originalGetState) return false;
|
|
31
|
+
if (state.status === "free" && state.isPro()) return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/stores/license-store.ts
|
|
36
|
+
var REVALIDATION_CHECK_MS = 60 * 60 * 1e3;
|
|
37
|
+
var _revalidatingPromise = null;
|
|
38
|
+
var _revalidationInterval = null;
|
|
39
|
+
async function doRevalidation(license, set) {
|
|
40
|
+
const result = await revalidate(license);
|
|
41
|
+
if (result === "expired") {
|
|
42
|
+
set({ status: "expired", license: { ...license, status: "expired" }, degradation: "expired" });
|
|
43
|
+
} else {
|
|
44
|
+
const updated = await loadLicense();
|
|
45
|
+
const effective = updated ?? license;
|
|
46
|
+
set({ license: effective, degradation: getDegradationLevel(effective) });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
var useLicenseStore = create((set, get) => ({
|
|
50
|
+
status: "validating",
|
|
51
|
+
license: null,
|
|
52
|
+
error: null,
|
|
53
|
+
degradation: "none",
|
|
54
|
+
initialize: async () => {
|
|
55
|
+
initStoreIntegrity(useLicenseStore);
|
|
56
|
+
await ensureDataDirs();
|
|
57
|
+
const license = await loadLicense();
|
|
58
|
+
if (!license) {
|
|
59
|
+
set({ status: "free", license: null, degradation: "none" });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (isExpired(license)) {
|
|
63
|
+
set({ status: "expired", license, degradation: "expired" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const level = getDegradationLevel(license);
|
|
67
|
+
if (level === "expired") {
|
|
68
|
+
set({ status: "expired", license, degradation: "expired" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
set({ status: license.plan, license, degradation: level });
|
|
72
|
+
if (needsRevalidation(license)) {
|
|
73
|
+
if (!_revalidatingPromise) {
|
|
74
|
+
_revalidatingPromise = doRevalidation(license, set).finally(() => {
|
|
75
|
+
_revalidatingPromise = null;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
await _revalidatingPromise;
|
|
79
|
+
}
|
|
80
|
+
if (_revalidationInterval) clearInterval(_revalidationInterval);
|
|
81
|
+
_revalidationInterval = setInterval(() => {
|
|
82
|
+
const current = get().license;
|
|
83
|
+
const status = get().status;
|
|
84
|
+
if (!current || status !== "pro" && status !== "team") return;
|
|
85
|
+
if (!needsRevalidation(current)) return;
|
|
86
|
+
if (_revalidatingPromise) return;
|
|
87
|
+
_revalidatingPromise = doRevalidation(current, set).finally(() => {
|
|
88
|
+
_revalidatingPromise = null;
|
|
89
|
+
});
|
|
90
|
+
}, REVALIDATION_CHECK_MS);
|
|
91
|
+
_revalidationInterval.unref();
|
|
92
|
+
},
|
|
93
|
+
activate: async (key) => {
|
|
94
|
+
set({ error: null });
|
|
95
|
+
try {
|
|
96
|
+
const license = await activate(key);
|
|
97
|
+
set({ status: license.plan, license, degradation: "none" });
|
|
98
|
+
return true;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
101
|
+
set({ error: msg });
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
deactivate: async () => {
|
|
106
|
+
const { license } = get();
|
|
107
|
+
if (license) {
|
|
108
|
+
const { remoteSuccess } = await deactivate(license);
|
|
109
|
+
if (!remoteSuccess) {
|
|
110
|
+
set({ status: "free", license: null, degradation: "none", error: "License removed locally but server deactivation failed. It may remain active remotely." });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
set({ status: "free", license: null, degradation: "none", error: null });
|
|
115
|
+
},
|
|
116
|
+
revalidate: async () => {
|
|
117
|
+
const { license } = get();
|
|
118
|
+
if (!license) return;
|
|
119
|
+
try {
|
|
120
|
+
await doRevalidation(license, set);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
set({ error: err instanceof Error ? err.message : String(err) });
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
// Team is a superset of Pro — team users have full Pro access plus team features.
|
|
126
|
+
// Pro users do NOT get Team features (Compliance) without paying for the Team tier.
|
|
127
|
+
isPro: () => {
|
|
128
|
+
const s = get().status;
|
|
129
|
+
return s === "pro" || s === "team";
|
|
130
|
+
},
|
|
131
|
+
isTeam: () => get().status === "team"
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
verifyStoreIntegrity,
|
|
136
|
+
useLicenseStore
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=chunk-CCAT52XY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/license-store.ts","../src/lib/license/anti-tamper.ts"],"sourcesContent":["import { create } from 'zustand';\nimport type { LicenseData, LicenseStatus } from '../lib/license/types.js';\nimport * as manager from '../lib/license/license-manager.js';\nimport { getDegradationLevel } from '../lib/license/license-manager.js';\nimport type { DegradationLevel } from '../lib/license/license-manager.js';\nimport { ensureDataDirs } from '../lib/data-dir.js';\nimport { initStoreIntegrity } from '../lib/license/anti-tamper.js';\n\nconst REVALIDATION_CHECK_MS = 60 * 60 * 1000; // Check every hour\n\n// ARQ-002: Promise-based mutex for revalidation\nlet _revalidatingPromise: Promise<void> | null = null;\nlet _revalidationInterval: ReturnType<typeof setInterval> | null = null;\n\ninterface LicenseState {\n status: LicenseStatus;\n license: LicenseData | null;\n error: string | null;\n degradation: DegradationLevel;\n\n initialize: () => Promise<void>;\n activate: (key: string) => Promise<boolean>;\n deactivate: () => Promise<void>;\n revalidate: () => Promise<void>;\n isPro: () => boolean;\n isTeam: () => boolean;\n}\n\nasync function doRevalidation(\n license: LicenseData,\n set: (partial: Partial<LicenseState>) => void,\n): Promise<void> {\n const result = await manager.revalidate(license);\n if (result === 'expired') {\n set({ status: 'expired', license: { ...license, status: 'expired' }, degradation: 'expired' });\n } else {\n const updated = await manager.loadLicense();\n const effective = updated ?? license;\n set({ license: effective, degradation: getDegradationLevel(effective) });\n }\n}\n\nexport const useLicenseStore = create<LicenseState>((set, get) => ({\n status: 'validating',\n license: null,\n error: null,\n degradation: 'none',\n\n initialize: async () => {\n initStoreIntegrity(useLicenseStore);\n await ensureDataDirs();\n const license = await manager.loadLicense();\n\n if (!license) {\n set({ status: 'free', license: null, degradation: 'none' });\n return;\n }\n\n // SEG-009: built-in perennial accounts removed; every license — including\n // operator/admin — is validated against Polar like a normal customer.\n\n if (manager.isExpired(license)) {\n set({ status: 'expired', license, degradation: 'expired' });\n return;\n }\n\n // Layer 15: Check degradation level based on offline time\n const level = getDegradationLevel(license);\n if (level === 'expired') {\n set({ status: 'expired', license, degradation: 'expired' });\n return;\n }\n\n // Set tier immediately (warning/limited still shown as the licensed tier, but pro-guard checks degradation)\n set({ status: license.plan, license, degradation: level });\n\n if (manager.needsRevalidation(license)) {\n if (!_revalidatingPromise) {\n _revalidatingPromise = doRevalidation(license, set)\n .finally(() => { _revalidatingPromise = null; });\n }\n await _revalidatingPromise;\n }\n\n // Periodically re-check license validity during the session\n if (_revalidationInterval) clearInterval(_revalidationInterval);\n _revalidationInterval = setInterval(() => {\n const current = get().license;\n const status = get().status;\n if (!current || (status !== 'pro' && status !== 'team')) return;\n if (!manager.needsRevalidation(current)) return;\n if (_revalidatingPromise) return;\n _revalidatingPromise = doRevalidation(current, set)\n .finally(() => { _revalidatingPromise = null; });\n }, REVALIDATION_CHECK_MS);\n _revalidationInterval.unref();\n },\n\n activate: async (key) => {\n set({ error: null });\n try {\n const license = await manager.activate(key);\n set({ status: license.plan, license, degradation: 'none' });\n return true;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n set({ error: msg });\n return false;\n }\n },\n\n deactivate: async () => {\n const { license } = get();\n if (license) {\n const { remoteSuccess } = await manager.deactivate(license);\n if (!remoteSuccess) {\n set({ status: 'free', license: null, degradation: 'none', error: 'License removed locally but server deactivation failed. It may remain active remotely.' });\n return;\n }\n }\n set({ status: 'free', license: null, degradation: 'none', error: null });\n },\n\n revalidate: async () => {\n const { license } = get();\n if (!license) return;\n try {\n await doRevalidation(license, set);\n } catch (err) {\n set({ error: err instanceof Error ? err.message : String(err) });\n }\n },\n\n // Team is a superset of Pro — team users have full Pro access plus team features.\n // Pro users do NOT get Team features (Compliance) without paying for the Team tier.\n isPro: () => { const s = get().status; return s === 'pro' || s === 'team'; },\n isTeam: () => get().status === 'team',\n}));\n","import type { LicenseStatus } from './types.js';\n\ninterface LicenseStoreSnapshot {\n status: LicenseStatus;\n isPro: () => boolean;\n}\n\ninterface LicenseStoreApi {\n getState: () => LicenseStoreSnapshot;\n}\n\n// Lazy-captured references: initialized on first call to initStoreIntegrity()\nlet _originalIsPro: (() => boolean) | null = null;\nlet _originalGetState: (() => LicenseStoreSnapshot) | null = null;\nlet _storeApi: LicenseStoreApi | null = null;\n\n/**\n * Capture the original store function references for later integrity checks.\n * Call this once during license store initialization.\n */\nexport function initStoreIntegrity(store: LicenseStoreApi): void {\n _storeApi = store;\n _originalIsPro = store.getState().isPro;\n _originalGetState = store.getState;\n}\n\n/**\n * Verify that the license store's isPro function hasn't been replaced\n * with a function that always returns true.\n */\nexport function verifyStoreIntegrity(): boolean {\n if (!_storeApi || !_originalIsPro || !_originalGetState) return false;\n\n const state = _storeApi.getState();\n\n // Check 1: isPro function reference hasn't changed\n if (state.isPro !== _originalIsPro) return false;\n\n // Check 2: getState itself hasn't been replaced\n if (_storeApi.getState !== _originalGetState) return false;\n\n // Check 3: If status is 'free', isPro() must return false\n // (catches patches that make isPro() always return true regardless of status)\n if (state.status === 'free' && state.isPro()) return false;\n\n return true;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-NRRQECXA.js";
|
|
4
4
|
import {
|
|
5
5
|
t
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-SDQYHY2L.js";
|
|
7
7
|
|
|
8
8
|
// src/lib/brew-tui-bar-installer.ts
|
|
9
9
|
import { rm, access, readFile } from "fs/promises";
|
|
@@ -128,6 +128,23 @@ async function installBrewTUIBar(_isPro, force = false) {
|
|
|
128
128
|
throw new Error(t("cli_brewtuibarDownloadFailed", { error: "Download exceeds 200 MB size limit" }));
|
|
129
129
|
}
|
|
130
130
|
let downloadedBytes = 0;
|
|
131
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
132
|
+
let lastReportedQuarter = -1;
|
|
133
|
+
function reportProgress() {
|
|
134
|
+
const totalMB = contentLength > 0 ? (contentLength / 1024 / 1024).toFixed(1) : null;
|
|
135
|
+
const doneMB = (downloadedBytes / 1024 / 1024).toFixed(1);
|
|
136
|
+
const pct = contentLength > 0 ? Math.floor(downloadedBytes / contentLength * 100) : -1;
|
|
137
|
+
if (isTTY) {
|
|
138
|
+
const line = totalMB ? ` ${doneMB} MB / ${totalMB} MB (${pct}%)` : ` ${doneMB} MB`;
|
|
139
|
+
process.stdout.write("\r" + line.padEnd(60, " "));
|
|
140
|
+
} else if (pct >= 0) {
|
|
141
|
+
const quarter = Math.floor(pct / 25);
|
|
142
|
+
if (quarter > lastReportedQuarter) {
|
|
143
|
+
lastReportedQuarter = quarter;
|
|
144
|
+
console.log(` ${pct}% (${doneMB} MB / ${totalMB} MB)`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
131
148
|
const fileStream = createWriteStream(TMP_ZIP);
|
|
132
149
|
const transformedBody = new ReadableStream({
|
|
133
150
|
async start(controller) {
|
|
@@ -142,6 +159,7 @@ async function installBrewTUIBar(_isPro, force = false) {
|
|
|
142
159
|
return;
|
|
143
160
|
}
|
|
144
161
|
controller.enqueue(value);
|
|
162
|
+
reportProgress();
|
|
145
163
|
}
|
|
146
164
|
controller.close();
|
|
147
165
|
} catch (err) {
|
|
@@ -150,6 +168,7 @@ async function installBrewTUIBar(_isPro, force = false) {
|
|
|
150
168
|
}
|
|
151
169
|
});
|
|
152
170
|
await pipeline(transformedBody, fileStream);
|
|
171
|
+
if (isTTY) process.stdout.write("\n");
|
|
153
172
|
let expectedHash = null;
|
|
154
173
|
try {
|
|
155
174
|
const checksumRes = await fetchWithTimeout(`${DOWNLOAD_URL}.sha256`, {}, 15e3);
|
|
@@ -205,7 +224,7 @@ async function launchBrewTUIBar() {
|
|
|
205
224
|
}
|
|
206
225
|
async function syncAndLaunchBrewTUIBar() {
|
|
207
226
|
if (process.platform !== "darwin") return;
|
|
208
|
-
const { checkBrewTUIBarVersion } = await import("./version-check-
|
|
227
|
+
const { checkBrewTUIBarVersion } = await import("./version-check-UUJMLUK6.js");
|
|
209
228
|
try {
|
|
210
229
|
if (!await isBrewTUIBarInstalled()) {
|
|
211
230
|
console.log(t("cli_brewtuibarInstalling"));
|
|
@@ -237,10 +256,11 @@ async function uninstallBrewTUIBar() {
|
|
|
237
256
|
|
|
238
257
|
export {
|
|
239
258
|
isBrewTUIBarInstalled,
|
|
259
|
+
bundleIdAt,
|
|
240
260
|
isBrewTUIBarRunning,
|
|
241
261
|
installBrewTUIBar,
|
|
242
262
|
launchBrewTUIBar,
|
|
243
263
|
syncAndLaunchBrewTUIBar,
|
|
244
264
|
uninstallBrewTUIBar
|
|
245
265
|
};
|
|
246
|
-
//# sourceMappingURL=chunk-
|
|
266
|
+
//# sourceMappingURL=chunk-I5VZR55J.js.map
|