brew-tui 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/build/{brew-tui-bar-installer-ACAVC66L.js → brew-tui-bar-installer-LS6N5D5P.js} +5 -3
  2. package/build/{brewfile-manager-HWTPBXPO.js → brewfile-manager-DNRM6CQ7.js} +3 -3
  3. package/build/chunk-7R4ME2NC.js +342 -0
  4. package/build/chunk-7R4ME2NC.js.map +1 -0
  5. package/build/chunk-CCAT52XY.js +138 -0
  6. package/build/chunk-CCAT52XY.js.map +1 -0
  7. package/build/{version-check-HXQJF64V.js → chunk-HXTTSU3T.js} +6 -5
  8. package/build/{chunk-ZC23DNMK.js → chunk-PYDQHHI2.js} +22 -354
  9. package/build/chunk-PYDQHHI2.js.map +1 -0
  10. package/build/{chunk-F2S7TGCS.js → chunk-SDQYHY2L.js} +3 -1
  11. package/build/chunk-SDQYHY2L.js.map +1 -0
  12. package/build/{chunk-Y45AXONF.js → chunk-UZMGXQKF.js} +2 -2
  13. package/build/{chunk-HKKLLJPO.js → chunk-YEMLGOXS.js} +23 -3
  14. package/build/chunk-YEMLGOXS.js.map +1 -0
  15. package/build/doctor-VRBE2VXF.js +133 -0
  16. package/build/doctor-VRBE2VXF.js.map +1 -0
  17. package/build/index.js +59 -159
  18. package/build/index.js.map +1 -1
  19. package/build/postinstall.js +10 -5
  20. package/build/postinstall.js.map +1 -1
  21. package/build/{sync-engine-G5ML7TJ5.js → sync-engine-KTH4K3NG.js} +4 -3
  22. package/build/version-check-3JMI6HOO.js +15 -0
  23. package/build/version-check-3JMI6HOO.js.map +1 -0
  24. package/package.json +1 -1
  25. package/build/chunk-F2S7TGCS.js.map +0 -1
  26. package/build/chunk-HKKLLJPO.js.map +0 -1
  27. package/build/chunk-ZC23DNMK.js.map +0 -1
  28. /package/build/{brew-tui-bar-installer-ACAVC66L.js.map → brew-tui-bar-installer-LS6N5D5P.js.map} +0 -0
  29. /package/build/{brewfile-manager-HWTPBXPO.js.map → brewfile-manager-DNRM6CQ7.js.map} +0 -0
  30. /package/build/{version-check-HXQJF64V.js.map → chunk-HXTTSU3T.js.map} +0 -0
  31. /package/build/{chunk-Y45AXONF.js.map → chunk-UZMGXQKF.js.map} +0 -0
  32. /package/build/{sync-engine-G5ML7TJ5.js.map → sync-engine-KTH4K3NG.js.map} +0 -0
@@ -3,163 +3,16 @@ import {
3
3
  } from "./chunk-NRRQECXA.js";
4
4
  import {
5
5
  t
6
- } from "./chunk-F2S7TGCS.js";
6
+ } from "./chunk-SDQYHY2L.js";
7
7
  import {
8
- captureSnapshot
9
- } from "./chunk-OXDZ4DCK.js";
10
- import {
11
- logger
12
- } from "./chunk-KDHEUNRI.js";
13
- import {
14
- DATA_DIR,
15
8
  LICENSE_PATH,
16
9
  ensureDataDirs,
17
10
  getMachineId
18
11
  } from "./chunk-LFGDNAXH.js";
19
12
 
20
- // src/lib/sync/sync-engine.ts
21
- import { readFile as readFile3, writeFile as writeFile3, rename as rename3 } from "fs/promises";
22
- import { join as join2 } from "path";
23
- import { hostname } from "os";
24
-
25
- // src/lib/sync/crypto.ts
26
- import { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from "crypto";
27
-
28
- // src/lib/sync/types.ts
29
- function isSyncPayload(value) {
30
- if (typeof value !== "object" || value === null) return false;
31
- const machines = value.machines;
32
- if (typeof machines !== "object" || machines === null || Array.isArray(machines)) return false;
33
- for (const m of Object.values(machines)) {
34
- if (typeof m !== "object" || m === null) return false;
35
- const state = m;
36
- if (typeof state.machineId !== "string" || typeof state.machineName !== "string" || typeof state.updatedAt !== "string" || typeof state.snapshot !== "object") {
37
- return false;
38
- }
39
- }
40
- return true;
41
- }
42
-
43
- // src/lib/sync/crypto.ts
44
- var ENCRYPTION_SECRET = "brew-tui-sync-aes256gcm-v1";
45
- var HKDF_SALT = "brew-tui-sync-salt-v1";
46
- var keyCache = /* @__PURE__ */ new Map();
47
- var _legacyKey = null;
48
- function deriveEncryptionKey(licenseKey) {
49
- const cached = keyCache.get(licenseKey);
50
- if (cached) return cached;
51
- const derived = Buffer.from(hkdfSync("sha256", ENCRYPTION_SECRET, HKDF_SALT, licenseKey, 32));
52
- keyCache.set(licenseKey, derived);
53
- return derived;
54
- }
55
- function deriveLegacyKey() {
56
- if (!_legacyKey) {
57
- _legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32, { N: 16384, r: 8, p: 1 });
58
- }
59
- return _legacyKey;
60
- }
61
- function encryptPayload(data, licenseKey) {
62
- const key = deriveEncryptionKey(licenseKey);
63
- const iv = randomBytes(12);
64
- const cipher = createCipheriv("aes-256-gcm", key, iv);
65
- const plaintext = JSON.stringify(data);
66
- const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
67
- const tag = cipher.getAuthTag();
68
- return {
69
- encrypted: ciphertext.toString("base64"),
70
- iv: iv.toString("base64"),
71
- tag: tag.toString("base64")
72
- };
73
- }
74
- function decryptPayload(encrypted, iv, tag, licenseKey) {
75
- const ivBuf = Buffer.from(iv, "base64");
76
- const tagBuf = Buffer.from(tag, "base64");
77
- const ciphertext = Buffer.from(encrypted, "base64");
78
- for (const key of [deriveEncryptionKey(licenseKey), deriveLegacyKey()]) {
79
- try {
80
- const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
81
- decipher.setAuthTag(tagBuf);
82
- const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
83
- const parsed = JSON.parse(plaintext.toString("utf-8"));
84
- if (!isSyncPayload(parsed)) throw new Error("Invalid sync payload shape");
85
- return parsed;
86
- } catch {
87
- }
88
- }
89
- throw new Error("Failed to decrypt sync payload");
90
- }
91
-
92
- // src/lib/sync/backends/icloud-backend.ts
93
- import { readFile, writeFile, rename, mkdir, stat } from "fs/promises";
94
- import { homedir } from "os";
95
- import { join } from "path";
96
- var ICLOUD_BASE = join(
97
- homedir(),
98
- "Library",
99
- "Mobile Documents",
100
- "com~apple~CloudDocs"
101
- );
102
- var ICLOUD_SYNC_DIR = join(ICLOUD_BASE, "BrewTUI");
103
- var ICLOUD_SYNC_PATH = join(ICLOUD_SYNC_DIR, "sync.json");
104
- async function isICloudAvailable() {
105
- try {
106
- await stat(ICLOUD_BASE);
107
- return true;
108
- } catch {
109
- return false;
110
- }
111
- }
112
- function isValidEnvelope(v) {
113
- if (!v || typeof v !== "object") return false;
114
- const obj = v;
115
- return obj["schemaVersion"] === 1 && typeof obj["encrypted"] === "string" && typeof obj["iv"] === "string" && typeof obj["tag"] === "string" && typeof obj["updatedAt"] === "string";
116
- }
117
- async function readSyncEnvelope() {
118
- try {
119
- const info = await stat(ICLOUD_SYNC_PATH);
120
- if (info.size === 0) {
121
- logger.warn("sync: iCloud envelope exists but is empty (placeholder?)");
122
- return null;
123
- }
124
- } catch (err) {
125
- if (err instanceof Error && err.code === "ENOENT") {
126
- try {
127
- const placeholder = ICLOUD_SYNC_PATH.replace(/sync\.json$/, ".sync.json.icloud");
128
- await stat(placeholder);
129
- logger.warn("sync: iCloud placeholder present, file not yet downloaded");
130
- } catch {
131
- }
132
- return null;
133
- }
134
- logger.warn("sync: could not stat iCloud envelope", { error: String(err) });
135
- return null;
136
- }
137
- try {
138
- const raw = await readFile(ICLOUD_SYNC_PATH, "utf-8");
139
- const parsed = JSON.parse(raw);
140
- if (!isValidEnvelope(parsed)) {
141
- logger.warn("sync: invalid envelope structure in iCloud file");
142
- return null;
143
- }
144
- return parsed;
145
- } catch (err) {
146
- logger.warn("sync: could not read iCloud envelope", { error: String(err) });
147
- return null;
148
- }
149
- }
150
- async function writeSyncEnvelope(envelope) {
151
- await mkdir(ICLOUD_SYNC_DIR, { recursive: true, mode: 448 });
152
- const tmpPath = ICLOUD_SYNC_PATH + ".tmp";
153
- await writeFile(tmpPath, JSON.stringify(envelope, null, 2), {
154
- encoding: "utf-8",
155
- mode: 384
156
- });
157
- await rename(tmpPath, ICLOUD_SYNC_PATH);
158
- }
159
-
160
13
  // src/lib/license/license-manager.ts
161
- import { readFile as readFile2, writeFile as writeFile2, rename as rename2, rm } from "fs/promises";
162
- import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes2, scryptSync as scryptSync2, hkdfSync as hkdfSync2 } from "crypto";
14
+ import { readFile, writeFile, rename, rm } from "fs/promises";
15
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from "crypto";
163
16
 
164
17
  // src/lib/license/polar-api.ts
165
18
  import { createHash } from "crypto";
@@ -310,26 +163,26 @@ function recordAttempt(success) {
310
163
  tracker.attempts = 0;
311
164
  }
312
165
  }
313
- var ENCRYPTION_SECRET2 = "brew-tui-license-aes256gcm-v1";
314
- var HKDF_SALT2 = "brew-tui-salt-v1";
166
+ var ENCRYPTION_SECRET = "brew-tui-license-aes256gcm-v1";
167
+ var HKDF_SALT = "brew-tui-salt-v1";
315
168
  var _derivedKey = null;
316
- var _legacyKey2 = null;
169
+ var _legacyKey = null;
317
170
  var _decryptedWithLegacyKey = false;
318
- async function deriveEncryptionKey2() {
171
+ async function deriveEncryptionKey() {
319
172
  if (_derivedKey) return _derivedKey;
320
173
  const machineId = await getMachineId();
321
- const derived = hkdfSync2("sha256", ENCRYPTION_SECRET2, HKDF_SALT2, machineId, 32);
174
+ const derived = hkdfSync("sha256", ENCRYPTION_SECRET, HKDF_SALT, machineId, 32);
322
175
  _derivedKey = Buffer.from(derived);
323
176
  return _derivedKey;
324
177
  }
325
- function deriveLegacyKey2() {
326
- if (!_legacyKey2) _legacyKey2 = scryptSync2(ENCRYPTION_SECRET2, HKDF_SALT2, 32);
327
- return _legacyKey2;
178
+ function deriveLegacyKey() {
179
+ if (!_legacyKey) _legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32);
180
+ return _legacyKey;
328
181
  }
329
182
  async function encryptLicenseData(data) {
330
- const key = await deriveEncryptionKey2();
331
- const iv = randomBytes2(12);
332
- const cipher = createCipheriv2("aes-256-gcm", key, iv);
183
+ const key = await deriveEncryptionKey();
184
+ const iv = randomBytes(12);
185
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
333
186
  const plaintext = JSON.stringify(data);
334
187
  const ciphertext = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
335
188
  const tag = cipher.getAuthTag();
@@ -344,13 +197,13 @@ async function decryptLicenseData(encrypted, iv, tag) {
344
197
  const tagBuf = Buffer.from(tag, "base64");
345
198
  const ciphertext = Buffer.from(encrypted, "base64");
346
199
  const candidates = [
347
- [await deriveEncryptionKey2(), false],
348
- [deriveLegacyKey2(), true]
200
+ [await deriveEncryptionKey(), false],
201
+ [deriveLegacyKey(), true]
349
202
  ];
350
203
  let lastErr;
351
204
  for (const [key, isLegacy] of candidates) {
352
205
  try {
353
- const decipher = createDecipheriv2("aes-256-gcm", key, ivBuf);
206
+ const decipher = createDecipheriv("aes-256-gcm", key, ivBuf);
354
207
  decipher.setAuthTag(tagBuf);
355
208
  const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
356
209
  const parsed = JSON.parse(plaintext.toString("utf-8"));
@@ -375,7 +228,7 @@ function isEncryptedLicenseFile(obj) {
375
228
  }
376
229
  async function loadLicense() {
377
230
  try {
378
- const raw = await readFile2(LICENSE_PATH, "utf-8");
231
+ const raw = await readFile(LICENSE_PATH, "utf-8");
379
232
  const parsed = JSON.parse(raw);
380
233
  if (!isLicenseFile(parsed)) {
381
234
  throw new Error("Invalid license data format");
@@ -418,8 +271,8 @@ async function saveLicense(data) {
418
271
  const machineId = await getMachineId();
419
272
  const file = { version: 1, encrypted, iv, tag, machineId };
420
273
  const tmpPath = LICENSE_PATH + ".tmp";
421
- await writeFile2(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
422
- await rename2(tmpPath, LICENSE_PATH);
274
+ await writeFile(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
275
+ await rename(tmpPath, LICENSE_PATH);
423
276
  }
424
277
  async function clearLicense() {
425
278
  try {
@@ -531,185 +384,6 @@ async function deactivate(license) {
531
384
  return { remoteSuccess };
532
385
  }
533
386
 
534
- // src/lib/sync/sync-engine.ts
535
- var SYNC_CONFIG_PATH = join2(DATA_DIR, "sync-config.json");
536
- async function loadSyncConfig() {
537
- try {
538
- const raw = await readFile3(SYNC_CONFIG_PATH, "utf-8");
539
- return JSON.parse(raw);
540
- } catch {
541
- return null;
542
- }
543
- }
544
- async function saveSyncConfig(config) {
545
- const tmpPath = SYNC_CONFIG_PATH + ".tmp";
546
- await writeFile3(tmpPath, JSON.stringify(config, null, 2), {
547
- encoding: "utf-8",
548
- mode: 384
549
- });
550
- await rename3(tmpPath, SYNC_CONFIG_PATH);
551
- }
552
- function detectConflicts(localSnapshot, otherMachines, localMachineId) {
553
- const conflicts = [];
554
- const localFormulaMap = new Map(localSnapshot.formulae.map((f) => [f.name, f.version]));
555
- const localCaskMap = new Map(localSnapshot.casks.map((c) => [c.name, c.version]));
556
- for (const machine of otherMachines) {
557
- if (machine.machineId === localMachineId) continue;
558
- for (const remoteFormula of machine.snapshot.formulae) {
559
- const localVersion = localFormulaMap.get(remoteFormula.name);
560
- if (localVersion !== void 0 && localVersion !== remoteFormula.version) {
561
- conflicts.push({
562
- packageName: remoteFormula.name,
563
- packageType: "formula",
564
- localVersion,
565
- remoteMachine: machine.machineName,
566
- remoteVersion: remoteFormula.version
567
- });
568
- }
569
- }
570
- for (const remoteCask of machine.snapshot.casks) {
571
- const localVersion = localCaskMap.get(remoteCask.name);
572
- if (localVersion !== void 0 && localVersion !== remoteCask.version) {
573
- conflicts.push({
574
- packageName: remoteCask.name,
575
- packageType: "cask",
576
- localVersion,
577
- remoteMachine: machine.machineName,
578
- remoteVersion: remoteCask.version
579
- });
580
- }
581
- }
582
- }
583
- return conflicts;
584
- }
585
- async function writeEnvelope(payload, licenseKey) {
586
- const now = (/* @__PURE__ */ new Date()).toISOString();
587
- const { encrypted, iv, tag } = encryptPayload(payload, licenseKey);
588
- const envelope = {
589
- schemaVersion: 1,
590
- encrypted,
591
- iv,
592
- tag,
593
- updatedAt: now
594
- };
595
- await writeSyncEnvelope(envelope);
596
- return now;
597
- }
598
- async function loadLicenseKeyOrThrow() {
599
- const license = await loadLicense();
600
- if (!license || !license.key) {
601
- throw new Error("Sync requires an active license");
602
- }
603
- return license.key;
604
- }
605
- function mergePayload(existing, localState) {
606
- return {
607
- machines: {
608
- ...existing.machines,
609
- [localState.machineId]: localState
610
- }
611
- };
612
- }
613
- async function sync(isPro, currentBrewfile) {
614
- if (!isPro) {
615
- throw new Error("Pro license required");
616
- }
617
- const available = await isICloudAvailable();
618
- if (!available) {
619
- return {
620
- success: false,
621
- conflicts: [],
622
- resolvedCount: 0,
623
- error: "iCloud Drive not available"
624
- };
625
- }
626
- const licenseKey = await loadLicenseKeyOrThrow();
627
- let existingPayload = null;
628
- try {
629
- const envelope = await readSyncEnvelope();
630
- if (envelope) {
631
- existingPayload = decryptPayload(envelope.encrypted, envelope.iv, envelope.tag, licenseKey);
632
- }
633
- } catch (err) {
634
- logger.warn("sync: could not decrypt existing payload, starting fresh", { error: String(err) });
635
- existingPayload = null;
636
- }
637
- const snapshot = await captureSnapshot();
638
- const machineId = await getMachineId();
639
- const machineName = hostname();
640
- const localState = {
641
- machineId,
642
- machineName,
643
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
644
- snapshot,
645
- ...currentBrewfile ? { brewfile: currentBrewfile } : {}
646
- };
647
- const otherMachines = existingPayload ? Object.values(existingPayload.machines).filter((m) => m.machineId !== machineId) : [];
648
- const conflicts = detectConflicts(snapshot, otherMachines, machineId);
649
- const basePayload = existingPayload ?? { machines: {} };
650
- const mergedPayload = mergePayload(basePayload, localState);
651
- if (conflicts.length > 0) {
652
- await writeEnvelope(mergedPayload, licenseKey);
653
- return {
654
- success: false,
655
- conflicts,
656
- resolvedCount: 0
657
- };
658
- }
659
- const now = await writeEnvelope(mergedPayload, licenseKey);
660
- const existingConfig = await loadSyncConfig();
661
- await saveSyncConfig({
662
- enabled: true,
663
- machineId,
664
- machineName,
665
- ...existingConfig ?? {},
666
- lastSync: now
667
- });
668
- logger.info("sync: completed successfully", { machineId, machines: Object.keys(mergedPayload.machines).length });
669
- return {
670
- success: true,
671
- conflicts: [],
672
- resolvedCount: 0
673
- };
674
- }
675
- async function applyConflictResolutions(payload, resolutions, localMachineId) {
676
- const updatedPayload = {
677
- machines: { ...payload.machines }
678
- };
679
- for (const { conflict, resolution } of resolutions) {
680
- if (resolution !== "use-remote") continue;
681
- const localMachine = updatedPayload.machines[localMachineId];
682
- if (!localMachine) {
683
- logger.warn("sync: cannot apply resolution, local machine missing in payload", { localMachineId });
684
- continue;
685
- }
686
- if (conflict.packageType === "formula") {
687
- updatedPayload.machines[localMachineId] = {
688
- ...localMachine,
689
- snapshot: {
690
- ...localMachine.snapshot,
691
- formulae: localMachine.snapshot.formulae.map(
692
- (f) => f.name === conflict.packageName ? { ...f, version: conflict.remoteVersion } : f
693
- )
694
- }
695
- };
696
- } else {
697
- updatedPayload.machines[localMachineId] = {
698
- ...localMachine,
699
- snapshot: {
700
- ...localMachine.snapshot,
701
- casks: localMachine.snapshot.casks.map(
702
- (c) => c.name === conflict.packageName ? { ...c, version: conflict.remoteVersion } : c
703
- )
704
- }
705
- };
706
- }
707
- }
708
- const licenseKey = await loadLicenseKeyOrThrow();
709
- await writeEnvelope(updatedPayload, licenseKey);
710
- logger.info("sync: conflict resolutions applied", { count: resolutions.length });
711
- }
712
-
713
387
  export {
714
388
  loadLicense,
715
389
  isExpired,
@@ -717,12 +391,6 @@ export {
717
391
  getDegradationLevel,
718
392
  activate,
719
393
  revalidate,
720
- deactivate,
721
- decryptPayload,
722
- readSyncEnvelope,
723
- loadSyncConfig,
724
- saveSyncConfig,
725
- sync,
726
- applyConflictResolutions
394
+ deactivate
727
395
  };
728
- //# sourceMappingURL=chunk-ZC23DNMK.js.map
396
+ //# sourceMappingURL=chunk-PYDQHHI2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/license/license-manager.ts","../src/lib/license/polar-api.ts","../src/lib/license/types.ts"],"sourcesContent":["import { readFile, writeFile, rename, rm } from 'node:fs/promises';\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync, hkdfSync } from 'node:crypto';\nimport { LICENSE_PATH, ensureDataDirs, getMachineId } from '../data-dir.js';\nimport { activateLicense as apiActivate, validateLicense as apiValidate, deactivateLicense as apiDeactivate } from './polar-api.js';\nimport { t } from '../../i18n/index.js';\nimport { isLicenseData, type LicenseData, type LicenseFile } from './types.js';\n\n// SEG-009 guard: previously a hardcoded map bypassed Polar entirely. The\n// function is kept as an always-null export so a regression test can pin\n// the behaviour and the import site in license-store stays stable.\nexport function getBuiltinAccountType(_email: string): 'pro' | 'team' | 'free' | null {\n return null;\n}\n\nconst REVALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h\nconst GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; // 7 days\n\n// ── Layer 18: Client-side rate limiting on activations ──\nconst ACTIVATION_COOLDOWN_MS = 30_000; // 30 seconds between attempts\nconst MAX_ATTEMPTS = 5;\nconst LOCKOUT_MS = 15 * 60 * 1000; // 15 min lockout after max attempts\n\ninterface ActivationTracker {\n attempts: number;\n lastAttempt: number;\n lockedUntil: number;\n}\n\n// UX-004: rate-limit state is intentionally in-memory only. It is a first\n// filter to slow down brute force inside one TUI session — the authoritative\n// activation throttle lives in the Polar backend, which sees attempts across\n// process restarts. Persisting this client-side would invite users to delete\n// the file and reset themselves; the trade-off is documented here on purpose.\nconst tracker: ActivationTracker = {\n attempts: 0,\n lastAttempt: 0,\n lockedUntil: 0,\n};\n\nfunction checkRateLimit(): void {\n const now = Date.now();\n\n // Check lockout\n if (now < tracker.lockedUntil) {\n const remaining = Math.ceil((tracker.lockedUntil - now) / 60000);\n throw new Error(t('cli_rateLimited', { minutes: remaining }));\n }\n\n // Check cooldown\n if (now - tracker.lastAttempt < ACTIVATION_COOLDOWN_MS) {\n throw new Error(t('cli_cooldown'));\n }\n}\n\nfunction recordAttempt(success: boolean): void {\n const now = Date.now();\n tracker.lastAttempt = now;\n\n if (success) {\n tracker.attempts = 0;\n return;\n }\n\n tracker.attempts++;\n if (tracker.attempts >= MAX_ATTEMPTS) {\n tracker.lockedUntil = now + LOCKOUT_MS;\n tracker.attempts = 0;\n }\n}\n\n// SECURITY (SEG-002): the bundle-only constants below USED to be the entire\n// derivation input — anyone with the npm bundle could decrypt any user's\n// license.json. Now the per-user machineId is mixed into the HKDF info, so\n// the bundle alone is no longer sufficient: an attacker also needs the\n// target's ~/.brew-tui/machine-id. The two constants stay published; what's\n// secret is the user's local machineId, which never leaves the machine.\n//\n// HKDF-SHA256 was chosen over scrypt because Swift's CryptoKit (used by\n// Brew-TUI-Bar to read the same license.json) ships HKDF natively but not scrypt.\n// machineId is a UUIDv4 with 122 bits of entropy, so the cost-hardening of\n// scrypt is not what's protecting the key — the secrecy of the machineId is.\nconst ENCRYPTION_SECRET = 'brew-tui-license-aes256gcm-v1';\nconst HKDF_SALT = 'brew-tui-salt-v1';\n\nlet _derivedKey: Buffer | null = null;\nlet _legacyKey: Buffer | null = null;\nlet _decryptedWithLegacyKey = false;\n\nasync function deriveEncryptionKey(): Promise<Buffer> {\n if (_derivedKey) return _derivedKey;\n const machineId = await getMachineId();\n // HKDF: ikm = SECRET, salt = HKDF_SALT, info = machineId, len = 32\n const derived = hkdfSync('sha256', ENCRYPTION_SECRET, HKDF_SALT, machineId, 32);\n _derivedKey = Buffer.from(derived);\n return _derivedKey;\n}\n\n// Legacy key — scrypt(SECRET, SALT) with no machineId. Pre-existing\n// license.json files written by 0.6.2 and earlier are ciphered with this.\n// decryptLicenseData falls back to it; the next saveLicense re-ciphers\n// using the HKDF key. TODO(SEG-003, 0.6.3): remove `_legacyKey` after\n// telemetry confirms zero fallback decrypts in the wild.\nfunction deriveLegacyKey(): Buffer {\n if (!_legacyKey) _legacyKey = scryptSync(ENCRYPTION_SECRET, HKDF_SALT, 32);\n return _legacyKey;\n}\n\nasync function encryptLicenseData(data: LicenseData): Promise<{ encrypted: string; iv: string; tag: string }> {\n const key = await deriveEncryptionKey();\n const iv = randomBytes(12); // 96-bit IV for GCM\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n\n const plaintext = JSON.stringify(data);\n const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n return {\n encrypted: ciphertext.toString('base64'),\n iv: iv.toString('base64'),\n tag: tag.toString('base64'),\n };\n}\n\nasync function decryptLicenseData(encrypted: string, iv: string, tag: string): Promise<LicenseData> {\n const ivBuf = Buffer.from(iv, 'base64');\n const tagBuf = Buffer.from(tag, 'base64');\n const ciphertext = Buffer.from(encrypted, 'base64');\n\n // Try the current (machine-bound) key first; fall back to the legacy\n // (bundle-only) key for upgrade compatibility.\n const candidates: Array<[Buffer, boolean]> = [\n [await deriveEncryptionKey(), false],\n [deriveLegacyKey(), true],\n ];\n let lastErr: unknown;\n for (const [key, isLegacy] of candidates) {\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, ivBuf);\n decipher.setAuthTag(tagBuf);\n const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n const parsed: unknown = JSON.parse(plaintext.toString('utf-8'));\n if (!isLicenseData(parsed)) {\n throw new Error('Decrypted license payload failed shape validation');\n }\n _decryptedWithLegacyKey = isLegacy;\n return parsed;\n } catch (err) { lastErr = err; }\n }\n throw lastErr instanceof Error ? lastErr : new Error('Failed to decrypt license');\n}\n\n// BK-003: Type guard for license data format\nfunction isLicenseFile(obj: unknown): obj is LicenseFile {\n return typeof obj === 'object' && obj !== null && (obj as Record<string, unknown>).version === 1;\n}\n\nfunction isEncryptedLicenseFile(obj: unknown): obj is LicenseFile & { encrypted: string; iv: string; tag: string } {\n if (!isLicenseFile(obj)) return false;\n const record = obj as unknown as Record<string, unknown>;\n return typeof record.encrypted === 'string'\n && typeof record.iv === 'string'\n && typeof record.tag === 'string';\n}\n\nexport async function loadLicense(): Promise<LicenseData | null> {\n try {\n const raw = await readFile(LICENSE_PATH, 'utf-8');\n const parsed: unknown = JSON.parse(raw);\n\n // BK-003: Validate parsed data\n if (!isLicenseFile(parsed)) {\n throw new Error('Invalid license data format');\n }\n\n const file = parsed as LicenseFile;\n\n if (file.version !== 1) {\n // Future: add migration logic here\n throw new Error('Unsupported data version');\n }\n\n // New encrypted format\n if (isEncryptedLicenseFile(file)) {\n const data = await decryptLicenseData(file.encrypted!, file.iv!, file.tag!);\n\n // SEG-002: Check machine ID if stored in the envelope.\n // getMachineId() now always resolves a value — if the user's machine-id\n // file was wiped, a new UUID is created and this check rejects the\n // license, prompting reactivation. Same behaviour the polar-api flow\n // already had on save.\n const fileRecord = file as unknown as Record<string, unknown>;\n if (fileRecord.machineId) {\n const currentMachineId = await getMachineId();\n if (fileRecord.machineId !== currentMachineId) {\n throw new Error('License was activated on a different machine');\n }\n }\n\n // If we fell back to the legacy bundle-only key, re-cipher with the\n // current machine-bound key so future reads use the strong path.\n if (_decryptedWithLegacyKey) {\n _decryptedWithLegacyKey = false;\n try { await saveLicense(data); } catch { /* best effort */ }\n }\n\n return data;\n }\n\n // Legacy unencrypted format — migrate to encrypted on read\n if (file.license) {\n const data = file.license;\n // Re-save in encrypted format\n await saveLicense(data);\n return data;\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\nexport async function saveLicense(data: LicenseData): Promise<void> {\n await ensureDataDirs();\n const { encrypted, iv, tag } = await encryptLicenseData(data);\n // SEG-002: Include machineId in the envelope for portability detection\n const machineId = await getMachineId();\n const file: Record<string, unknown> = { version: 1, encrypted, iv, tag, machineId };\n const tmpPath = LICENSE_PATH + '.tmp';\n await writeFile(tmpPath, JSON.stringify(file, null, 2), { encoding: 'utf-8', mode: 0o600 });\n await rename(tmpPath, LICENSE_PATH);\n}\n\nexport async function clearLicense(): Promise<void> {\n try {\n await rm(LICENSE_PATH);\n } catch { /* file may not exist */ }\n}\n\nexport function isExpired(license: LicenseData): boolean {\n if (!license.expiresAt) return false;\n const expiry = new Date(license.expiresAt).getTime();\n // Fail closed on corrupted/unparseable dates: NaN comparisons are always\n // false, so the previous version treated a garbage expiresAt as \"never\n // expires\", which is exploitable.\n if (isNaN(expiry)) return true;\n return expiry < Date.now();\n}\n\nexport function needsRevalidation(license: LicenseData): boolean {\n const lastValidated = new Date(license.lastValidatedAt).getTime();\n if (isNaN(lastValidated)) return true; // corrupted date → force revalidation\n return Date.now() - lastValidated > REVALIDATION_INTERVAL_MS;\n}\n\nexport function isWithinGracePeriod(license: LicenseData): boolean {\n const lastValidated = new Date(license.lastValidatedAt).getTime();\n if (isNaN(lastValidated)) return false; // corrupted date → no grace\n return Date.now() - lastValidated < GRACE_PERIOD_MS;\n}\n\n// ── Layer 15: Gradual degradation after extended offline ──\n\nexport type DegradationLevel = 'none' | 'warning' | 'limited' | 'expired';\nexport type RevalidationResult = 'valid' | 'grace' | 'expired';\n\n/**\n * Returns the degradation level based on time since last server validation.\n * - 0-7 days: none (full access)\n * - 7-14 days: warning (shows a notice but still works)\n * - 14-30 days: limited (some features disabled)\n * - 30+ days: expired (all Pro features disabled)\n */\nexport function getDegradationLevel(license: LicenseData): DegradationLevel {\n const lastValidated = new Date(license.lastValidatedAt).getTime();\n if (isNaN(lastValidated)) return 'expired'; // corrupted date → deny access\n const elapsed = Date.now() - lastValidated;\n if (elapsed < 0) return 'none'; // clock skew: future timestamp → treat as fresh\n const days = elapsed / (24 * 60 * 60 * 1000);\n\n if (days <= 7) return 'none';\n if (days <= 14) return 'warning';\n if (days <= 30) return 'limited';\n return 'expired';\n}\n\n// Layer 10: License key format validation\nfunction validateLicenseKey(key: string): void {\n // Polar keys are UUID-like: 8-4-4-4-12 hex chars or similar\n // Reject obviously invalid keys to avoid unnecessary API calls\n if (key.length < 10 || key.length > 100) {\n throw new Error('Invalid license key format');\n }\n // Only allow alphanumeric, hyphens, underscores\n if (!/^[\\w-]+$/.test(key)) {\n throw new Error('Invalid license key format');\n }\n}\n\n// Polar license-key benefits use distinct prefixes per tier:\n// Pro Monthly/Yearly → \"BTUI-...\"\n// Team Monthly/Yearly → \"BTUI-T-...\"\n// We detect the tier from the prefix instead of looking up the productId,\n// because Polar's customer-portal license endpoints don't echo product info\n// in the activation response.\nfunction detectPlan(key: string): 'pro' | 'team' {\n const upper = key.toUpperCase();\n return upper.startsWith('BTUI-T-') || upper.startsWith('BTUI-T_') ? 'team' : 'pro';\n}\n\nexport async function activate(key: string): Promise<LicenseData> {\n validateLicenseKey(key);\n checkRateLimit();\n\n let success = false;\n try {\n const res = await apiActivate(key);\n\n if (!res.activated) {\n throw new Error(res.error ?? 'Activation failed');\n }\n\n const license: LicenseData = {\n key,\n instanceId: res.instance.id,\n status: 'active',\n customerEmail: res.meta.customer_email,\n customerName: res.meta.customer_name,\n plan: detectPlan(key),\n activatedAt: new Date().toISOString(),\n expiresAt: res.license_key.expires_at,\n lastValidatedAt: new Date().toISOString(),\n };\n\n await saveLicense(license);\n success = true;\n return license;\n } finally {\n recordAttempt(success);\n }\n}\n\n/**\n * Revalidate the license against the server.\n * This also serves as Layer 19 (telemetry): each validation call\n * allows Polar to track activation count, last-seen timestamp,\n * and detect if the activation limit is exceeded (license sharing).\n */\n// EP-006: Detect if an error is a network error vs validation/contract error\nfunction isNetworkError(err: unknown): boolean {\n const msg = err instanceof Error ? err.message : String(err);\n return /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|network|timeout|abort/i.test(msg);\n}\n\nexport async function revalidate(license: LicenseData): Promise<RevalidationResult> {\n try {\n const res = await apiValidate(license.key, license.instanceId);\n\n if (res.valid) {\n const updated: LicenseData = {\n ...license,\n lastValidatedAt: new Date().toISOString(),\n status: 'active',\n expiresAt: res.license_key.expires_at,\n };\n await saveLicense(updated);\n return 'valid';\n }\n\n await saveLicense({ ...license, status: 'expired' });\n return 'expired';\n } catch (err) {\n // EP-006: Network errors trigger grace period; validation/contract errors mean expired\n if (isNetworkError(err)) {\n return isWithinGracePeriod(license) ? 'grace' : 'expired';\n }\n // Unexpected response or contract violation — treat as expired\n await saveLicense({ ...license, status: 'expired' });\n return 'expired';\n }\n}\n\nexport async function deactivate(license: LicenseData): Promise<{ remoteSuccess: boolean }> {\n // EP-001: apiDeactivate already wraps fetchWithRetry (3 attempts). The\n // outer loop multiplied that into 9 POSTs — Polar would count each as a\n // separate request and a flaky network would amplify load 3×.\n let remoteSuccess = false;\n try {\n await apiDeactivate(license.key, license.instanceId);\n remoteSuccess = true;\n } catch { /* local clear still happens below */ }\n await clearLicense();\n return { remoteSuccess };\n}\n","import { createHash } from 'node:crypto';\nimport type { PolarActivateResponse, PolarValidateResponse } from './types.js';\nimport { fetchWithRetry } from '../fetch-timeout.js';\nimport { getMachineId } from '../data-dir.js';\n\n// BK-009: hash truncado SHA-256 del machineId — opacidad adicional frente a\n// correlacion en logs de Polar. El servidor solo necesita un identificador\n// estable por equipo; no requiere el UUID en claro.\nfunction hashMachineLabel(machineId: string): string {\n return createHash('sha256').update(machineId).digest('hex').slice(0, 32);\n}\n\nconst BASE_URL = 'https://api.polar.sh/v1/customer-portal/license-keys';\n\n// ── GOV-004: Public organization ID (not a secret) ──\n// This is the public Polar organization identifier used for license key operations.\n// Found at: polar.sh/dashboard -> Settings -> General\nexport const POLAR_ORGANIZATION_ID = 'b8f245c0-d116-4457-92fb-1bda47139f82';\n\n// Polar product IDs (public, not secret) — useful for analytics, support, and\n// future server-side validation that wants to confirm what the customer bought.\nexport const POLAR_PRODUCT_IDS = {\n proMonthly: 'b925b882-464c-40c1-9ffd-b088ab31d9a3',\n proYearly: '8f97bb81-b950-4bc3-97c5-8133dd817d0b',\n teamMonthly: '7cf3fcb2-560d-4fbb-9936-15efac511b23',\n teamYearly: 'd096914d-902d-47b0-8d62-5c7e6fc4e087',\n} as const;\n\n// Public checkout URLs surfaced from the landing page and the CLI upgrade prompt.\n// Team links carry ?quantity=3 because Polar has no native min-seats enforcement\n// and the Team tier is sold from 3 seats up.\nexport const POLAR_CHECKOUT_URLS = {\n proMonthly: 'https://buy.polar.sh/polar_cl_QW1ZJ9887bU74drGr7JfujQfm3RKYnn1fuvc53DqD6D',\n proYearly: 'https://buy.polar.sh/polar_cl_yQsiUeDelyyEQznbWffD1j77JAyP24ra7iEVQ22PA4h',\n teamMonthly: 'https://buy.polar.sh/polar_cl_CO6xqSzKgFiQJwXnhZYGqisOP04Wspi0KKZSn38NjFZ?quantity=3',\n teamYearly: 'https://buy.polar.sh/polar_cl_BZowqmtaKwWEkRJNtBcashWg7oZOH6OhnnsJ204opNA?quantity=3',\n} as const;\n\n// Layer 11: API URL validation\nfunction validateApiUrl(url: string): void {\n const parsed = new URL(url);\n if (parsed.protocol !== 'https:') {\n throw new Error('HTTPS required for license API');\n }\n if (!parsed.hostname.endsWith('polar.sh')) {\n throw new Error('Invalid API host');\n }\n}\n\n// Raw Polar response shapes\ninterface PolarActivation {\n id: string; // activation_id\n license_key: {\n status: string;\n expires_at: string | null;\n };\n}\n\ninterface PolarValidated {\n id: string;\n status: string; // 'granted' | 'revoked' | 'disabled'\n expires_at: string | null;\n customer: {\n email: string | null;\n name: string | null;\n };\n activation: { id: string } | null;\n}\n\nasync function post<T>(endpoint: string, body: Record<string, unknown>, expectEmpty = false): Promise<T> {\n // BK-008: Polar requiere trailing slash en sus rutas. Sin la barra final el\n // servidor responde 307 y `fetch` con redirect followed pierde la cabecera\n // Authorization → 405. Aseguramos la barra final aqui para que el caller\n // pueda seguir usando rutas semanticas sin recordar la convencion.\n const url = `${BASE_URL}/${endpoint}/`;\n validateApiUrl(url);\n\n const res = await fetchWithRetry(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n }, 15_000);\n\n if (!res.ok) {\n let message = `Request failed with status ${res.status}`;\n try {\n const errBody = await res.json() as { detail?: string; error?: string; message?: string };\n if (typeof errBody.detail === 'string') message = errBody.detail;\n else if (typeof errBody.error === 'string') message = errBody.error;\n else if (typeof errBody.message === 'string') message = errBody.message;\n } catch {\n // non-JSON error body — use generic message above\n }\n throw new Error(message);\n }\n\n if (expectEmpty || res.status === 204) return undefined as T;\n return res.json() as Promise<T>;\n}\n\nexport async function activateLicense(key: string): Promise<PolarActivateResponse> {\n const machineId = await getMachineId();\n\n const activation = await post<PolarActivation>('activate', {\n key,\n organization_id: POLAR_ORGANIZATION_ID,\n // SEG-004 + BK-009: identificador estable por equipo, hasheado para\n // que el UUID en claro no aparezca en logs de Polar.\n label: hashMachineLabel(machineId),\n });\n\n // EP-001: Runtime validation of activation response\n if (!activation || typeof activation.id !== 'string' || !activation.license_key) {\n throw new Error('Invalid activation response: missing required fields');\n }\n\n // Polar's activate response doesn't include customer info — fetch it via validate\n let customerEmail = '';\n let customerName = '';\n try {\n const validated = await post<PolarValidated>('validate', {\n key,\n organization_id: POLAR_ORGANIZATION_ID,\n activation_id: activation.id,\n });\n customerEmail = validated.customer?.email ?? '';\n customerName = validated.customer?.name ?? '';\n } catch {\n // customer info is non-critical — activation still succeeds\n }\n\n return {\n activated: true,\n error: null,\n instance: { id: activation.id },\n license_key: {\n id: 0,\n status: activation.license_key.status,\n key,\n activation_limit: 0,\n activations_count: 0,\n expires_at: activation.license_key.expires_at,\n },\n meta: { customer_email: customerEmail, customer_name: customerName },\n };\n}\n\nexport async function validateLicense(key: string, instanceId: string): Promise<PolarValidateResponse> {\n const res = await post<PolarValidated>('validate', {\n key,\n organization_id: POLAR_ORGANIZATION_ID,\n activation_id: instanceId,\n });\n\n // EP-002: Runtime validation of validate response\n if (!res || typeof res.id !== 'string' || typeof res.status !== 'string' || !res.customer) {\n throw new Error('Invalid validation response: missing required fields');\n }\n\n const notExpired = res.expires_at === null || new Date(res.expires_at) > new Date();\n const valid = res.status === 'granted' && notExpired;\n\n return {\n valid,\n error: valid ? null : `License ${res.status}`,\n license_key: {\n id: 0,\n status: res.status,\n key,\n expires_at: res.expires_at,\n },\n instance: { id: instanceId },\n };\n}\n\nexport async function deactivateLicense(key: string, instanceId: string): Promise<void> {\n await post<void>(\n 'deactivate',\n { key, organization_id: POLAR_ORGANIZATION_ID, activation_id: instanceId },\n true,\n );\n}\n","export interface LicenseData {\n key: string;\n instanceId: string;\n status: 'active' | 'expired' | 'inactive';\n customerEmail: string;\n customerName: string;\n plan: 'pro' | 'team';\n activatedAt: string;\n expiresAt: string | null;\n lastValidatedAt: string;\n}\n\n// BK-006: type guard for license payload after AES-GCM decrypt. A corrupt or\n// migrated file could JSON.parse to anything — refuse instead of crashing on\n// undefined accesses downstream.\nexport function isLicenseData(value: unknown): value is LicenseData {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.key === 'string' &&\n typeof v.instanceId === 'string' &&\n (v.status === 'active' || v.status === 'expired' || v.status === 'inactive') &&\n typeof v.customerEmail === 'string' &&\n typeof v.customerName === 'string' &&\n (v.plan === 'pro' || v.plan === 'team') &&\n typeof v.activatedAt === 'string' &&\n (v.expiresAt === null || typeof v.expiresAt === 'string') &&\n typeof v.lastValidatedAt === 'string'\n );\n}\n\nexport interface LicenseFile {\n version: 1;\n license?: LicenseData | null; // legacy unencrypted\n hmac?: string; // legacy\n encrypted?: string; // AES-256-GCM encrypted license JSON\n iv?: string;\n tag?: string;\n}\n\nexport type LicenseStatus = 'free' | 'pro' | 'team' | 'expired' | 'validating';\n\nexport interface PolarActivateResponse {\n activated: boolean;\n error: string | null;\n license_key: {\n id: number;\n status: string;\n key: string;\n activation_limit: number;\n activations_count: number;\n expires_at: string | null;\n };\n instance: { id: string };\n meta: { customer_name: string; customer_email: string };\n}\n\nexport interface PolarValidateResponse {\n valid: boolean;\n error: string | null;\n license_key: {\n id: number;\n status: string;\n key: string;\n expires_at: string | null;\n };\n instance: { id: string };\n}\n\nexport type ProFeatureId =\n | 'profiles'\n | 'smart-cleanup'\n | 'history'\n | 'security-audit'\n | 'rollback'\n | 'brewfile'\n | 'sync'\n | 'impact-analysis';\n\nexport type TeamFeatureId = 'compliance';\n"],"mappings":"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","names":[]}
@@ -378,6 +378,7 @@ var en = {
378
378
  cli_brewtuibarForeignBundle: "\u2718 /Applications/Brew-TUI-Bar.app exists but its bundle ID is `{{id}}`, not com.molinesdesigns.brewtuibar. Refusing to touch a foreign app. Remove or rename it first.",
379
379
  postinstall_skipped: "Note: Brew-TUI-Bar auto-install skipped: {{error}}",
380
380
  postinstall_manualHint: "You can install it manually later with: brew-tui install-brew-tui-bar",
381
+ cli_versionMismatchWarning: "\u26A0 Brew-TUI-Bar {{installed}} is out of sync with this CLI ({{expected}}). It will be updated automatically the next time you run `brew-tui` or restart the app.",
381
382
  cli_deactivateRemoteFailed: "\u26A0 Warning: Could not reach the server to deactivate remotely. The license was removed locally but may still count as active.",
382
383
  // ── License degradation (Layer 15) ──
383
384
  license_offlineWarning: "Your license has not been validated for {{days}} days. Please connect to the internet.",
@@ -901,6 +902,7 @@ var es = {
901
902
  cli_brewtuibarForeignBundle: "\u2718 /Applications/Brew-TUI-Bar.app existe pero su bundle ID es `{{id}}`, no com.molinesdesigns.brewtuibar. No se tocar\xE1 una app ajena. Elim\xEDnala o ren\xF3mbrala primero.",
902
903
  postinstall_skipped: "Nota: instalaci\xF3n autom\xE1tica de Brew-TUI-Bar saltada: {{error}}",
903
904
  postinstall_manualHint: "Puedes instalarla manualmente m\xE1s tarde con: brew-tui install-brew-tui-bar",
905
+ cli_versionMismatchWarning: "\u26A0 Brew-TUI-Bar {{installed}} no coincide con esta CLI ({{expected}}). Se actualizar\xE1 autom\xE1ticamente la pr\xF3xima vez que ejecutes `brew-tui` o reinicies la app.",
904
906
  cli_deactivateRemoteFailed: "\u26A0 Advertencia: No se pudo contactar al servidor para desactivar remotamente. La licencia se elimin\xF3 localmente pero puede seguir contando como activa.",
905
907
  // ── License degradation (Layer 15) ──
906
908
  license_offlineWarning: "Tu licencia no se ha validado en {{days}} d\xEDas. Por favor con\xE9ctate a internet.",
@@ -1095,4 +1097,4 @@ export {
1095
1097
  t,
1096
1098
  tp
1097
1099
  };
1098
- //# sourceMappingURL=chunk-F2S7TGCS.js.map
1100
+ //# sourceMappingURL=chunk-SDQYHY2L.js.map