estatehelm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3012 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_commander = require("commander");
28
+
29
+ // src/login.ts
30
+ var readline = __toESM(require("readline"));
31
+ var import_open = __toESM(require("open"));
32
+
33
+ // ../api-client/src/auth.ts
34
+ var TokenAuthAdapter = class {
35
+ getToken;
36
+ constructor(getToken) {
37
+ this.getToken = getToken;
38
+ }
39
+ async getAuthHeaders() {
40
+ const token = await this.getToken();
41
+ if (token) {
42
+ return { Authorization: `Bearer ${token}` };
43
+ }
44
+ return {};
45
+ }
46
+ getCredentials() {
47
+ return "same-origin";
48
+ }
49
+ };
50
+
51
+ // ../api-client/src/types.ts
52
+ var HOUSEHOLD_MEMBER_ROLES = {
53
+ OWNER: "owner",
54
+ MEMBER: "member",
55
+ EXECUTOR: "executor"
56
+ };
57
+
58
+ // ../api-client/src/client.ts
59
+ var EntityBatcher = class {
60
+ pending = /* @__PURE__ */ new Map();
61
+ householdId = null;
62
+ includeDeleted = false;
63
+ timer = null;
64
+ fetchFn;
65
+ constructor(fetchFn) {
66
+ this.fetchFn = fetchFn;
67
+ }
68
+ request(householdId, entityType, includeDeleted = false) {
69
+ if (this.pending.size > 0 && this.householdId !== householdId) {
70
+ this.flush();
71
+ }
72
+ this.householdId = householdId;
73
+ this.includeDeleted = includeDeleted;
74
+ return new Promise((resolve, reject) => {
75
+ this.pending.set(entityType, { resolve, reject });
76
+ if (!this.timer) {
77
+ this.timer = setTimeout(() => this.flush(), 0);
78
+ }
79
+ });
80
+ }
81
+ async flush() {
82
+ const entityTypes = Array.from(this.pending.keys());
83
+ const callbacks = new Map(this.pending);
84
+ const householdId = this.householdId;
85
+ const includeDeleted = this.includeDeleted;
86
+ this.pending.clear();
87
+ this.timer = null;
88
+ this.householdId = null;
89
+ this.includeDeleted = false;
90
+ if (entityTypes.length === 0) return;
91
+ try {
92
+ const response = await this.fetchFn(householdId, entityTypes, includeDeleted);
93
+ const items = response.items || [];
94
+ const groupedByType = /* @__PURE__ */ new Map();
95
+ for (const type of entityTypes) {
96
+ groupedByType.set(type, []);
97
+ }
98
+ for (const item of items) {
99
+ const type = item.entityType;
100
+ if (groupedByType.has(type)) {
101
+ groupedByType.get(type).push(item);
102
+ }
103
+ }
104
+ for (const [type, { resolve }] of callbacks) {
105
+ resolve(groupedByType.get(type) || []);
106
+ }
107
+ } catch (err) {
108
+ for (const { reject } of callbacks.values()) {
109
+ reject(err);
110
+ }
111
+ }
112
+ }
113
+ };
114
+ var ApiClient = class {
115
+ config;
116
+ // Cache for in-flight GET requests to deduplicate concurrent calls
117
+ inFlightRequests = /* @__PURE__ */ new Map();
118
+ // Batcher for entity requests
119
+ entityBatcher;
120
+ constructor(config) {
121
+ this.config = config;
122
+ this.entityBatcher = new EntityBatcher(
123
+ (householdId, entityTypes, includeDeleted) => this.getEntitiesMultiple(householdId, entityTypes, includeDeleted)
124
+ );
125
+ }
126
+ getApiUrl(path3) {
127
+ const cleanPath = path3.startsWith("/") ? path3 : `/${path3}`;
128
+ return `${this.config.baseUrl}/api/${this.config.apiVersion}${cleanPath}`;
129
+ }
130
+ async request(method, path3, options) {
131
+ const url = this.getApiUrl(path3);
132
+ const authHeaders = await this.config.auth.getAuthHeaders();
133
+ const headers = {
134
+ "Content-Type": "application/json",
135
+ ...authHeaders,
136
+ ...options?.headers
137
+ };
138
+ const config = {
139
+ method,
140
+ headers,
141
+ credentials: this.config.auth.getCredentials()
142
+ };
143
+ if (options?.body) {
144
+ config.body = JSON.stringify(options.body);
145
+ }
146
+ let response;
147
+ try {
148
+ response = await fetch(url, config);
149
+ } catch (networkError) {
150
+ console.error("Network error:", { url, method, error: networkError });
151
+ const error = Object.assign(
152
+ new Error("Unable to connect to server. Please check your connection and try again."),
153
+ { status: 0, code: "NETWORK_ERROR", originalError: networkError }
154
+ );
155
+ throw error;
156
+ }
157
+ if (!response.ok) {
158
+ const error = await response.json().catch(() => ({
159
+ message: "An error occurred"
160
+ }));
161
+ const logLevel = response.status === 400 || response.status === 404 ? console.debug : console.error;
162
+ logLevel("API Error:", {
163
+ url,
164
+ method,
165
+ status: response.status,
166
+ error
167
+ });
168
+ if (response.status === 401) {
169
+ console.warn("Authentication required");
170
+ this.config.onAuthError?.();
171
+ throw Object.assign(new Error("Authentication required"), { status: 401 });
172
+ }
173
+ if (response.status === 402) {
174
+ throw Object.assign(
175
+ new Error(error.message || "Upgrade required to access this feature"),
176
+ {
177
+ status: 402,
178
+ code: error.error || "limit_exceeded",
179
+ details: error.details,
180
+ requiredPlan: error.details?.requiredPlan
181
+ }
182
+ );
183
+ }
184
+ if (response.status === 403) {
185
+ throw Object.assign(
186
+ new Error(error.message || "You do not have permission to perform this action"),
187
+ { status: 403, code: "FORBIDDEN" }
188
+ );
189
+ }
190
+ if (response.status === 412) {
191
+ throw Object.assign(
192
+ new Error("VERSION_CONFLICT"),
193
+ {
194
+ status: 412,
195
+ code: "VERSION_CONFLICT",
196
+ currentVersion: error.details?.currentVersion,
197
+ expectedVersion: error.details?.expectedVersion
198
+ }
199
+ );
200
+ }
201
+ let errorMessage = error.error || error.message || error.title || `HTTP ${response.status}`;
202
+ if (error.errors && typeof error.errors === "object") {
203
+ const validationErrors = Object.entries(error.errors).map(([field, messages]) => {
204
+ const msgs = Array.isArray(messages) ? messages : [messages];
205
+ return `${field}: ${msgs.join(", ")}`;
206
+ }).join("; ");
207
+ errorMessage = validationErrors || errorMessage;
208
+ }
209
+ const apiError = Object.assign(new Error(errorMessage), {
210
+ status: response.status,
211
+ error
212
+ });
213
+ throw apiError;
214
+ }
215
+ if (response.status === 204) {
216
+ return {};
217
+ }
218
+ return response.json();
219
+ }
220
+ async get(path3, options) {
221
+ const deduplicatePaths = ["/webauthn/credentials", "/webauthn/key-bundles/current"];
222
+ const shouldDeduplicate = deduplicatePaths.some((p) => path3.includes(p));
223
+ if (shouldDeduplicate) {
224
+ const cacheKey = `get:${path3}`;
225
+ const inFlight = this.inFlightRequests.get(cacheKey);
226
+ if (inFlight) {
227
+ return inFlight;
228
+ }
229
+ const request = this.request("GET", path3, options).finally(() => this.inFlightRequests.delete(cacheKey));
230
+ this.inFlightRequests.set(cacheKey, request);
231
+ return request;
232
+ }
233
+ return this.request("GET", path3, options);
234
+ }
235
+ async post(path3, body, options) {
236
+ return this.request("POST", path3, { ...options, body });
237
+ }
238
+ async put(path3, body, options) {
239
+ return this.request("PUT", path3, { ...options, body });
240
+ }
241
+ async patch(path3, body, options) {
242
+ return this.request("PATCH", path3, { ...options, body });
243
+ }
244
+ async delete(path3, options) {
245
+ return this.request("DELETE", path3, options);
246
+ }
247
+ /**
248
+ * Fetch binary data as a Blob (for downloading files like PDFs).
249
+ */
250
+ async getBlob(path3, options) {
251
+ const url = this.getApiUrl(path3);
252
+ const authHeaders = await this.config.auth.getAuthHeaders();
253
+ const headers = {
254
+ ...authHeaders,
255
+ ...options?.headers
256
+ };
257
+ const config = {
258
+ method: "GET",
259
+ headers,
260
+ credentials: this.config.auth.getCredentials()
261
+ };
262
+ let response;
263
+ try {
264
+ response = await fetch(url, config);
265
+ } catch (networkError) {
266
+ console.error("Network error:", { url, error: networkError });
267
+ const error = Object.assign(
268
+ new Error("Unable to connect to server. Please check your connection and try again."),
269
+ { status: 0, code: "NETWORK_ERROR", originalError: networkError }
270
+ );
271
+ throw error;
272
+ }
273
+ if (!response.ok) {
274
+ const error = await response.json().catch(() => ({
275
+ message: "An error occurred"
276
+ }));
277
+ const apiError = Object.assign(new Error(error.message || `HTTP ${response.status}`), {
278
+ status: response.status,
279
+ error
280
+ });
281
+ throw apiError;
282
+ }
283
+ return response.blob();
284
+ }
285
+ // ===== HOUSEHOLD METHODS =====
286
+ async getHouseholds() {
287
+ return this.get("/households");
288
+ }
289
+ async createHousehold(data) {
290
+ return this.post("/households", data);
291
+ }
292
+ async updateHousehold(id, data) {
293
+ await this.put(`/households/${id}`, data);
294
+ }
295
+ async deleteHousehold(id) {
296
+ await this.delete(`/households/${id}`);
297
+ }
298
+ /**
299
+ * Extend trial for households with less than 5 entities.
300
+ * Allows users who haven't fully explored to continue without subscribing.
301
+ */
302
+ async extendTrial(householdId) {
303
+ return this.post(`/households/${householdId}/extend-trial`, {});
304
+ }
305
+ /**
306
+ * Permanently delete the current user's account and all personal data.
307
+ * This includes: key bundles, WebAuthn credentials, personal vault entities,
308
+ * member key access records, legal consents, user profile, and authentication identity.
309
+ *
310
+ * WARNING: This action is irreversible. The user must re-register to use the service again.
311
+ */
312
+ async deleteCurrentUser() {
313
+ await this.delete("/users/me");
314
+ }
315
+ async checkUserHasHousehold() {
316
+ const households = await this.getHouseholds();
317
+ return {
318
+ hasHousehold: households.length > 0,
319
+ households: households.length > 0 ? households : void 0
320
+ };
321
+ }
322
+ // ===== HOUSEHOLD KEYS METHODS =====
323
+ async getHouseholdKeys(householdId) {
324
+ const cacheKey = `getHouseholdKeys:${householdId}`;
325
+ const inFlight = this.inFlightRequests.get(cacheKey);
326
+ if (inFlight) {
327
+ return inFlight;
328
+ }
329
+ const request = this.get(`/households/${householdId}/keys`).finally(() => {
330
+ this.inFlightRequests.delete(cacheKey);
331
+ });
332
+ this.inFlightRequests.set(cacheKey, request);
333
+ return request;
334
+ }
335
+ async grantHouseholdKey(householdId, data) {
336
+ await this.post(`/households/${householdId}/keys/grant`, data);
337
+ }
338
+ async batchGrantHouseholdKeys(householdId, data) {
339
+ await this.post(`/households/${householdId}/keys/grant/batch`, data);
340
+ }
341
+ async revokeHouseholdKey(householdId, userId, keyType) {
342
+ await this.delete(`/households/${householdId}/keys/${userId}/${keyType}`);
343
+ }
344
+ async getMembersKeyAccess(householdId) {
345
+ return this.get(`/households/${householdId}/keys/access`);
346
+ }
347
+ async getKeyTypes(householdId) {
348
+ return this.get(`/households/${householdId}/keys/types`);
349
+ }
350
+ async deleteKeyType(householdId, keyType) {
351
+ await this.delete(`/households/${householdId}/keys/types/${keyType}`);
352
+ }
353
+ // ===== ENCRYPTED ENTITIES METHODS =====
354
+ /**
355
+ * Fetch entities for a single type. When batched=true (default), requests are
356
+ * automatically combined with other concurrent calls into a single HTTP request.
357
+ */
358
+ async getEntities(householdId, params) {
359
+ const batched = params?.batched ?? true;
360
+ if (batched && params?.entityType && !params?.limit && !params?.offset) {
361
+ const items = await this.entityBatcher.request(
362
+ householdId,
363
+ params.entityType,
364
+ params.includeDeleted ?? false
365
+ );
366
+ return { items, total: items.length };
367
+ }
368
+ const queryParams = new URLSearchParams();
369
+ if (householdId) queryParams.set("householdId", householdId);
370
+ if (params?.entityType) queryParams.set("entityType", params.entityType);
371
+ if (params?.limit) queryParams.set("limit", params.limit.toString());
372
+ if (params?.offset) queryParams.set("offset", params.offset.toString());
373
+ if (params?.includeDeleted) queryParams.set("includeDeleted", params.includeDeleted.toString());
374
+ const query = queryParams.toString();
375
+ const path3 = `/entities${query ? `?${query}` : ""}`;
376
+ return this.get(path3);
377
+ }
378
+ /**
379
+ * Internal method to fetch multiple entity types in one request.
380
+ * Used by the EntityBatcher.
381
+ */
382
+ async getEntitiesMultiple(householdId, entityTypes, includeDeleted) {
383
+ const queryParams = new URLSearchParams();
384
+ if (householdId) queryParams.set("householdId", householdId);
385
+ if (entityTypes.length > 0) queryParams.set("entityTypes", entityTypes.join(","));
386
+ if (includeDeleted) queryParams.set("includeDeleted", "true");
387
+ const query = queryParams.toString();
388
+ const path3 = `/entities${query ? `?${query}` : ""}`;
389
+ return this.get(path3);
390
+ }
391
+ async getEntity(householdId, entityId) {
392
+ const queryParams = new URLSearchParams();
393
+ if (householdId) queryParams.set("householdId", householdId);
394
+ const query = queryParams.toString();
395
+ const path3 = `/entities/${entityId}${query ? `?${query}` : ""}`;
396
+ return this.get(path3);
397
+ }
398
+ async createEntity(householdId, data) {
399
+ const queryParams = new URLSearchParams();
400
+ if (householdId) queryParams.set("householdId", householdId);
401
+ const query = queryParams.toString();
402
+ const path3 = `/entities${query ? `?${query}` : ""}`;
403
+ return this.post(path3, data);
404
+ }
405
+ async updateEntity(householdId, entityId, data, version) {
406
+ const queryParams = new URLSearchParams();
407
+ if (householdId) queryParams.set("householdId", householdId);
408
+ const query = queryParams.toString();
409
+ const path3 = `/entities/${entityId}${query ? `?${query}` : ""}`;
410
+ return this.put(path3, data, {
411
+ headers: { "If-Match": `"${version}"` }
412
+ });
413
+ }
414
+ async deleteEntity(householdId, entityId, version) {
415
+ const queryParams = new URLSearchParams();
416
+ if (householdId) queryParams.set("householdId", householdId);
417
+ const query = queryParams.toString();
418
+ const path3 = `/entities/${entityId}${query ? `?${query}` : ""}`;
419
+ await this.delete(path3, {
420
+ headers: { "If-Match": `"${version}"` }
421
+ });
422
+ }
423
+ // ===== HOUSEHOLD MEMBERS METHODS =====
424
+ async getHouseholdMembers(householdId) {
425
+ return this.get(`/households/${householdId}/members`);
426
+ }
427
+ async inviteHouseholdMember(householdId, email, role = HOUSEHOLD_MEMBER_ROLES.MEMBER) {
428
+ return this.post(`/households/${householdId}/members/invite`, { email, role });
429
+ }
430
+ async updateMemberRole(householdId, userId, data) {
431
+ await this.put(`/households/${householdId}/members/${userId}`, data);
432
+ }
433
+ async removeHouseholdMember(householdId, userId) {
434
+ await this.delete(`/households/${householdId}/members/${userId}`);
435
+ }
436
+ async getHouseholdPeople(householdId) {
437
+ const members = await this.getHouseholdMembers(householdId);
438
+ return { people: members };
439
+ }
440
+ // DEPRECATED: Legacy invitation methods - will be removed
441
+ async createHouseholdInvitation(householdId, role = HOUSEHOLD_MEMBER_ROLES.MEMBER, residentId, expiresIn, oneTimeUse) {
442
+ return this.post(`/households/${householdId}/invitations`, {
443
+ role,
444
+ residentId,
445
+ expiresIn,
446
+ oneTimeUse
447
+ });
448
+ }
449
+ async revokeInvitation(invitationToken) {
450
+ await this.delete(`/households/invitations/${invitationToken}`);
451
+ }
452
+ // ===== CONTACTS =====
453
+ async getContacts(householdId) {
454
+ return this.get(`/contacts?householdId=${householdId}`);
455
+ }
456
+ async createContact(householdId, data) {
457
+ return this.post("/contacts", { ...data, household_id: householdId });
458
+ }
459
+ async updateContact(id, data) {
460
+ return this.put(`/contacts/${id}`, data);
461
+ }
462
+ async deleteContact(id) {
463
+ await this.delete(`/contacts/${id}`);
464
+ }
465
+ // ===== INSURANCE POLICIES =====
466
+ async getInsurancePolicies(householdId) {
467
+ return this.get(`/insurance-policies?householdId=${householdId}`);
468
+ }
469
+ async createInsurancePolicy(householdId, data) {
470
+ return this.post("/insurance-policies", { ...data, household_id: householdId });
471
+ }
472
+ async updateInsurancePolicy(id, data, householdId) {
473
+ return this.put(`/insurance-policies/${id}`, { ...data, household_id: householdId });
474
+ }
475
+ async deleteInsurancePolicy(id) {
476
+ await this.delete(`/insurance-policies/${id}`);
477
+ }
478
+ // ===== GENERIC RESOURCE METHODS =====
479
+ // These are used by entity contexts and can be extended
480
+ async getResources(path3, householdId) {
481
+ return this.get(`${path3}?householdId=${householdId}`);
482
+ }
483
+ async createResource(path3, householdId, data) {
484
+ return this.post(path3, { ...data, household_id: householdId });
485
+ }
486
+ async updateResource(path3, id, data) {
487
+ return this.put(`${path3}/${id}`, data);
488
+ }
489
+ async deleteResource(path3, id) {
490
+ await this.delete(`${path3}/${id}`);
491
+ }
492
+ // ===== USER KEY METHODS =====
493
+ async batchAddKeys(data) {
494
+ return this.post("/users/batch-add-keys", data);
495
+ }
496
+ /**
497
+ * Get the current user's key bundle (public key, encrypted private key, etc.)
498
+ * This is deduplicated automatically - concurrent calls return the same promise.
499
+ */
500
+ async getCurrentKeyBundle() {
501
+ return this.get("/webauthn/key-bundles/current");
502
+ }
503
+ // ===== BILLING METHODS =====
504
+ /**
505
+ * Create a Stripe Checkout session to start a subscription.
506
+ * Only household owners can call this.
507
+ */
508
+ async createCheckoutSession(householdId, planCode) {
509
+ return this.post(`/billing/${householdId}/checkout`, { planCode });
510
+ }
511
+ /**
512
+ * Create a Stripe Customer Portal session for managing subscription.
513
+ * Only household owners can call this.
514
+ */
515
+ async createPortalSession(householdId) {
516
+ return this.post(`/billing/${householdId}/portal`, {});
517
+ }
518
+ /**
519
+ * Get available subscription plans.
520
+ * This endpoint is public (no auth required).
521
+ */
522
+ async getAvailablePlans() {
523
+ return this.get("/billing/plans");
524
+ }
525
+ /**
526
+ * Start a free trial for a household.
527
+ * No credit card required.
528
+ */
529
+ async startTrial(householdId, tier) {
530
+ return this.post(`/billing/${householdId}/start-trial`, { tier });
531
+ }
532
+ /**
533
+ * Upgrade an existing subscription to a new plan.
534
+ * This updates the subscription in place instead of creating a new one.
535
+ */
536
+ async upgradeSubscription(householdId, planCode) {
537
+ return this.post(`/billing/${householdId}/upgrade`, { planCode });
538
+ }
539
+ /**
540
+ * Get billing details for subscription sync.
541
+ * Used by frontend to sync EstateHelm subscription entity.
542
+ */
543
+ async getBillingDetails(householdId) {
544
+ return this.get(`/billing/${householdId}/details`);
545
+ }
546
+ // ===== MOBILE BILLING METHODS (Apple IAP / Google Play) =====
547
+ /**
548
+ * Verify an Apple In-App Purchase transaction.
549
+ * Called by iOS app after a successful StoreKit purchase.
550
+ */
551
+ async verifyApplePurchase(householdId, data) {
552
+ return this.post(`/apple-billing/${householdId}/verify-transaction`, data);
553
+ }
554
+ /**
555
+ * Sync Apple subscription status with the server.
556
+ */
557
+ async syncAppleSubscription(householdId) {
558
+ return this.post(`/apple-billing/${householdId}/sync`, {});
559
+ }
560
+ /**
561
+ * Get Apple subscription status for a household.
562
+ */
563
+ async getAppleSubscriptionStatus(householdId) {
564
+ return this.get(`/apple-billing/${householdId}/status`);
565
+ }
566
+ /**
567
+ * Get available Apple products (for StoreKit).
568
+ */
569
+ async getAppleProducts() {
570
+ return this.get("/apple-billing/products");
571
+ }
572
+ /**
573
+ * Verify a Google Play purchase.
574
+ * Called by Android app after a successful Google Play Billing purchase.
575
+ */
576
+ async verifyGooglePurchase(householdId, data) {
577
+ return this.post(`/google-billing/${householdId}/verify-purchase`, data);
578
+ }
579
+ /**
580
+ * Sync Google Play subscription status with the server.
581
+ */
582
+ async syncGoogleSubscription(householdId) {
583
+ return this.post(`/google-billing/${householdId}/sync`, {});
584
+ }
585
+ /**
586
+ * Get Google Play subscription status for a household.
587
+ */
588
+ async getGoogleSubscriptionStatus(householdId) {
589
+ return this.get(`/google-billing/${householdId}/status`);
590
+ }
591
+ /**
592
+ * Get available Google Play products.
593
+ */
594
+ async getGoogleProducts() {
595
+ return this.get("/google-billing/products");
596
+ }
597
+ };
598
+
599
+ // ../encryption/src/utils.ts
600
+ function base64Encode(bytes) {
601
+ const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("");
602
+ return btoa(binString);
603
+ }
604
+ function base64Decode(base64) {
605
+ try {
606
+ const cleanBase64 = base64.replace(/\s/g, "");
607
+ const binString = atob(cleanBase64);
608
+ return Uint8Array.from(binString, (char) => char.codePointAt(0));
609
+ } catch (error) {
610
+ throw new Error(`Failed to decode base64: ${error instanceof Error ? error.message : "Unknown error"}`);
611
+ }
612
+ }
613
+ function stringToBytes(str) {
614
+ return new TextEncoder().encode(str);
615
+ }
616
+ function bytesToString(bytes) {
617
+ return new TextDecoder().decode(bytes);
618
+ }
619
+
620
+ // ../encryption/src/householdKeys.ts
621
+ var HOUSEHOLD_KEY_SIZE = 32;
622
+ async function importHouseholdKeyForDerivation(keyBytes) {
623
+ if (keyBytes.length !== HOUSEHOLD_KEY_SIZE) {
624
+ throw new Error(`Invalid household key size: expected ${HOUSEHOLD_KEY_SIZE} bytes, got ${keyBytes.length}`);
625
+ }
626
+ try {
627
+ return await crypto.subtle.importKey(
628
+ "raw",
629
+ keyBytes,
630
+ "HKDF",
631
+ false,
632
+ ["deriveBits"]
633
+ );
634
+ } catch (error) {
635
+ throw new Error(
636
+ `Failed to import household key for derivation: ${error instanceof Error ? error.message : "Unknown error"}`
637
+ );
638
+ }
639
+ }
640
+
641
+ // ../encryption/src/entityKeys.ts
642
+ var ENTITY_KEY_SIZE = 32;
643
+ async function deriveEntityKey(householdKey, entityId, entityType) {
644
+ if (!entityId || entityId.trim().length === 0) {
645
+ throw new Error("Entity ID cannot be empty");
646
+ }
647
+ if (!entityType || entityType.trim().length === 0) {
648
+ throw new Error("Entity type cannot be empty");
649
+ }
650
+ const infoString = `${entityType}:${entityId}`;
651
+ try {
652
+ const keyMaterial = await importHouseholdKeyForDerivation(householdKey);
653
+ const info = stringToBytes(infoString);
654
+ const derivedBits = await crypto.subtle.deriveBits(
655
+ {
656
+ name: "HKDF",
657
+ hash: "SHA-256",
658
+ salt: new Uint8Array(0),
659
+ // Empty salt for deterministic derivation
660
+ info
661
+ },
662
+ keyMaterial,
663
+ ENTITY_KEY_SIZE * 8
664
+ // 256 bits
665
+ );
666
+ return new Uint8Array(derivedBits);
667
+ } catch (error) {
668
+ throw new Error(
669
+ `Failed to derive entity key for ${entityType}:${entityId}: ${error instanceof Error ? error.message : "Unknown error"}`
670
+ );
671
+ }
672
+ }
673
+ async function importEntityKey(entityKeyBytes) {
674
+ if (entityKeyBytes.length !== ENTITY_KEY_SIZE) {
675
+ throw new Error(`Invalid entity key size: expected ${ENTITY_KEY_SIZE} bytes, got ${entityKeyBytes.length}`);
676
+ }
677
+ try {
678
+ return await crypto.subtle.importKey(
679
+ "raw",
680
+ entityKeyBytes,
681
+ {
682
+ name: "AES-GCM",
683
+ length: ENTITY_KEY_SIZE * 8
684
+ },
685
+ false,
686
+ // Not extractable (security best practice)
687
+ ["encrypt", "decrypt"]
688
+ );
689
+ } catch (error) {
690
+ throw new Error(
691
+ `Failed to import entity key: ${error instanceof Error ? error.message : "Unknown error"}`
692
+ );
693
+ }
694
+ }
695
+
696
+ // ../encryption/src/entityEncryption.ts
697
+ var IV_SIZE = 12;
698
+ var AUTH_TAG_SIZE = 16;
699
+ function unpackEncryptedBlob(packedBlob) {
700
+ const blob = base64Decode(packedBlob);
701
+ if (blob.length < 1 + IV_SIZE + AUTH_TAG_SIZE) {
702
+ throw new Error("Invalid encrypted blob: too short");
703
+ }
704
+ const version = blob[0];
705
+ const iv = blob.slice(1, 1 + IV_SIZE);
706
+ const ciphertext = blob.slice(1 + IV_SIZE);
707
+ return { version, iv, ciphertext };
708
+ }
709
+ async function decryptEntity(householdKey, encrypted, options = {}) {
710
+ if (options.expectedType && encrypted.entityType !== options.expectedType) {
711
+ throw new Error(
712
+ `Entity type mismatch: expected ${options.expectedType}, got ${encrypted.entityType}`
713
+ );
714
+ }
715
+ try {
716
+ const entityKeyBytes = options.entityKey ? options.entityKey : await deriveEntityKey(householdKey, encrypted.entityId, encrypted.entityType);
717
+ const entityKey = await importEntityKey(entityKeyBytes);
718
+ const ciphertext = base64Decode(encrypted.ciphertext);
719
+ const iv = base64Decode(encrypted.iv);
720
+ const decryptParams = {
721
+ name: "AES-GCM",
722
+ iv
723
+ };
724
+ if (options.additionalData) {
725
+ decryptParams.additionalData = options.additionalData;
726
+ }
727
+ const plaintext = await crypto.subtle.decrypt(
728
+ decryptParams,
729
+ entityKey,
730
+ ciphertext
731
+ );
732
+ const jsonData = bytesToString(new Uint8Array(plaintext));
733
+ return JSON.parse(jsonData);
734
+ } catch (error) {
735
+ if (error instanceof Error && error.name === "OperationError") {
736
+ throw new Error(
737
+ `Failed to decrypt ${encrypted.entityType} entity ${encrypted.entityId}: Authentication failed. This may indicate a wrong household key, corrupted data, or tampered ciphertext.`
738
+ );
739
+ }
740
+ throw new Error(
741
+ `Failed to decrypt ${encrypted.entityType} entity ${encrypted.entityId}: ${error instanceof Error ? error.message : "Unknown error"}`
742
+ );
743
+ }
744
+ }
745
+
746
+ // ../encryption/src/entityKeyMapping.ts
747
+ var ENTITY_KEY_TYPE_MAP = {
748
+ // General household items
749
+ "property": "general",
750
+ "maintenance_task": "task",
751
+ "pet": "general",
752
+ "vehicle": "general",
753
+ "device": "general",
754
+ "valuable": "general",
755
+ "valuables": "general",
756
+ // Route alias
757
+ "access_code": "access_code",
758
+ "contact": "general",
759
+ "service": "general",
760
+ "document": "general",
761
+ "travel": "general",
762
+ "resident": "general",
763
+ "home_improvement": "general",
764
+ // Property improvements
765
+ "vehicle_maintenance": "general",
766
+ // Vehicle maintenance history
767
+ "vehicle_service_visit": "general",
768
+ // Vehicle service visits
769
+ "pet_vet_visit": "general",
770
+ // Pet vet visits (like vehicle_service_visit for vehicles)
771
+ "pet_health": "general",
772
+ // Pet health records (simple single records)
773
+ "military_record": "general",
774
+ // Military service records
775
+ "education_record": "general",
776
+ // Education records (diplomas, transcripts, etc.)
777
+ "credential": "general",
778
+ // Credentials (professional licenses, government IDs, etc.)
779
+ "credentials": "general",
780
+ // Route alias
781
+ "membership_record": "general",
782
+ // Membership records (airline, hotel, retail loyalty programs)
783
+ // Health records (sensitive - requires health key)
784
+ "health_record": "health",
785
+ // Financial
786
+ "bank_account": "financial",
787
+ "investment": "financial",
788
+ "tax_document": "financial",
789
+ "tax_year": "financial",
790
+ "taxes": "financial",
791
+ // Route alias
792
+ "financial_account": "financial",
793
+ "financial": "financial",
794
+ // Route alias
795
+ // Legal (owner-only)
796
+ "legal": "legal",
797
+ // Insurance (both names for compatibility)
798
+ "insurance": "general",
799
+ "insurance_policy": "general",
800
+ "subscription": "subscription",
801
+ // Passwords (shared household credentials)
802
+ "password": "password",
803
+ // Identity (Personal Vault)
804
+ "identity": "identity",
805
+ // Calendar Connections (Personal Vault - user's OAuth tokens for calendar sync)
806
+ "calendar_connection": "identity",
807
+ // Continuity (Personal Vault - shared messages for beneficiaries)
808
+ "continuity": "continuity",
809
+ // Emergency (household-scoped emergency info, encrypted with general key so all members can decrypt)
810
+ "emergency": "general"
811
+ };
812
+ function getKeyTypeForEntity(entityType) {
813
+ const keyType = ENTITY_KEY_TYPE_MAP[entityType];
814
+ if (!keyType) {
815
+ throw new Error(
816
+ `No key type mapping found for entity type: ${entityType}. Please add it to ENTITY_KEY_TYPE_MAP in entityKeyMapping.ts`
817
+ );
818
+ }
819
+ return keyType;
820
+ }
821
+
822
+ // ../encryption/src/recoveryKey.ts
823
+ var RECOVERY_KEY_SIZE = 16;
824
+ var GROUP_SIZE = 4;
825
+ var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
826
+ function encodeBase32(bytes) {
827
+ let bits = "";
828
+ for (const byte of bytes) {
829
+ bits += byte.toString(2).padStart(8, "0");
830
+ }
831
+ while (bits.length % 5 !== 0) {
832
+ bits += "0";
833
+ }
834
+ let result = "";
835
+ for (let i = 0; i < bits.length; i += 5) {
836
+ const chunk = bits.substring(i, i + 5);
837
+ const index = parseInt(chunk, 2);
838
+ result += BASE32_ALPHABET[index];
839
+ }
840
+ return result;
841
+ }
842
+ function decodeBase32(base32) {
843
+ const cleaned = base32.toUpperCase().replace(/[^A-Z2-7]/g, "");
844
+ let bits = "";
845
+ for (const char of cleaned) {
846
+ const index = BASE32_ALPHABET.indexOf(char);
847
+ if (index === -1) {
848
+ throw new Error(`Invalid base32 character: ${char}`);
849
+ }
850
+ bits += index.toString(2).padStart(5, "0");
851
+ }
852
+ const bytes = [];
853
+ for (let i = 0; i < bits.length - bits.length % 8; i += 8) {
854
+ const byte = parseInt(bits.substring(i, i + 8), 2);
855
+ bytes.push(byte);
856
+ }
857
+ return new Uint8Array(bytes);
858
+ }
859
+ function formatRecoveryKey(base32) {
860
+ const groups = [];
861
+ for (let i = 0; i < base32.length; i += GROUP_SIZE) {
862
+ groups.push(base32.substring(i, i + GROUP_SIZE));
863
+ }
864
+ return groups.join("-");
865
+ }
866
+ function unformatRecoveryKey(formatted) {
867
+ return formatted.toUpperCase().replace(/[^A-Z2-7]/g, "");
868
+ }
869
+ function validateRecoveryKey(recoveryKey) {
870
+ try {
871
+ const unformatted = unformatRecoveryKey(recoveryKey);
872
+ if (unformatted.length < 20 || unformatted.length > 30) {
873
+ return false;
874
+ }
875
+ for (const char of unformatted) {
876
+ if (!BASE32_ALPHABET.includes(char)) {
877
+ return false;
878
+ }
879
+ }
880
+ const bytes = decodeBase32(unformatted);
881
+ return bytes.length >= RECOVERY_KEY_SIZE - 2 && bytes.length <= RECOVERY_KEY_SIZE + 2;
882
+ } catch {
883
+ return false;
884
+ }
885
+ }
886
+ function parseRecoveryKey(recoveryKey) {
887
+ if (!validateRecoveryKey(recoveryKey)) {
888
+ throw new Error("Invalid recovery key format");
889
+ }
890
+ const unformatted = unformatRecoveryKey(recoveryKey);
891
+ const bytes = decodeBase32(unformatted);
892
+ const normalizedBytes = new Uint8Array(RECOVERY_KEY_SIZE);
893
+ normalizedBytes.set(bytes.slice(0, RECOVERY_KEY_SIZE));
894
+ const base32 = encodeBase32(normalizedBytes);
895
+ const formatted = formatRecoveryKey(base32);
896
+ const base64 = base64Encode(normalizedBytes);
897
+ return {
898
+ bytes: normalizedBytes,
899
+ formatted,
900
+ base64
901
+ };
902
+ }
903
+ async function deriveWrapKey(recoveryKeyBytes, serverWrapSecret, info = "hearthcoo-wrap-key-v1") {
904
+ if (recoveryKeyBytes.length !== RECOVERY_KEY_SIZE) {
905
+ throw new Error(`Recovery key must be ${RECOVERY_KEY_SIZE} bytes`);
906
+ }
907
+ if (serverWrapSecret.length !== 32) {
908
+ throw new Error("Server wrap secret must be 32 bytes");
909
+ }
910
+ const recoveryKeyMaterial = await crypto.subtle.importKey(
911
+ "raw",
912
+ recoveryKeyBytes,
913
+ "HKDF",
914
+ false,
915
+ ["deriveKey"]
916
+ );
917
+ const wrapKey = await crypto.subtle.deriveKey(
918
+ {
919
+ name: "HKDF",
920
+ hash: "SHA-256",
921
+ salt: serverWrapSecret,
922
+ info: new TextEncoder().encode(info)
923
+ },
924
+ recoveryKeyMaterial,
925
+ {
926
+ name: "AES-GCM",
927
+ length: 256
928
+ },
929
+ false,
930
+ ["encrypt", "decrypt"]
931
+ );
932
+ return wrapKey;
933
+ }
934
+
935
+ // ../types/src/keys.ts
936
+ var DEFAULT_KEY_BUNDLE_ALG = "ECDH-P-521";
937
+
938
+ // ../types/src/options.ts
939
+ var PERSONAL_LEGAL_DOCUMENT_TYPE_OPTIONS = [
940
+ { label: "Birth Certificate", value: "birth_certificate" },
941
+ { label: "Citizenship Certificate", value: "citizenship_certificate" },
942
+ { label: "Divorce Decree", value: "divorce_decree" },
943
+ { label: "Healthcare Directive", value: "healthcare_directive" },
944
+ { label: "Living Will", value: "living_will" },
945
+ { label: "Marriage Certificate", value: "marriage_certificate" },
946
+ { label: "Power of Attorney", value: "power_of_attorney" },
947
+ { label: "Social Security Card", value: "social_security_card" },
948
+ { label: "Will", value: "will" },
949
+ { label: "Property Deed", value: "property_deed" },
950
+ { label: "Vehicle Title", value: "vehicle_title" },
951
+ { label: "Other", value: "other" }
952
+ ];
953
+ var LLC_LEGAL_DOCUMENT_TYPE_OPTIONS = [
954
+ { label: "LLC Formation Certificate", value: "llc_formation" },
955
+ { label: "Operating Agreement", value: "operating_agreement" },
956
+ { label: "Annual Report", value: "annual_report" },
957
+ { label: "Sales Tax Registration", value: "sales_tax_registration" },
958
+ { label: "Business License", value: "business_license" },
959
+ { label: "Registered Agent Certificate", value: "registered_agent" },
960
+ { label: "DBA Certificate", value: "dba_certificate" },
961
+ { label: "Business Insurance Certificate", value: "business_insurance_certificate" },
962
+ { label: "EIN Certificate", value: "ein_certificate" },
963
+ { label: "Property Deed", value: "property_deed" },
964
+ { label: "Vehicle Title", value: "vehicle_title" },
965
+ { label: "Other", value: "other" }
966
+ ];
967
+ var TRUST_LEGAL_DOCUMENT_TYPE_OPTIONS = [
968
+ { label: "Trust Agreement", value: "trust_agreement" },
969
+ { label: "Certificate of Trust", value: "certificate_of_trust" },
970
+ { label: "Trust Amendment", value: "trust_amendment" },
971
+ { label: "Schedule of Assets", value: "schedule_of_assets" },
972
+ { label: "Trustee Acceptance", value: "trustee_acceptance" },
973
+ { label: "Trustee Resignation", value: "trustee_resignation" },
974
+ { label: "Pour-Over Will", value: "pour_over_will" },
975
+ { label: "Trust Restatement", value: "trust_restatement" },
976
+ { label: "Beneficiary Designation", value: "beneficiary_designation" },
977
+ { label: "EIN Certificate", value: "ein_certificate" },
978
+ { label: "Property Deed", value: "property_deed" },
979
+ { label: "Vehicle Title", value: "vehicle_title" },
980
+ { label: "Other", value: "other" }
981
+ ];
982
+ var LEGAL_DOCUMENT_TYPE_OPTIONS = [
983
+ ...PERSONAL_LEGAL_DOCUMENT_TYPE_OPTIONS.filter((o) => o.value !== "other"),
984
+ ...LLC_LEGAL_DOCUMENT_TYPE_OPTIONS.filter((o) => !["property_deed", "vehicle_title", "other", "ein_certificate"].includes(o.value)),
985
+ ...TRUST_LEGAL_DOCUMENT_TYPE_OPTIONS.filter((o) => !["property_deed", "vehicle_title", "other", "ein_certificate"].includes(o.value)),
986
+ { label: "EIN Certificate", value: "ein_certificate" },
987
+ { label: "Other", value: "other" }
988
+ ];
989
+
990
+ // ../types/src/feature-limits.json
991
+ var feature_limits_default = {
992
+ limits: {
993
+ property: { household: 1, estate: 50, requiredPlan: "estate" },
994
+ vehicle: { household: 3, estate: 50, requiredPlan: "household" },
995
+ pet: { household: 10, estate: 50, requiredPlan: "household" },
996
+ contact: { household: 200, estate: 1e3, requiredPlan: "household" },
997
+ resident: { household: 6, estate: 50, requiredPlan: "household" },
998
+ maintenance_task: { household: 100, estate: 1e3, requiredPlan: "household" },
999
+ subscription: { household: 100, estate: 1e3, requiredPlan: "household" },
1000
+ valuable: { household: 20, estate: 2e3, requiredPlan: "household" },
1001
+ legal: { household: 5, estate: 500, requiredPlan: "household" },
1002
+ financial_account: { household: 100, estate: 1e3, requiredPlan: "household" },
1003
+ service: { household: 20, estate: 100, requiredPlan: "household" },
1004
+ insurance: { household: 20, estate: 100, requiredPlan: "household" },
1005
+ device: { household: 50, estate: 1e3, requiredPlan: "household" },
1006
+ identity: { household: 10, estate: 50, requiredPlan: "household" }
1007
+ }
1008
+ };
1009
+
1010
+ // ../types/src/plans.ts
1011
+ var PLAN_FEATURES = {
1012
+ household: [
1013
+ "1 property",
1014
+ "Up to 3 vehicles",
1015
+ "Up to 5 family members",
1016
+ "Store contractors, providers, and emergency contacts",
1017
+ "Track cleaners, gardeners, and other service visits",
1018
+ "Maintenance task wizard for your property and vehicles",
1019
+ "Maintenance reminders",
1020
+ "Emergency procedures",
1021
+ "Subscription cost and renewal tracking",
1022
+ "Link insurance to assets, expiration reminders",
1023
+ "Valuables cataloging",
1024
+ "Encrypted legal documents",
1025
+ "Secure sharing"
1026
+ ],
1027
+ estate: [
1028
+ "Dozens of properties and vehicles",
1029
+ "Dozens of members including family and staff",
1030
+ "Link LLCs and Trusts to your assets",
1031
+ "Continuity Protocol (automatic contingency access)",
1032
+ "Everything in Household"
1033
+ ]
1034
+ };
1035
+ var PLAN_DEFINITIONS = {
1036
+ household_monthly: {
1037
+ planCode: "household_monthly",
1038
+ tier: "household",
1039
+ name: "Household",
1040
+ description: "For managing your home and family with confidence",
1041
+ amountCents: 999,
1042
+ billingCycle: "monthly",
1043
+ features: PLAN_FEATURES.household
1044
+ },
1045
+ household_annual: {
1046
+ planCode: "household_annual",
1047
+ tier: "household",
1048
+ name: "Household",
1049
+ description: "For managing your home and family with confidence",
1050
+ descriptionAnnual: "For managing your home and family with confidence - Save 17%",
1051
+ amountCents: 9900,
1052
+ billingCycle: "annual",
1053
+ features: PLAN_FEATURES.household
1054
+ },
1055
+ estate_monthly: {
1056
+ planCode: "estate_monthly",
1057
+ tier: "estate",
1058
+ name: "Estate",
1059
+ description: "For estates, multiple properties, and long term planning",
1060
+ amountCents: 1999,
1061
+ billingCycle: "monthly",
1062
+ features: PLAN_FEATURES.estate,
1063
+ tagline: "Designed for families who want their home information, digital assets, and legal documents to stay accessible \u2014 even if something happens."
1064
+ },
1065
+ estate_annual: {
1066
+ planCode: "estate_annual",
1067
+ tier: "estate",
1068
+ name: "Estate",
1069
+ description: "For estates, multiple properties, and long term planning",
1070
+ descriptionAnnual: "For estates, multiple properties, and long term planning - Save 17%",
1071
+ amountCents: 19900,
1072
+ billingCycle: "annual",
1073
+ features: PLAN_FEATURES.estate,
1074
+ tagline: "Designed for families who want their home information, digital assets, and legal documents to stay accessible \u2014 even if something happens."
1075
+ }
1076
+ };
1077
+ var FEATURE_LIMITS = feature_limits_default.limits;
1078
+
1079
+ // ../types/src/files.ts
1080
+ var MAX_FILE_SIZE_MB = 200;
1081
+ var MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
1082
+ var MAX_HEARTBEAT_VIDEO_SIZE_MB = 500;
1083
+ var MAX_HEARTBEAT_VIDEO_SIZE_BYTES = MAX_HEARTBEAT_VIDEO_SIZE_MB * 1024 * 1024;
1084
+
1085
+ // ../types/src/navColors.ts
1086
+ var NAV_KEYS = [
1087
+ // Bottom nav (always visible)
1088
+ "/dashboard",
1089
+ "/people",
1090
+ "/password",
1091
+ "/search",
1092
+ "/more",
1093
+ // More drawer - Group 1 (Assets)
1094
+ "/property",
1095
+ "/pet",
1096
+ "/valuables",
1097
+ "/vehicle",
1098
+ // More drawer - Group 2 (Financial & Services)
1099
+ "/subscription",
1100
+ "/financial",
1101
+ "/service",
1102
+ "/contact",
1103
+ "/insurance",
1104
+ // More drawer - Group 3 (Bottom row)
1105
+ "/signout",
1106
+ "/settings",
1107
+ "/continuity",
1108
+ "/share",
1109
+ // Other routes (sidebar, less common)
1110
+ "/access_code",
1111
+ "/credentials",
1112
+ "/device",
1113
+ "/legal",
1114
+ "/taxes"
1115
+ ];
1116
+ var SECONDARY_KEYS = ["records", "maintenance", "improvements"];
1117
+ function cycleColors(colors, secondary) {
1118
+ const result = {};
1119
+ NAV_KEYS.forEach((key, i) => {
1120
+ result[key] = colors[i % colors.length];
1121
+ });
1122
+ const sec = secondary || colors;
1123
+ SECONDARY_KEYS.forEach((key, i) => {
1124
+ result[key] = sec[i % sec.length];
1125
+ });
1126
+ return result;
1127
+ }
1128
+ function stripeColors(stripes) {
1129
+ const result = {};
1130
+ const itemsPerStripe = Math.ceil(NAV_KEYS.length / stripes.length);
1131
+ NAV_KEYS.forEach((key, i) => {
1132
+ const stripeIndex = Math.floor(i / itemsPerStripe);
1133
+ const stripe = stripes[Math.min(stripeIndex, stripes.length - 1)];
1134
+ result[key] = stripe[i % stripe.length];
1135
+ });
1136
+ SECONDARY_KEYS.forEach((key, i) => {
1137
+ result[key] = stripes[i % stripes.length][0];
1138
+ });
1139
+ return result;
1140
+ }
1141
+ var CHRISTMAS_NAV_COLORS = cycleColors([
1142
+ "text-red-600",
1143
+ "text-green-600",
1144
+ "text-red-500",
1145
+ "text-yellow-500",
1146
+ "text-green-500",
1147
+ "text-red-500",
1148
+ "text-green-600"
1149
+ ]);
1150
+ var HALLOWEEN_NAV_COLORS = cycleColors([
1151
+ "text-orange-500",
1152
+ "text-purple-600",
1153
+ "text-orange-600",
1154
+ "text-purple-500",
1155
+ "text-orange-500",
1156
+ "text-purple-600",
1157
+ "text-zinc-800"
1158
+ ]);
1159
+ var VALENTINES_NAV_COLORS = cycleColors([
1160
+ "text-pink-500",
1161
+ "text-red-500",
1162
+ "text-rose-500",
1163
+ "text-pink-600",
1164
+ "text-red-500",
1165
+ "text-rose-500",
1166
+ "text-red-600"
1167
+ ]);
1168
+ var PRIDE_NAV_COLORS = cycleColors([
1169
+ "text-red-500",
1170
+ "text-orange-500",
1171
+ "text-yellow-500",
1172
+ "text-green-500",
1173
+ "text-blue-500",
1174
+ "text-purple-500"
1175
+ ]);
1176
+ var STPATRICKS_NAV_COLORS = cycleColors([
1177
+ "text-green-600",
1178
+ "text-green-500",
1179
+ "text-yellow-500",
1180
+ "text-emerald-500"
1181
+ ]);
1182
+ var CANADADAY_NAV_COLORS = cycleColors(["text-red-600", "text-slate-400", "text-red-500"]);
1183
+ var JULY4TH_NAV_COLORS = cycleColors(["text-red-500", "text-blue-600", "text-slate-400", "text-red-600", "text-blue-500"]);
1184
+ var THANKSGIVING_NAV_COLORS = cycleColors(["text-orange-600", "text-amber-700", "text-yellow-600", "text-orange-500", "text-amber-600"]);
1185
+ var EARTHDAY_NAV_COLORS = cycleColors(["text-green-600", "text-blue-500", "text-emerald-500", "text-sky-500", "text-green-500", "text-blue-600"]);
1186
+ var NEWYEAR_NAV_COLORS = cycleColors(["text-yellow-500", "text-slate-400", "text-yellow-600", "text-slate-500"]);
1187
+ var EASTER_NAV_COLORS = cycleColors(["text-pink-500", "text-purple-500", "text-yellow-500", "text-emerald-500", "text-sky-500"]);
1188
+ var COSTARICA_NAV_COLORS = stripeColors([
1189
+ ["text-blue-600", "text-blue-500"],
1190
+ // Blue stripe
1191
+ ["text-slate-400"],
1192
+ // White stripe
1193
+ ["text-red-600", "text-red-500"],
1194
+ // Red stripe (center)
1195
+ ["text-slate-400"],
1196
+ // White stripe
1197
+ ["text-blue-600", "text-blue-500"]
1198
+ // Blue stripe
1199
+ ]);
1200
+
1201
+ // ../encryption/src/keyBundle.ts
1202
+ async function decryptPrivateKeyWithWrapKey(encryptedPrivateKey, wrapKey) {
1203
+ const packed = base64Decode(encryptedPrivateKey);
1204
+ const version = packed[0];
1205
+ if (version !== 1) {
1206
+ throw new Error(`Unsupported encryption version: ${version}`);
1207
+ }
1208
+ const iv = packed.slice(1, 13);
1209
+ const ciphertext = packed.slice(13);
1210
+ const plaintextBuffer = await crypto.subtle.decrypt(
1211
+ {
1212
+ name: "AES-GCM",
1213
+ iv
1214
+ },
1215
+ wrapKey,
1216
+ ciphertext
1217
+ );
1218
+ return new Uint8Array(plaintextBuffer);
1219
+ }
1220
+ async function importPrivateKey(privateKeyBytes, algorithm = DEFAULT_KEY_BUNDLE_ALG) {
1221
+ const jwkString = new TextDecoder().decode(privateKeyBytes);
1222
+ const jwk = JSON.parse(jwkString);
1223
+ let namedCurve;
1224
+ if (algorithm.includes("P-521")) {
1225
+ namedCurve = "P-521";
1226
+ } else if (algorithm.includes("P-384")) {
1227
+ namedCurve = "P-384";
1228
+ } else if (algorithm.includes("P-256")) {
1229
+ namedCurve = "P-256";
1230
+ } else {
1231
+ throw new Error(`Unsupported algorithm: ${algorithm}`);
1232
+ }
1233
+ return await crypto.subtle.importKey(
1234
+ "jwk",
1235
+ jwk,
1236
+ {
1237
+ name: "ECDH",
1238
+ namedCurve
1239
+ },
1240
+ true,
1241
+ ["deriveKey"]
1242
+ );
1243
+ }
1244
+
1245
+ // ../encryption/src/asymmetricWrap.ts
1246
+ async function unwrapHouseholdKey(wrappedKey, privateKey, algorithm = DEFAULT_KEY_BUNDLE_ALG) {
1247
+ const packed = base64Decode(wrappedKey);
1248
+ const version = packed[0];
1249
+ if (version !== 1) {
1250
+ throw new Error(`Unsupported wrap version: ${version}`);
1251
+ }
1252
+ const namedCurve = algorithm.includes("P-521") ? "P-521" : algorithm.includes("P-384") ? "P-384" : algorithm.includes("P-256") ? "P-256" : (() => {
1253
+ throw new Error(`Unsupported algorithm: ${algorithm}`);
1254
+ })();
1255
+ const publicKeySize = namedCurve === "P-521" ? 133 : namedCurve === "P-384" ? 97 : 65;
1256
+ let offset = 1;
1257
+ const ephemeralPublicKeyBytes = packed.slice(offset, offset + publicKeySize);
1258
+ offset += publicKeySize;
1259
+ const iv = packed.slice(offset, offset + 12);
1260
+ offset += 12;
1261
+ const ciphertext = packed.slice(offset);
1262
+ const ephemeralPublicKey = await crypto.subtle.importKey(
1263
+ "raw",
1264
+ ephemeralPublicKeyBytes,
1265
+ {
1266
+ name: "ECDH",
1267
+ namedCurve
1268
+ },
1269
+ false,
1270
+ []
1271
+ );
1272
+ const sharedSecret = await crypto.subtle.deriveKey(
1273
+ {
1274
+ name: "ECDH",
1275
+ public: ephemeralPublicKey
1276
+ },
1277
+ privateKey,
1278
+ {
1279
+ name: "AES-GCM",
1280
+ length: 256
1281
+ },
1282
+ false,
1283
+ ["decrypt"]
1284
+ );
1285
+ const plaintextBuffer = await crypto.subtle.decrypt(
1286
+ {
1287
+ name: "AES-GCM",
1288
+ iv
1289
+ },
1290
+ sharedSecret,
1291
+ ciphertext
1292
+ );
1293
+ return new Uint8Array(plaintextBuffer);
1294
+ }
1295
+
1296
+ // ../encryption/src/webauthnDeviceBound.ts
1297
+ var RP_ID = typeof window !== "undefined" ? window.location.hostname : "estatehelm.com";
1298
+
1299
+ // src/config.ts
1300
+ var import_env_paths = __toESM(require("env-paths"));
1301
+ var path = __toESM(require("path"));
1302
+ var fs = __toESM(require("fs"));
1303
+ var paths = (0, import_env_paths.default)("estatehelm", { suffix: "" });
1304
+ var DATA_DIR = paths.data;
1305
+ var CACHE_DB_PATH = path.join(DATA_DIR, "cache.db");
1306
+ var CONFIG_PATH = path.join(DATA_DIR, "config.json");
1307
+ var DEVICE_ID_PATH = path.join(DATA_DIR, ".device-id");
1308
+ var KEYTAR_SERVICE = "estatehelm";
1309
+ var KEYTAR_ACCOUNTS = {
1310
+ BEARER_TOKEN: "bearer-token",
1311
+ REFRESH_TOKEN: "refresh-token",
1312
+ DEVICE_CREDENTIALS: "device-credentials"
1313
+ };
1314
+ var API_BASE_URL = process.env.ESTATEHELM_API_URL || "https://api.estatehelm.com";
1315
+ var OAUTH_CONFIG = {
1316
+ // OAuth authorization URL (opens in browser)
1317
+ authUrl: `${API_BASE_URL}/.ory/self-service/login/browser`,
1318
+ // OAuth token exchange endpoint
1319
+ tokenUrl: `${API_BASE_URL}/api/v2/auth/device-token`,
1320
+ // Device code flow endpoint
1321
+ deviceCodeUrl: `${API_BASE_URL}/api/v2/auth/device-code`,
1322
+ // Client ID for CLI app
1323
+ clientId: "estatehelm-cli"
1324
+ };
1325
+ var DEFAULT_CONFIG = {
1326
+ defaultMode: "full"
1327
+ };
1328
+ function ensureDataDir() {
1329
+ if (!fs.existsSync(DATA_DIR)) {
1330
+ fs.mkdirSync(DATA_DIR, { recursive: true });
1331
+ }
1332
+ }
1333
+ function loadConfig() {
1334
+ ensureDataDir();
1335
+ try {
1336
+ if (fs.existsSync(CONFIG_PATH)) {
1337
+ const data = fs.readFileSync(CONFIG_PATH, "utf-8");
1338
+ return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
1339
+ }
1340
+ } catch (err) {
1341
+ console.warn("[Config] Failed to load config:", err);
1342
+ }
1343
+ return DEFAULT_CONFIG;
1344
+ }
1345
+ function saveConfig(config) {
1346
+ ensureDataDir();
1347
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
1348
+ }
1349
+ function getDeviceId() {
1350
+ ensureDataDir();
1351
+ try {
1352
+ if (fs.existsSync(DEVICE_ID_PATH)) {
1353
+ return fs.readFileSync(DEVICE_ID_PATH, "utf-8").trim();
1354
+ }
1355
+ } catch {
1356
+ }
1357
+ const id = `mcp-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
1358
+ fs.writeFileSync(DEVICE_ID_PATH, id);
1359
+ return id;
1360
+ }
1361
+ function getDevicePlatform() {
1362
+ const platform = process.platform;
1363
+ switch (platform) {
1364
+ case "darwin":
1365
+ return "macOS";
1366
+ case "win32":
1367
+ return "Windows";
1368
+ case "linux":
1369
+ return "Linux";
1370
+ default:
1371
+ return platform;
1372
+ }
1373
+ }
1374
+ function getDeviceUserAgent() {
1375
+ return `estatehelm-mcp/1.0 (${getDevicePlatform()})`;
1376
+ }
1377
+ function sanitizeToken(token) {
1378
+ if (token.length <= 8) return "***";
1379
+ return `${token.slice(0, 4)}...${token.slice(-4)}`;
1380
+ }
1381
+
1382
+ // src/keyStore.ts
1383
+ var import_keytar = __toESM(require("keytar"));
1384
+ async function saveBearerToken(token) {
1385
+ await import_keytar.default.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.BEARER_TOKEN, token);
1386
+ console.log(`[KeyStore] Saved bearer token: ${sanitizeToken(token)}`);
1387
+ }
1388
+ async function getBearerToken() {
1389
+ return import_keytar.default.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.BEARER_TOKEN);
1390
+ }
1391
+ async function saveRefreshToken(token) {
1392
+ await import_keytar.default.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.REFRESH_TOKEN, token);
1393
+ console.log(`[KeyStore] Saved refresh token: ${sanitizeToken(token)}`);
1394
+ }
1395
+ async function getRefreshToken() {
1396
+ return import_keytar.default.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.REFRESH_TOKEN);
1397
+ }
1398
+ async function saveDeviceCredentials(credentials) {
1399
+ const json = JSON.stringify(credentials);
1400
+ await import_keytar.default.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.DEVICE_CREDENTIALS, json);
1401
+ console.log(`[KeyStore] Saved device credentials`);
1402
+ }
1403
+ async function getDeviceCredentials() {
1404
+ const json = await import_keytar.default.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.DEVICE_CREDENTIALS);
1405
+ if (!json) return null;
1406
+ try {
1407
+ return JSON.parse(json);
1408
+ } catch {
1409
+ console.warn("[KeyStore] Failed to parse device credentials");
1410
+ return null;
1411
+ }
1412
+ }
1413
+ async function getCredentials() {
1414
+ const [bearerToken, refreshToken, deviceCredentials] = await Promise.all([
1415
+ getBearerToken(),
1416
+ getRefreshToken(),
1417
+ getDeviceCredentials()
1418
+ ]);
1419
+ if (!bearerToken || !deviceCredentials) {
1420
+ return null;
1421
+ }
1422
+ return {
1423
+ bearerToken,
1424
+ refreshToken: refreshToken || "",
1425
+ deviceCredentials
1426
+ };
1427
+ }
1428
+ async function clearCredentials() {
1429
+ await Promise.all([
1430
+ import_keytar.default.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.BEARER_TOKEN),
1431
+ import_keytar.default.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.REFRESH_TOKEN),
1432
+ import_keytar.default.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.DEVICE_CREDENTIALS)
1433
+ ]);
1434
+ console.log("[KeyStore] Cleared all credentials");
1435
+ }
1436
+
1437
+ // src/login.ts
1438
+ function prompt(question) {
1439
+ const rl = readline.createInterface({
1440
+ input: process.stdin,
1441
+ output: process.stdout
1442
+ });
1443
+ return new Promise((resolve) => {
1444
+ rl.question(question, (answer) => {
1445
+ rl.close();
1446
+ resolve(answer);
1447
+ });
1448
+ });
1449
+ }
1450
+ function createApiClient(token) {
1451
+ return new ApiClient({
1452
+ baseUrl: API_BASE_URL,
1453
+ apiVersion: "v2",
1454
+ auth: new TokenAuthAdapter(async () => token)
1455
+ });
1456
+ }
1457
+ async function startDeviceCodeFlow() {
1458
+ const response = await fetch(`${API_BASE_URL}/api/v2/auth/device-code`, {
1459
+ method: "POST",
1460
+ headers: {
1461
+ "Content-Type": "application/json"
1462
+ },
1463
+ body: JSON.stringify({
1464
+ clientId: OAUTH_CONFIG.clientId,
1465
+ scope: "openid profile email offline_access"
1466
+ })
1467
+ });
1468
+ if (!response.ok) {
1469
+ const error = await response.json().catch(() => ({}));
1470
+ throw new Error(error.message || `Device code request failed: ${response.status}`);
1471
+ }
1472
+ return response.json();
1473
+ }
1474
+ async function pollForToken(deviceCode, interval, expiresIn) {
1475
+ const startTime = Date.now();
1476
+ const expireTime = startTime + expiresIn * 1e3;
1477
+ while (Date.now() < expireTime) {
1478
+ await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
1479
+ const response = await fetch(`${API_BASE_URL}/api/v2/auth/device-token`, {
1480
+ method: "POST",
1481
+ headers: {
1482
+ "Content-Type": "application/json"
1483
+ },
1484
+ body: JSON.stringify({
1485
+ clientId: OAUTH_CONFIG.clientId,
1486
+ deviceCode,
1487
+ grantType: "urn:ietf:params:oauth:grant-type:device_code"
1488
+ })
1489
+ });
1490
+ if (response.ok) {
1491
+ return response.json();
1492
+ }
1493
+ const error = await response.json().catch(() => ({}));
1494
+ if (error.error === "authorization_pending") {
1495
+ continue;
1496
+ }
1497
+ if (error.error === "slow_down") {
1498
+ interval += 5;
1499
+ continue;
1500
+ }
1501
+ throw new Error(error.message || `Token request failed: ${response.status}`);
1502
+ }
1503
+ throw new Error("Device code expired");
1504
+ }
1505
+ async function getServerWrapSecret(client) {
1506
+ const response = await client.post("/webauthn/initialize", {});
1507
+ return base64Decode(response.serverWrapSecret);
1508
+ }
1509
+ async function getCurrentKeyBundle(client) {
1510
+ return client.get("/webauthn/key-bundles/current");
1511
+ }
1512
+ async function registerTrustedDevice(client, keyBundleId, privateKeyBytes) {
1513
+ const deviceKey = crypto.getRandomValues(new Uint8Array(32));
1514
+ const deviceKeyMaterial = await crypto.subtle.importKey(
1515
+ "raw",
1516
+ deviceKey,
1517
+ "AES-GCM",
1518
+ false,
1519
+ ["encrypt", "decrypt"]
1520
+ );
1521
+ const iv = crypto.getRandomValues(new Uint8Array(12));
1522
+ const ciphertext = await crypto.subtle.encrypt(
1523
+ { name: "AES-GCM", iv },
1524
+ deviceKeyMaterial,
1525
+ privateKeyBytes
1526
+ );
1527
+ const encryptedPayload = base64Encode(
1528
+ new Uint8Array([
1529
+ ...deviceKey,
1530
+ ...iv,
1531
+ ...new Uint8Array(ciphertext)
1532
+ ])
1533
+ );
1534
+ const deviceId = getDeviceId();
1535
+ const response = await client.post("/webauthn/credentials", {
1536
+ userKeyBundleId: keyBundleId,
1537
+ credentialType: "trusted-device",
1538
+ credentialId: base64Encode(new TextEncoder().encode(deviceId)),
1539
+ encryptedPayload,
1540
+ devicePlatform: getDevicePlatform(),
1541
+ deviceUserAgent: getDeviceUserAgent()
1542
+ });
1543
+ return {
1544
+ credentialId: response.id,
1545
+ encryptedPayload
1546
+ };
1547
+ }
1548
+ async function decryptDeviceCredentials(encryptedPayload) {
1549
+ const packed = base64Decode(encryptedPayload);
1550
+ const deviceKey = packed.slice(0, 32);
1551
+ const iv = packed.slice(32, 44);
1552
+ const ciphertext = packed.slice(44);
1553
+ const deviceKeyMaterial = await crypto.subtle.importKey(
1554
+ "raw",
1555
+ deviceKey,
1556
+ "AES-GCM",
1557
+ false,
1558
+ ["decrypt"]
1559
+ );
1560
+ const plaintext = await crypto.subtle.decrypt(
1561
+ { name: "AES-GCM", iv },
1562
+ deviceKeyMaterial,
1563
+ ciphertext
1564
+ );
1565
+ return new Uint8Array(plaintext);
1566
+ }
1567
+ async function login() {
1568
+ console.log("\nEstateHelm Login");
1569
+ console.log("================\n");
1570
+ console.log("Starting authentication...");
1571
+ const deviceCodeResponse = await startDeviceCodeFlow();
1572
+ console.log(`
1573
+ Please visit: ${deviceCodeResponse.verificationUri}`);
1574
+ console.log(`Enter code: ${deviceCodeResponse.userCode}
1575
+ `);
1576
+ console.log("Opening browser...");
1577
+ await (0, import_open.default)(deviceCodeResponse.verificationUri);
1578
+ console.log("Waiting for authentication...");
1579
+ const tokenResponse = await pollForToken(
1580
+ deviceCodeResponse.deviceCode,
1581
+ deviceCodeResponse.interval,
1582
+ deviceCodeResponse.expiresIn
1583
+ );
1584
+ console.log("Authentication successful!");
1585
+ console.log(`Token: ${sanitizeToken(tokenResponse.accessToken)}`);
1586
+ await saveBearerToken(tokenResponse.accessToken);
1587
+ await saveRefreshToken(tokenResponse.refreshToken);
1588
+ const client = createApiClient(tokenResponse.accessToken);
1589
+ console.log("\nFetching encryption keys...");
1590
+ const serverWrapSecret = await getServerWrapSecret(client);
1591
+ const keyBundle = await getCurrentKeyBundle(client);
1592
+ console.log(`Key bundle: ${keyBundle.id} (${keyBundle.alg})`);
1593
+ console.log("\nYour Recovery Key is required to decrypt your data.");
1594
+ console.log("Format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\n");
1595
+ const recoveryKeyInput = await prompt("Enter your Recovery Key: ");
1596
+ const recoveryKey = parseRecoveryKey(recoveryKeyInput.trim());
1597
+ console.log("Recovery key validated!");
1598
+ const wrapKey = await deriveWrapKey(recoveryKey.bytes, serverWrapSecret);
1599
+ const privateKeyBytes = await decryptPrivateKeyWithWrapKey(
1600
+ keyBundle.encryptedPrivateKey,
1601
+ wrapKey
1602
+ );
1603
+ console.log("Private key decrypted!");
1604
+ console.log("Registering device...");
1605
+ const credentials = await registerTrustedDevice(client, keyBundle.id, privateKeyBytes);
1606
+ await saveDeviceCredentials({
1607
+ credentialId: credentials.credentialId,
1608
+ encryptedPayload: credentials.encryptedPayload,
1609
+ privateKeyBytes: base64Encode(privateKeyBytes)
1610
+ });
1611
+ console.log("\n\u2713 Login complete!");
1612
+ console.log("You can now use: estatehelm mcp");
1613
+ }
1614
+ async function checkLogin() {
1615
+ const credentials = await getCredentials();
1616
+ if (!credentials) {
1617
+ return { loggedIn: false };
1618
+ }
1619
+ try {
1620
+ const client = createApiClient(credentials.bearerToken);
1621
+ const households = await client.getHouseholds();
1622
+ return {
1623
+ loggedIn: true,
1624
+ households: households.map((h) => ({ id: h.id, name: h.name }))
1625
+ };
1626
+ } catch (error) {
1627
+ return { loggedIn: false };
1628
+ }
1629
+ }
1630
+ async function logout() {
1631
+ console.log("Logging out...");
1632
+ const credentials = await getCredentials();
1633
+ if (credentials) {
1634
+ try {
1635
+ const client = createApiClient(credentials.bearerToken);
1636
+ await client.delete(`/webauthn/credentials/${credentials.deviceCredentials.credentialId}`);
1637
+ console.log("Device revoked from server");
1638
+ } catch {
1639
+ }
1640
+ }
1641
+ await clearCredentials();
1642
+ console.log("\u2713 Logged out");
1643
+ }
1644
+ async function getAuthenticatedClient() {
1645
+ const token = await getBearerToken();
1646
+ if (!token) return null;
1647
+ return createApiClient(token);
1648
+ }
1649
+ async function getPrivateKey() {
1650
+ const credentials = await getCredentials();
1651
+ if (!credentials) return null;
1652
+ try {
1653
+ const privateKeyBytes = await decryptDeviceCredentials(
1654
+ credentials.deviceCredentials.encryptedPayload
1655
+ );
1656
+ return importPrivateKey(privateKeyBytes);
1657
+ } catch {
1658
+ return null;
1659
+ }
1660
+ }
1661
+
1662
+ // src/server.ts
1663
+ var import_server = require("@modelcontextprotocol/sdk/server/index.js");
1664
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
1665
+ var import_types5 = require("@modelcontextprotocol/sdk/types.js");
1666
+
1667
+ // src/filter.ts
1668
+ var REDACTION_RULES = {
1669
+ // Password entries
1670
+ password: ["password", "notes"],
1671
+ // Identity documents
1672
+ identity: ["password", "recoveryKey", "securityAnswers"],
1673
+ // Financial accounts
1674
+ bank_account: ["accountNumber", "routingNumber"],
1675
+ investment: ["accountNumber"],
1676
+ // Access codes
1677
+ access_code: ["code", "pin"],
1678
+ // Credentials (show last 4 of document number)
1679
+ credential: ["documentNumber"]
1680
+ };
1681
+ var PARTIAL_REDACTION_FIELDS = ["documentNumber", "accountNumber"];
1682
+ var REDACTED = "[REDACTED]";
1683
+ function redactEntity(entity, entityType, mode) {
1684
+ if (mode === "full") {
1685
+ return entity;
1686
+ }
1687
+ const fieldsToRedact = REDACTION_RULES[entityType] || [];
1688
+ if (fieldsToRedact.length === 0) {
1689
+ return entity;
1690
+ }
1691
+ const redacted = { ...entity };
1692
+ for (const field of fieldsToRedact) {
1693
+ if (field in redacted && redacted[field] != null) {
1694
+ const value = redacted[field];
1695
+ if (PARTIAL_REDACTION_FIELDS.includes(field) && typeof value === "string" && value.length > 4) {
1696
+ redacted[field] = `****${value.slice(-4)}`;
1697
+ } else {
1698
+ redacted[field] = REDACTED;
1699
+ }
1700
+ }
1701
+ }
1702
+ return redacted;
1703
+ }
1704
+
1705
+ // ../cache-sqlite/src/sqliteStore.ts
1706
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"));
1707
+
1708
+ // ../cache/src/schema.ts
1709
+ var DB_VERSION = 3;
1710
+ function makeCacheKey(householdId, entityType) {
1711
+ return `${householdId}:${entityType}`;
1712
+ }
1713
+ function makeMembersCacheKey(householdId) {
1714
+ return `_members_${householdId}`;
1715
+ }
1716
+
1717
+ // ../cache-sqlite/src/sqliteStore.ts
1718
+ var fs2 = __toESM(require("fs"));
1719
+ var path2 = __toESM(require("path"));
1720
+ var SqliteCacheStore = class {
1721
+ db;
1722
+ constructor(dbPath) {
1723
+ const dir = path2.dirname(dbPath);
1724
+ if (!fs2.existsSync(dir)) {
1725
+ fs2.mkdirSync(dir, { recursive: true });
1726
+ }
1727
+ this.db = new import_better_sqlite3.default(dbPath);
1728
+ this.db.pragma("journal_mode = WAL");
1729
+ this.initializeSchema();
1730
+ }
1731
+ initializeSchema() {
1732
+ const versionRow = this.db.prepare(
1733
+ "SELECT value FROM metadata WHERE key = 'schema_version'"
1734
+ ).get();
1735
+ const currentVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
1736
+ if (currentVersion < DB_VERSION) {
1737
+ this.migrate(currentVersion);
1738
+ }
1739
+ }
1740
+ migrate(fromVersion) {
1741
+ console.log(`[SqliteCache] Migrating from version ${fromVersion} to ${DB_VERSION}`);
1742
+ this.db.exec(`
1743
+ -- Metadata table (key-value store)
1744
+ CREATE TABLE IF NOT EXISTS metadata (
1745
+ key TEXT PRIMARY KEY,
1746
+ value TEXT NOT NULL
1747
+ );
1748
+
1749
+ -- Cache metadata (user/household info)
1750
+ CREATE TABLE IF NOT EXISTS cache_metadata (
1751
+ id TEXT PRIMARY KEY DEFAULT 'metadata',
1752
+ household_id TEXT,
1753
+ user_id TEXT,
1754
+ user_identity TEXT, -- JSON
1755
+ last_full_sync TEXT,
1756
+ last_changelog_id INTEGER DEFAULT 0,
1757
+ offline_enabled INTEGER DEFAULT 0,
1758
+ created_at TEXT
1759
+ );
1760
+
1761
+ -- Credentials for offline unlock
1762
+ CREATE TABLE IF NOT EXISTS credentials (
1763
+ user_id TEXT PRIMARY KEY,
1764
+ credential_id TEXT NOT NULL,
1765
+ prf_input TEXT NOT NULL,
1766
+ cached_at TEXT NOT NULL
1767
+ );
1768
+
1769
+ -- Encrypted key cache
1770
+ CREATE TABLE IF NOT EXISTS keys (
1771
+ user_id TEXT PRIMARY KEY,
1772
+ household_id TEXT NOT NULL,
1773
+ iv TEXT NOT NULL,
1774
+ encrypted_data TEXT NOT NULL,
1775
+ cached_at TEXT NOT NULL
1776
+ );
1777
+
1778
+ -- Entity cache
1779
+ CREATE TABLE IF NOT EXISTS entities (
1780
+ cache_key TEXT PRIMARY KEY,
1781
+ household_id TEXT NOT NULL,
1782
+ entity_type TEXT NOT NULL,
1783
+ items TEXT NOT NULL, -- JSON array
1784
+ last_sync TEXT NOT NULL,
1785
+ expected_count INTEGER,
1786
+ changelog_id INTEGER
1787
+ );
1788
+
1789
+ -- Attachment cache
1790
+ CREATE TABLE IF NOT EXISTS attachments (
1791
+ file_id TEXT PRIMARY KEY,
1792
+ encrypted_data BLOB NOT NULL,
1793
+ entity_id TEXT NOT NULL,
1794
+ entity_type TEXT NOT NULL,
1795
+ mime_type TEXT NOT NULL,
1796
+ key_type TEXT NOT NULL,
1797
+ version INTEGER NOT NULL,
1798
+ cached_at TEXT NOT NULL,
1799
+ crypto_version INTEGER,
1800
+ key_derivation_id TEXT
1801
+ );
1802
+
1803
+ CREATE INDEX IF NOT EXISTS idx_attachments_entity_id ON attachments(entity_id);
1804
+ `);
1805
+ this.db.prepare(
1806
+ "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)"
1807
+ ).run(DB_VERSION.toString());
1808
+ console.log("[SqliteCache] Migration complete");
1809
+ }
1810
+ // ============================================================================
1811
+ // Metadata Operations
1812
+ // ============================================================================
1813
+ async getMetadata() {
1814
+ const row = this.db.prepare(
1815
+ "SELECT * FROM cache_metadata WHERE id = ?"
1816
+ ).get("metadata");
1817
+ if (!row) return null;
1818
+ return {
1819
+ householdId: row.household_id,
1820
+ userId: row.user_id,
1821
+ userIdentity: row.user_identity ? JSON.parse(row.user_identity) : void 0,
1822
+ lastFullSync: row.last_full_sync,
1823
+ lastChangelogId: row.last_changelog_id,
1824
+ offlineEnabled: !!row.offline_enabled,
1825
+ createdAt: row.created_at
1826
+ };
1827
+ }
1828
+ async saveMetadata(metadata) {
1829
+ this.db.prepare(`
1830
+ INSERT OR REPLACE INTO cache_metadata
1831
+ (id, household_id, user_id, user_identity, last_full_sync, last_changelog_id, offline_enabled, created_at)
1832
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1833
+ `).run(
1834
+ "metadata",
1835
+ metadata.householdId,
1836
+ metadata.userId,
1837
+ metadata.userIdentity ? JSON.stringify(metadata.userIdentity) : null,
1838
+ metadata.lastFullSync,
1839
+ metadata.lastChangelogId,
1840
+ metadata.offlineEnabled ? 1 : 0,
1841
+ metadata.createdAt
1842
+ );
1843
+ }
1844
+ async getLastChangelogId() {
1845
+ const metadata = await this.getMetadata();
1846
+ return metadata?.lastChangelogId ?? 0;
1847
+ }
1848
+ async updateLastChangelogId(changelogId, householdId, userId) {
1849
+ const existing = await this.getMetadata();
1850
+ if (existing) {
1851
+ this.db.prepare(
1852
+ "UPDATE cache_metadata SET last_changelog_id = ? WHERE id = ?"
1853
+ ).run(changelogId, "metadata");
1854
+ } else if (householdId && userId) {
1855
+ await this.saveMetadata({
1856
+ householdId,
1857
+ userId,
1858
+ lastFullSync: null,
1859
+ lastChangelogId: changelogId,
1860
+ offlineEnabled: false,
1861
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1862
+ });
1863
+ }
1864
+ }
1865
+ // ============================================================================
1866
+ // Credential Operations
1867
+ // ============================================================================
1868
+ async hasOfflineCredential(userId) {
1869
+ const row = this.db.prepare(
1870
+ "SELECT 1 FROM credentials WHERE user_id = ?"
1871
+ ).get(userId);
1872
+ return !!row;
1873
+ }
1874
+ async getOfflineCredential(userId) {
1875
+ const row = this.db.prepare(
1876
+ "SELECT * FROM credentials WHERE user_id = ?"
1877
+ ).get(userId);
1878
+ if (!row) return null;
1879
+ return {
1880
+ userId: row.user_id,
1881
+ credentialId: row.credential_id,
1882
+ prfInput: row.prf_input,
1883
+ cachedAt: row.cached_at
1884
+ };
1885
+ }
1886
+ async saveOfflineCredential(credential) {
1887
+ this.db.prepare(`
1888
+ INSERT OR REPLACE INTO credentials (user_id, credential_id, prf_input, cached_at)
1889
+ VALUES (?, ?, ?, ?)
1890
+ `).run(
1891
+ credential.userId,
1892
+ credential.credentialId,
1893
+ credential.prfInput,
1894
+ credential.cachedAt
1895
+ );
1896
+ console.log("[SqliteCache] Saved offline credential for user:", credential.userId);
1897
+ }
1898
+ async removeOfflineCredential(userId) {
1899
+ this.db.prepare("DELETE FROM credentials WHERE user_id = ?").run(userId);
1900
+ }
1901
+ // ============================================================================
1902
+ // Key Cache Operations
1903
+ // ============================================================================
1904
+ async getKeyCache(userId) {
1905
+ const row = this.db.prepare(
1906
+ "SELECT * FROM keys WHERE user_id = ?"
1907
+ ).get(userId);
1908
+ if (!row) return null;
1909
+ return {
1910
+ userId: row.user_id,
1911
+ householdId: row.household_id,
1912
+ iv: row.iv,
1913
+ encryptedData: row.encrypted_data,
1914
+ cachedAt: row.cached_at
1915
+ };
1916
+ }
1917
+ async saveKeyCache(keyCache) {
1918
+ this.db.prepare(`
1919
+ INSERT OR REPLACE INTO keys (user_id, household_id, iv, encrypted_data, cached_at)
1920
+ VALUES (?, ?, ?, ?, ?)
1921
+ `).run(
1922
+ keyCache.userId,
1923
+ keyCache.householdId,
1924
+ keyCache.iv,
1925
+ keyCache.encryptedData,
1926
+ keyCache.cachedAt
1927
+ );
1928
+ console.log("[SqliteCache] Saved encrypted keys for user:", keyCache.userId);
1929
+ }
1930
+ async removeKeyCache(userId) {
1931
+ this.db.prepare("DELETE FROM keys WHERE user_id = ?").run(userId);
1932
+ }
1933
+ // ============================================================================
1934
+ // Entity Cache Operations
1935
+ // ============================================================================
1936
+ async getEntityCache(householdId, entityType) {
1937
+ const cacheKey = makeCacheKey(householdId, entityType);
1938
+ const row = this.db.prepare(
1939
+ "SELECT * FROM entities WHERE cache_key = ?"
1940
+ ).get(cacheKey);
1941
+ if (!row) return null;
1942
+ const entry = {
1943
+ cacheKey: row.cache_key,
1944
+ householdId: row.household_id,
1945
+ entityType: row.entity_type,
1946
+ items: JSON.parse(row.items),
1947
+ lastSync: row.last_sync,
1948
+ expectedCount: row.expected_count,
1949
+ changelogId: row.changelog_id
1950
+ };
1951
+ if (entry.expectedCount === void 0 || entry.items.length !== entry.expectedCount) {
1952
+ return null;
1953
+ }
1954
+ if (entry.changelogId === void 0) {
1955
+ return null;
1956
+ }
1957
+ return entry;
1958
+ }
1959
+ async getAllEntityCaches(householdId) {
1960
+ const query = householdId ? "SELECT * FROM entities WHERE household_id = ?" : "SELECT * FROM entities";
1961
+ const rows = householdId ? this.db.prepare(query).all(householdId) : this.db.prepare(query).all();
1962
+ return rows.map((row) => ({
1963
+ cacheKey: row.cache_key,
1964
+ householdId: row.household_id,
1965
+ entityType: row.entity_type,
1966
+ items: JSON.parse(row.items),
1967
+ lastSync: row.last_sync,
1968
+ expectedCount: row.expected_count,
1969
+ changelogId: row.changelog_id
1970
+ }));
1971
+ }
1972
+ async saveEntityCache(householdId, entityType, items, changelogId) {
1973
+ const cacheKey = makeCacheKey(householdId, entityType);
1974
+ this.db.prepare(`
1975
+ INSERT OR REPLACE INTO entities
1976
+ (cache_key, household_id, entity_type, items, last_sync, expected_count, changelog_id)
1977
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1978
+ `).run(
1979
+ cacheKey,
1980
+ householdId,
1981
+ entityType,
1982
+ JSON.stringify(items),
1983
+ (/* @__PURE__ */ new Date()).toISOString(),
1984
+ items.length,
1985
+ changelogId
1986
+ );
1987
+ }
1988
+ async updateEntityInCache(householdId, entity) {
1989
+ const { entityType } = entity;
1990
+ const cacheKey = makeCacheKey(householdId, entityType);
1991
+ const existing = await this.getEntityCache(householdId, entityType);
1992
+ const items = existing?.items || [];
1993
+ const existingIndex = items.findIndex((e) => e.id === entity.id);
1994
+ const isUpdate = existingIndex >= 0;
1995
+ if (isUpdate) {
1996
+ items[existingIndex] = entity;
1997
+ } else {
1998
+ items.push(entity);
1999
+ }
2000
+ this.db.prepare(`
2001
+ INSERT OR REPLACE INTO entities
2002
+ (cache_key, household_id, entity_type, items, last_sync, expected_count, changelog_id)
2003
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2004
+ `).run(
2005
+ cacheKey,
2006
+ householdId,
2007
+ entityType,
2008
+ JSON.stringify(items),
2009
+ (/* @__PURE__ */ new Date()).toISOString(),
2010
+ items.length,
2011
+ existing?.changelogId ?? null
2012
+ );
2013
+ return isUpdate;
2014
+ }
2015
+ async removeEntityFromCache(householdId, entityType, entityId) {
2016
+ const cacheKey = makeCacheKey(householdId, entityType);
2017
+ const existing = await this.getEntityCache(householdId, entityType);
2018
+ if (!existing) return;
2019
+ const items = existing.items.filter((e) => e.id !== entityId);
2020
+ this.db.prepare(`
2021
+ UPDATE entities SET items = ?, expected_count = ?, last_sync = ? WHERE cache_key = ?
2022
+ `).run(
2023
+ JSON.stringify(items),
2024
+ items.length,
2025
+ (/* @__PURE__ */ new Date()).toISOString(),
2026
+ cacheKey
2027
+ );
2028
+ }
2029
+ async getEntityVersions(householdId, entityType) {
2030
+ const cache = await this.getEntityCache(householdId, entityType);
2031
+ const versions = /* @__PURE__ */ new Map();
2032
+ if (cache) {
2033
+ for (const entity of cache.items) {
2034
+ versions.set(entity.id, entity.version);
2035
+ }
2036
+ }
2037
+ return versions;
2038
+ }
2039
+ // ============================================================================
2040
+ // Household Members Cache
2041
+ // ============================================================================
2042
+ async getHouseholdMembersCache(householdId) {
2043
+ const cacheKey = makeMembersCacheKey(householdId);
2044
+ const row = this.db.prepare(
2045
+ "SELECT * FROM entities WHERE cache_key = ?"
2046
+ ).get(cacheKey);
2047
+ if (!row) return null;
2048
+ const items = JSON.parse(row.items);
2049
+ if (!items.members) return null;
2050
+ return {
2051
+ householdId: row.household_id,
2052
+ members: items.members,
2053
+ cachedAt: items.cachedAt || row.last_sync
2054
+ };
2055
+ }
2056
+ async saveHouseholdMembersCache(householdId, members) {
2057
+ const cacheKey = makeMembersCacheKey(householdId);
2058
+ const cachedAt = (/* @__PURE__ */ new Date()).toISOString();
2059
+ this.db.prepare(`
2060
+ INSERT OR REPLACE INTO entities
2061
+ (cache_key, household_id, entity_type, items, last_sync, expected_count)
2062
+ VALUES (?, ?, ?, ?, ?, ?)
2063
+ `).run(
2064
+ cacheKey,
2065
+ householdId,
2066
+ "_members",
2067
+ JSON.stringify({ members, cachedAt }),
2068
+ cachedAt,
2069
+ members.length
2070
+ );
2071
+ }
2072
+ // ============================================================================
2073
+ // Attachment Cache Operations
2074
+ // ============================================================================
2075
+ async getAttachmentCache(fileId) {
2076
+ const row = this.db.prepare(
2077
+ "SELECT * FROM attachments WHERE file_id = ?"
2078
+ ).get(fileId);
2079
+ if (!row) return null;
2080
+ return {
2081
+ fileId: row.file_id,
2082
+ encryptedData: row.encrypted_data,
2083
+ entityId: row.entity_id,
2084
+ entityType: row.entity_type,
2085
+ mimeType: row.mime_type,
2086
+ keyType: row.key_type,
2087
+ version: row.version,
2088
+ cachedAt: row.cached_at,
2089
+ cryptoVersion: row.crypto_version,
2090
+ keyDerivationId: row.key_derivation_id
2091
+ };
2092
+ }
2093
+ async saveAttachmentCache(attachment) {
2094
+ let data;
2095
+ if (attachment.encryptedData instanceof Buffer) {
2096
+ data = attachment.encryptedData;
2097
+ } else if (attachment.encryptedData instanceof Blob) {
2098
+ const arrayBuffer = await attachment.encryptedData.arrayBuffer();
2099
+ data = Buffer.from(arrayBuffer);
2100
+ } else {
2101
+ data = Buffer.from(attachment.encryptedData);
2102
+ }
2103
+ this.db.prepare(`
2104
+ INSERT OR REPLACE INTO attachments
2105
+ (file_id, encrypted_data, entity_id, entity_type, mime_type, key_type, version, cached_at, crypto_version, key_derivation_id)
2106
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2107
+ `).run(
2108
+ attachment.fileId,
2109
+ data,
2110
+ attachment.entityId,
2111
+ attachment.entityType,
2112
+ attachment.mimeType,
2113
+ attachment.keyType,
2114
+ attachment.version,
2115
+ attachment.cachedAt,
2116
+ attachment.cryptoVersion ?? null,
2117
+ attachment.keyDerivationId ?? null
2118
+ );
2119
+ console.log("[SqliteCache] Cached attachment:", attachment.fileId);
2120
+ }
2121
+ async removeAttachmentCache(fileId) {
2122
+ this.db.prepare("DELETE FROM attachments WHERE file_id = ?").run(fileId);
2123
+ }
2124
+ async getAttachmentsForEntity(entityId) {
2125
+ const rows = this.db.prepare(
2126
+ "SELECT * FROM attachments WHERE entity_id = ?"
2127
+ ).all(entityId);
2128
+ return rows.map((row) => ({
2129
+ fileId: row.file_id,
2130
+ encryptedData: row.encrypted_data,
2131
+ entityId: row.entity_id,
2132
+ entityType: row.entity_type,
2133
+ mimeType: row.mime_type,
2134
+ keyType: row.key_type,
2135
+ version: row.version,
2136
+ cachedAt: row.cached_at,
2137
+ cryptoVersion: row.crypto_version,
2138
+ keyDerivationId: row.key_derivation_id
2139
+ }));
2140
+ }
2141
+ // ============================================================================
2142
+ // Cache Management
2143
+ // ============================================================================
2144
+ async clearAllCache() {
2145
+ this.db.exec(`
2146
+ DELETE FROM cache_metadata;
2147
+ DELETE FROM credentials;
2148
+ DELETE FROM keys;
2149
+ DELETE FROM entities;
2150
+ DELETE FROM attachments;
2151
+ `);
2152
+ console.log("[SqliteCache] All cache data cleared");
2153
+ }
2154
+ async clearUserCache(userId) {
2155
+ this.db.exec("DELETE FROM cache_metadata");
2156
+ this.db.prepare("DELETE FROM credentials WHERE user_id = ?").run(userId);
2157
+ this.db.prepare("DELETE FROM keys WHERE user_id = ?").run(userId);
2158
+ this.db.exec("DELETE FROM entities");
2159
+ this.db.exec("DELETE FROM attachments");
2160
+ console.log("[SqliteCache] User cache cleared:", userId);
2161
+ }
2162
+ async isOfflineCacheAvailable(userId) {
2163
+ const [metadata, credential, keys] = await Promise.all([
2164
+ this.getMetadata(),
2165
+ this.getOfflineCredential(userId),
2166
+ this.getKeyCache(userId)
2167
+ ]);
2168
+ return !!(metadata && metadata.offlineEnabled && metadata.userId === userId && credential && keys);
2169
+ }
2170
+ async getCacheStats() {
2171
+ const metadata = await this.getMetadata();
2172
+ const entityCaches = await this.getAllEntityCaches();
2173
+ const attachmentCount = this.db.prepare(
2174
+ "SELECT COUNT(*) as count FROM attachments"
2175
+ ).get().count;
2176
+ return {
2177
+ entityTypes: entityCaches.length,
2178
+ totalEntities: entityCaches.reduce((sum, cache) => sum + cache.items.length, 0),
2179
+ attachments: attachmentCount,
2180
+ lastSync: metadata?.lastFullSync || null
2181
+ };
2182
+ }
2183
+ async close() {
2184
+ this.db.close();
2185
+ console.log("[SqliteCache] Database closed");
2186
+ }
2187
+ };
2188
+
2189
+ // src/cache.ts
2190
+ var cacheStore = null;
2191
+ var decryptedCache = /* @__PURE__ */ new Map();
2192
+ var householdKeysCache = /* @__PURE__ */ new Map();
2193
+ var householdsCache = [];
2194
+ async function initCache() {
2195
+ if (!cacheStore) {
2196
+ cacheStore = new SqliteCacheStore(CACHE_DB_PATH);
2197
+ console.error(`[Cache] Initialized at ${CACHE_DB_PATH}`);
2198
+ }
2199
+ }
2200
+ function getCache() {
2201
+ if (!cacheStore) {
2202
+ throw new Error("Cache not initialized. Call initCache() first.");
2203
+ }
2204
+ return cacheStore;
2205
+ }
2206
+ async function closeCache() {
2207
+ if (cacheStore) {
2208
+ await cacheStore.close();
2209
+ cacheStore = null;
2210
+ }
2211
+ decryptedCache.clear();
2212
+ householdKeysCache.clear();
2213
+ }
2214
+ async function clearCache() {
2215
+ const cache = getCache();
2216
+ await cache.clearAllCache();
2217
+ decryptedCache.clear();
2218
+ householdKeysCache.clear();
2219
+ householdsCache = [];
2220
+ console.error("[Cache] Cleared");
2221
+ }
2222
+ async function syncIfNeeded(client, privateKey, force = false) {
2223
+ const cache = getCache();
2224
+ const households = await client.getHouseholds();
2225
+ householdsCache = households.map((h) => ({ id: h.id, name: h.name }));
2226
+ let synced = false;
2227
+ for (const household of households) {
2228
+ await loadHouseholdKeys(client, household.id, privateKey);
2229
+ const localChangelogId = await cache.getLastChangelogId();
2230
+ if (!force && localChangelogId > 0) {
2231
+ try {
2232
+ const response = await client.get(
2233
+ `/households/${household.id}/sync/changes?latestOnly=true`
2234
+ );
2235
+ if (response.latestChangelogId <= localChangelogId) {
2236
+ console.error(`[Cache] Household ${household.id} up to date (changelog ${localChangelogId})`);
2237
+ continue;
2238
+ }
2239
+ console.error(`[Cache] Household ${household.id} has changes (${localChangelogId} -> ${response.latestChangelogId})`);
2240
+ } catch (err) {
2241
+ console.error(`[Cache] Failed to check changelog for ${household.id}:`, err);
2242
+ continue;
2243
+ }
2244
+ }
2245
+ await syncHousehold(client, household.id, cache);
2246
+ synced = true;
2247
+ }
2248
+ return synced;
2249
+ }
2250
+ async function syncHousehold(client, householdId, cache) {
2251
+ const entityTypes = [
2252
+ "pet",
2253
+ "property",
2254
+ "vehicle",
2255
+ "contact",
2256
+ "insurance",
2257
+ "bank_account",
2258
+ "investment",
2259
+ "subscription",
2260
+ "maintenance_task",
2261
+ "password",
2262
+ "access_code",
2263
+ "document",
2264
+ "medical",
2265
+ "prescription",
2266
+ "credential",
2267
+ "utility"
2268
+ ];
2269
+ let latestChangelogId = 0;
2270
+ for (const entityType of entityTypes) {
2271
+ try {
2272
+ const response = await client.getEntities(householdId, { entityType, batched: false });
2273
+ const items = response.items || [];
2274
+ const cachedItems = items.map((item) => ({
2275
+ id: item.id,
2276
+ entityType: item.entityType,
2277
+ encryptedData: item.encryptedData,
2278
+ keyType: item.keyType,
2279
+ householdId: item.householdId,
2280
+ ownerUserId: item.ownerUserId,
2281
+ version: item.version,
2282
+ createdAt: item.createdAt,
2283
+ updatedAt: item.updatedAt,
2284
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString()
2285
+ }));
2286
+ const changelogResponse = await client.get(
2287
+ `/households/${householdId}/sync/changes?latestOnly=true`
2288
+ );
2289
+ latestChangelogId = Math.max(latestChangelogId, changelogResponse.latestChangelogId);
2290
+ await cache.saveEntityCache(householdId, entityType, cachedItems, latestChangelogId);
2291
+ console.error(`[Cache] Synced ${items.length} ${entityType}(s) for household ${householdId}`);
2292
+ } catch (err) {
2293
+ if (err.status !== 404) {
2294
+ console.error(`[Cache] Failed to sync ${entityType} for ${householdId}:`, err.message);
2295
+ }
2296
+ }
2297
+ }
2298
+ await cache.updateLastChangelogId(latestChangelogId, householdId);
2299
+ }
2300
+ async function loadHouseholdKeys(client, householdId, privateKey) {
2301
+ if (householdKeysCache.has(`${householdId}:general`)) {
2302
+ return;
2303
+ }
2304
+ try {
2305
+ const keys = await client.getHouseholdKeys(householdId);
2306
+ for (const key of keys) {
2307
+ const cacheKey = `${householdId}:${key.keyType}`;
2308
+ if (householdKeysCache.has(cacheKey)) {
2309
+ continue;
2310
+ }
2311
+ const householdKeyBytes = await unwrapHouseholdKey(
2312
+ key.encryptedKey,
2313
+ privateKey
2314
+ );
2315
+ householdKeysCache.set(cacheKey, householdKeyBytes);
2316
+ }
2317
+ console.error(`[Cache] Loaded ${keys.length} keys for household ${householdId}`);
2318
+ } catch (err) {
2319
+ console.error(`[Cache] Failed to load keys for household ${householdId}:`, err);
2320
+ throw err;
2321
+ }
2322
+ }
2323
+ function getHouseholdKey(householdId, keyType) {
2324
+ return householdKeysCache.get(`${householdId}:${keyType}`);
2325
+ }
2326
+ async function getDecryptedEntities(householdId, entityType, privateKey) {
2327
+ const cache = getCache();
2328
+ const cacheEntry = await cache.getEntityCache(householdId, entityType);
2329
+ if (!cacheEntry || cacheEntry.items.length === 0) {
2330
+ return [];
2331
+ }
2332
+ const results = [];
2333
+ for (const item of cacheEntry.items) {
2334
+ const decryptCacheKey = `${householdId}:${entityType}:${item.id}`;
2335
+ if (decryptedCache.has(decryptCacheKey)) {
2336
+ results.push(decryptedCache.get(decryptCacheKey));
2337
+ continue;
2338
+ }
2339
+ try {
2340
+ const keyType = getKeyTypeForEntity(entityType);
2341
+ const householdKeyBytes = getHouseholdKey(householdId, keyType);
2342
+ if (!householdKeyBytes) {
2343
+ console.error(`[Cache] No key for ${householdId}:${keyType}`);
2344
+ continue;
2345
+ }
2346
+ const { iv, ciphertext } = unpackEncryptedBlob(item.encryptedData);
2347
+ const encryptedEntity = {
2348
+ entityId: item.id,
2349
+ entityType: item.entityType,
2350
+ keyType: item.keyType,
2351
+ ciphertext: base64Encode(ciphertext),
2352
+ iv: base64Encode(iv),
2353
+ encryptedAt: new Date(item.createdAt),
2354
+ derivedEntityKey: new Uint8Array()
2355
+ };
2356
+ const decrypted = await decryptEntity(
2357
+ householdKeyBytes,
2358
+ encryptedEntity
2359
+ );
2360
+ const entity = {
2361
+ ...decrypted,
2362
+ id: item.id,
2363
+ entityType: item.entityType,
2364
+ householdId: item.householdId,
2365
+ version: item.version,
2366
+ createdAt: item.createdAt,
2367
+ updatedAt: item.updatedAt
2368
+ };
2369
+ decryptedCache.set(decryptCacheKey, entity);
2370
+ results.push(entity);
2371
+ } catch (err) {
2372
+ console.error(`[Cache] Failed to decrypt ${entityType}:${item.id}:`, err);
2373
+ }
2374
+ }
2375
+ return results;
2376
+ }
2377
+ async function getHouseholds() {
2378
+ return householdsCache;
2379
+ }
2380
+ async function getCacheStats() {
2381
+ const cache = getCache();
2382
+ return cache.getCacheStats();
2383
+ }
2384
+
2385
+ // src/server.ts
2386
+ async function startServer(mode) {
2387
+ const config = loadConfig();
2388
+ const privacyMode = mode || config.defaultMode;
2389
+ console.error(`[MCP] Starting server in ${privacyMode} mode`);
2390
+ const client = await getAuthenticatedClient();
2391
+ if (!client) {
2392
+ console.error("[MCP] Not logged in. Run: estatehelm login");
2393
+ process.exit(1);
2394
+ }
2395
+ const privateKey = await getPrivateKey();
2396
+ if (!privateKey) {
2397
+ console.error("[MCP] Failed to load encryption keys. Run: estatehelm login");
2398
+ process.exit(1);
2399
+ }
2400
+ await initCache();
2401
+ console.error("[MCP] Checking for updates...");
2402
+ const synced = await syncIfNeeded(client, privateKey);
2403
+ if (synced) {
2404
+ console.error("[MCP] Cache updated");
2405
+ } else {
2406
+ console.error("[MCP] Cache is up to date");
2407
+ }
2408
+ const server = new import_server.Server(
2409
+ {
2410
+ name: "estatehelm",
2411
+ version: "1.0.0"
2412
+ },
2413
+ {
2414
+ capabilities: {
2415
+ resources: {},
2416
+ tools: {},
2417
+ prompts: {}
2418
+ }
2419
+ }
2420
+ );
2421
+ server.setRequestHandler(import_types5.ListResourcesRequestSchema, async () => {
2422
+ const households = await getHouseholds();
2423
+ const resources = [
2424
+ {
2425
+ uri: "estatehelm://households",
2426
+ name: "All Households",
2427
+ description: "List of all households you have access to",
2428
+ mimeType: "application/json"
2429
+ }
2430
+ ];
2431
+ for (const household of households) {
2432
+ resources.push({
2433
+ uri: `estatehelm://households/${household.id}`,
2434
+ name: household.name,
2435
+ description: `Household: ${household.name}`,
2436
+ mimeType: "application/json"
2437
+ });
2438
+ const entityTypes = [
2439
+ "pet",
2440
+ "property",
2441
+ "vehicle",
2442
+ "contact",
2443
+ "insurance",
2444
+ "bank_account",
2445
+ "investment",
2446
+ "subscription",
2447
+ "maintenance_task",
2448
+ "password",
2449
+ "access_code",
2450
+ "document",
2451
+ "medical",
2452
+ "prescription",
2453
+ "credential",
2454
+ "utility"
2455
+ ];
2456
+ for (const type of entityTypes) {
2457
+ resources.push({
2458
+ uri: `estatehelm://households/${household.id}/${type}`,
2459
+ name: `${household.name} - ${formatEntityType(type)}`,
2460
+ description: `${formatEntityType(type)} in ${household.name}`,
2461
+ mimeType: "application/json"
2462
+ });
2463
+ }
2464
+ }
2465
+ return { resources };
2466
+ });
2467
+ server.setRequestHandler(import_types5.ReadResourceRequestSchema, async (request) => {
2468
+ const uri = request.params.uri;
2469
+ const parsed = parseResourceUri(uri);
2470
+ if (!parsed) {
2471
+ throw new Error(`Invalid resource URI: ${uri}`);
2472
+ }
2473
+ let content;
2474
+ if (parsed.type === "households" && !parsed.householdId) {
2475
+ content = await getHouseholds();
2476
+ } else if (parsed.type === "households" && parsed.householdId && !parsed.entityType) {
2477
+ const households = await getHouseholds();
2478
+ content = households.find((h) => h.id === parsed.householdId);
2479
+ if (!content) {
2480
+ throw new Error(`Household not found: ${parsed.householdId}`);
2481
+ }
2482
+ } else if (parsed.householdId && parsed.entityType) {
2483
+ const entities = await getDecryptedEntities(parsed.householdId, parsed.entityType, privateKey);
2484
+ content = entities.map((e) => redactEntity(e, parsed.entityType, privacyMode));
2485
+ } else {
2486
+ throw new Error(`Unsupported resource: ${uri}`);
2487
+ }
2488
+ return {
2489
+ contents: [
2490
+ {
2491
+ uri,
2492
+ mimeType: "application/json",
2493
+ text: JSON.stringify(content, null, 2)
2494
+ }
2495
+ ]
2496
+ };
2497
+ });
2498
+ server.setRequestHandler(import_types5.ListToolsRequestSchema, async () => {
2499
+ return {
2500
+ tools: [
2501
+ {
2502
+ name: "search_entities",
2503
+ description: "Search across all entities in EstateHelm",
2504
+ inputSchema: {
2505
+ type: "object",
2506
+ properties: {
2507
+ query: {
2508
+ type: "string",
2509
+ description: "Search query"
2510
+ },
2511
+ householdId: {
2512
+ type: "string",
2513
+ description: "Optional: Limit search to a specific household"
2514
+ },
2515
+ entityType: {
2516
+ type: "string",
2517
+ description: "Optional: Limit search to a specific entity type"
2518
+ }
2519
+ },
2520
+ required: ["query"]
2521
+ }
2522
+ },
2523
+ {
2524
+ name: "get_household_summary",
2525
+ description: "Get a summary of a household including counts and key dates",
2526
+ inputSchema: {
2527
+ type: "object",
2528
+ properties: {
2529
+ householdId: {
2530
+ type: "string",
2531
+ description: "The household ID"
2532
+ }
2533
+ },
2534
+ required: ["householdId"]
2535
+ }
2536
+ },
2537
+ {
2538
+ name: "get_expiring_items",
2539
+ description: "Get items expiring within a given number of days",
2540
+ inputSchema: {
2541
+ type: "object",
2542
+ properties: {
2543
+ days: {
2544
+ type: "number",
2545
+ description: "Number of days to look ahead (default: 30)"
2546
+ },
2547
+ householdId: {
2548
+ type: "string",
2549
+ description: "Optional: Limit to a specific household"
2550
+ }
2551
+ }
2552
+ }
2553
+ },
2554
+ {
2555
+ name: "refresh",
2556
+ description: "Force refresh of cached data from the server",
2557
+ inputSchema: {
2558
+ type: "object",
2559
+ properties: {}
2560
+ }
2561
+ }
2562
+ ]
2563
+ };
2564
+ });
2565
+ server.setRequestHandler(import_types5.CallToolRequestSchema, async (request) => {
2566
+ const { name, arguments: args } = request.params;
2567
+ switch (name) {
2568
+ case "search_entities": {
2569
+ const { query, householdId, entityType } = args;
2570
+ const households = await getHouseholds();
2571
+ const searchHouseholds = householdId ? households.filter((h) => h.id === householdId) : households;
2572
+ const results = [];
2573
+ for (const household of searchHouseholds) {
2574
+ const entityTypes = entityType ? [entityType] : [
2575
+ "pet",
2576
+ "property",
2577
+ "vehicle",
2578
+ "contact",
2579
+ "insurance",
2580
+ "bank_account",
2581
+ "investment",
2582
+ "subscription",
2583
+ "maintenance_task",
2584
+ "password",
2585
+ "access_code"
2586
+ ];
2587
+ for (const type of entityTypes) {
2588
+ const entities = await getDecryptedEntities(household.id, type, privateKey);
2589
+ const matches = entities.filter((e) => searchEntity(e, query));
2590
+ for (const match of matches) {
2591
+ results.push({
2592
+ householdId: household.id,
2593
+ householdName: household.name,
2594
+ entityType: type,
2595
+ entity: redactEntity(match, type, privacyMode)
2596
+ });
2597
+ }
2598
+ }
2599
+ }
2600
+ return {
2601
+ content: [
2602
+ {
2603
+ type: "text",
2604
+ text: JSON.stringify(results, null, 2)
2605
+ }
2606
+ ]
2607
+ };
2608
+ }
2609
+ case "get_household_summary": {
2610
+ const { householdId } = args;
2611
+ const households = await getHouseholds();
2612
+ const household = households.find((h) => h.id === householdId);
2613
+ if (!household) {
2614
+ throw new Error(`Household not found: ${householdId}`);
2615
+ }
2616
+ const entityTypes = [
2617
+ "pet",
2618
+ "property",
2619
+ "vehicle",
2620
+ "contact",
2621
+ "insurance",
2622
+ "bank_account",
2623
+ "investment",
2624
+ "subscription",
2625
+ "maintenance_task",
2626
+ "password",
2627
+ "access_code"
2628
+ ];
2629
+ const counts = {};
2630
+ for (const type of entityTypes) {
2631
+ const entities = await getDecryptedEntities(householdId, type, privateKey);
2632
+ counts[type] = entities.length;
2633
+ }
2634
+ const summary = {
2635
+ household: {
2636
+ id: household.id,
2637
+ name: household.name
2638
+ },
2639
+ counts,
2640
+ totalEntities: Object.values(counts).reduce((a, b) => a + b, 0)
2641
+ };
2642
+ return {
2643
+ content: [
2644
+ {
2645
+ type: "text",
2646
+ text: JSON.stringify(summary, null, 2)
2647
+ }
2648
+ ]
2649
+ };
2650
+ }
2651
+ case "get_expiring_items": {
2652
+ const { days = 30, householdId } = args;
2653
+ const households = await getHouseholds();
2654
+ const searchHouseholds = householdId ? households.filter((h) => h.id === householdId) : households;
2655
+ const now = /* @__PURE__ */ new Date();
2656
+ const cutoff = new Date(now.getTime() + days * 24 * 60 * 60 * 1e3);
2657
+ const expiring = [];
2658
+ for (const household of searchHouseholds) {
2659
+ const insurance = await getDecryptedEntities(household.id, "insurance", privateKey);
2660
+ for (const policy of insurance) {
2661
+ if (policy.expirationDate) {
2662
+ const expires = new Date(policy.expirationDate);
2663
+ if (expires <= cutoff) {
2664
+ expiring.push({
2665
+ householdId: household.id,
2666
+ householdName: household.name,
2667
+ type: "insurance",
2668
+ name: policy.name || policy.policyNumber,
2669
+ expiresAt: policy.expirationDate,
2670
+ daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
2671
+ });
2672
+ }
2673
+ }
2674
+ }
2675
+ const vehicles = await getDecryptedEntities(household.id, "vehicle", privateKey);
2676
+ for (const vehicle of vehicles) {
2677
+ if (vehicle.registrationExpiration) {
2678
+ const expires = new Date(vehicle.registrationExpiration);
2679
+ if (expires <= cutoff) {
2680
+ expiring.push({
2681
+ householdId: household.id,
2682
+ householdName: household.name,
2683
+ type: "vehicle_registration",
2684
+ name: `${vehicle.year || ""} ${vehicle.make || ""} ${vehicle.model || ""}`.trim(),
2685
+ expiresAt: vehicle.registrationExpiration,
2686
+ daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
2687
+ });
2688
+ }
2689
+ }
2690
+ }
2691
+ const subscriptions = await getDecryptedEntities(household.id, "subscription", privateKey);
2692
+ for (const sub of subscriptions) {
2693
+ if (sub.renewalDate) {
2694
+ const renews = new Date(sub.renewalDate);
2695
+ if (renews <= cutoff) {
2696
+ expiring.push({
2697
+ householdId: household.id,
2698
+ householdName: household.name,
2699
+ type: "subscription",
2700
+ name: sub.name || sub.serviceName,
2701
+ expiresAt: sub.renewalDate,
2702
+ daysUntil: Math.ceil((renews.getTime() - now.getTime()) / (24 * 60 * 60 * 1e3))
2703
+ });
2704
+ }
2705
+ }
2706
+ }
2707
+ }
2708
+ expiring.sort((a, b) => a.daysUntil - b.daysUntil);
2709
+ return {
2710
+ content: [
2711
+ {
2712
+ type: "text",
2713
+ text: JSON.stringify(expiring, null, 2)
2714
+ }
2715
+ ]
2716
+ };
2717
+ }
2718
+ case "refresh": {
2719
+ const synced2 = await syncIfNeeded(client, privateKey, true);
2720
+ return {
2721
+ content: [
2722
+ {
2723
+ type: "text",
2724
+ text: synced2 ? "Cache refreshed with latest data" : "Cache was already up to date"
2725
+ }
2726
+ ]
2727
+ };
2728
+ }
2729
+ default:
2730
+ throw new Error(`Unknown tool: ${name}`);
2731
+ }
2732
+ });
2733
+ server.setRequestHandler(import_types5.ListPromptsRequestSchema, async () => {
2734
+ return {
2735
+ prompts: [
2736
+ {
2737
+ name: "household_summary",
2738
+ description: "Get an overview of a household",
2739
+ arguments: [
2740
+ {
2741
+ name: "householdId",
2742
+ description: "Optional household ID (uses first household if not specified)",
2743
+ required: false
2744
+ }
2745
+ ]
2746
+ },
2747
+ {
2748
+ name: "expiring_soon",
2749
+ description: "Show items expiring soon",
2750
+ arguments: [
2751
+ {
2752
+ name: "days",
2753
+ description: "Number of days to look ahead (default: 30)",
2754
+ required: false
2755
+ }
2756
+ ]
2757
+ },
2758
+ {
2759
+ name: "emergency_contacts",
2760
+ description: "Show emergency contacts",
2761
+ arguments: []
2762
+ }
2763
+ ]
2764
+ };
2765
+ });
2766
+ server.setRequestHandler(import_types5.GetPromptRequestSchema, async (request) => {
2767
+ const { name, arguments: args } = request.params;
2768
+ switch (name) {
2769
+ case "household_summary": {
2770
+ const householdId = args?.householdId;
2771
+ const households = await getHouseholds();
2772
+ const household = householdId ? households.find((h) => h.id === householdId) : households[0];
2773
+ if (!household) {
2774
+ throw new Error("No household found");
2775
+ }
2776
+ return {
2777
+ messages: [
2778
+ {
2779
+ role: "user",
2780
+ content: {
2781
+ type: "text",
2782
+ text: `Please give me an overview of my household "${household.name}". Include counts of different items, any upcoming expirations, and notable information.`
2783
+ }
2784
+ }
2785
+ ]
2786
+ };
2787
+ }
2788
+ case "expiring_soon": {
2789
+ const days = args?.days || 30;
2790
+ return {
2791
+ messages: [
2792
+ {
2793
+ role: "user",
2794
+ content: {
2795
+ type: "text",
2796
+ text: `What items are expiring in the next ${days} days? Include insurance policies, vehicle registrations, subscriptions, and any other items with expiration dates.`
2797
+ }
2798
+ }
2799
+ ]
2800
+ };
2801
+ }
2802
+ case "emergency_contacts": {
2803
+ return {
2804
+ messages: [
2805
+ {
2806
+ role: "user",
2807
+ content: {
2808
+ type: "text",
2809
+ text: "Show me all emergency contacts across my households. Include their names, phone numbers, and relationship to the household."
2810
+ }
2811
+ }
2812
+ ]
2813
+ };
2814
+ }
2815
+ default:
2816
+ throw new Error(`Unknown prompt: ${name}`);
2817
+ }
2818
+ });
2819
+ const transport = new import_stdio.StdioServerTransport();
2820
+ await server.connect(transport);
2821
+ console.error("[MCP] Server started");
2822
+ }
2823
+ function parseResourceUri(uri) {
2824
+ const match = uri.match(/^estatehelm:\/\/([^/]+)(?:\/([^/]+))?(?:\/([^/]+))?(?:\/([^/]+))?$/);
2825
+ if (!match) return null;
2826
+ const [, type, householdId, entityType, entityId] = match;
2827
+ return { type, householdId, entityType, entityId };
2828
+ }
2829
+ function formatEntityType(type) {
2830
+ return type.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
2831
+ }
2832
+ function searchEntity(entity, query) {
2833
+ const lowerQuery = query.toLowerCase();
2834
+ const searchFields = [
2835
+ "name",
2836
+ "title",
2837
+ "description",
2838
+ "notes",
2839
+ "make",
2840
+ "model",
2841
+ "policyNumber",
2842
+ "serviceName",
2843
+ "username",
2844
+ "email"
2845
+ ];
2846
+ for (const field of searchFields) {
2847
+ if (entity[field] && String(entity[field]).toLowerCase().includes(lowerQuery)) {
2848
+ return true;
2849
+ }
2850
+ }
2851
+ return false;
2852
+ }
2853
+
2854
+ // src/index.ts
2855
+ var program = new import_commander.Command();
2856
+ program.name("estatehelm").description("EstateHelm CLI - MCP server for AI assistants").version("1.0.0");
2857
+ program.command("login").description("Authenticate with EstateHelm").action(async () => {
2858
+ try {
2859
+ await login();
2860
+ } catch (err) {
2861
+ console.error("Login failed:", err.message);
2862
+ process.exit(1);
2863
+ }
2864
+ });
2865
+ program.command("logout").description("Remove credentials and revoke device").action(async () => {
2866
+ try {
2867
+ await initCache();
2868
+ await clearCache();
2869
+ await closeCache();
2870
+ await logout();
2871
+ } catch (err) {
2872
+ console.error("Logout failed:", err.message);
2873
+ process.exit(1);
2874
+ }
2875
+ });
2876
+ program.command("mcp").description("Start the MCP server").option("-m, --mode <mode>", "Privacy mode: full or safe", "full").option("--poll <interval>", "Background sync interval (e.g., 15m)").action(async (options) => {
2877
+ try {
2878
+ const mode = options.mode;
2879
+ if (mode !== "full" && mode !== "safe") {
2880
+ console.error('Invalid mode. Use "full" or "safe".');
2881
+ process.exit(1);
2882
+ }
2883
+ await startServer(mode);
2884
+ } catch (err) {
2885
+ console.error("MCP server failed:", err.message);
2886
+ process.exit(1);
2887
+ }
2888
+ });
2889
+ program.command("status").description("Show current login status and cache info").action(async () => {
2890
+ try {
2891
+ const status = await checkLogin();
2892
+ if (!status.loggedIn) {
2893
+ console.log("Not logged in.");
2894
+ console.log("Run: estatehelm login");
2895
+ return;
2896
+ }
2897
+ console.log("\u2713 Logged in");
2898
+ if (status.households && status.households.length > 0) {
2899
+ console.log(`\u2713 Households: ${status.households.map((h) => h.name).join(", ")}`);
2900
+ }
2901
+ try {
2902
+ await initCache();
2903
+ const stats = await getCacheStats();
2904
+ console.log(`\u2713 Cache: ${stats.totalEntities} entities, ${stats.entityTypes} types`);
2905
+ if (stats.lastSync) {
2906
+ console.log(`\u2713 Last sync: ${stats.lastSync}`);
2907
+ }
2908
+ await closeCache();
2909
+ } catch {
2910
+ console.log("\u2713 Cache: Not initialized");
2911
+ }
2912
+ const config = loadConfig();
2913
+ console.log(`\u2713 Default mode: ${config.defaultMode}`);
2914
+ } catch (err) {
2915
+ console.error("Status check failed:", err.message);
2916
+ process.exit(1);
2917
+ }
2918
+ });
2919
+ program.command("sync").description("Force sync cache from server").action(async () => {
2920
+ try {
2921
+ console.log("Syncing...");
2922
+ const client = await getAuthenticatedClient();
2923
+ if (!client) {
2924
+ console.error("Not logged in. Run: estatehelm login");
2925
+ process.exit(1);
2926
+ }
2927
+ const privateKey = await getPrivateKey();
2928
+ if (!privateKey) {
2929
+ console.error("Failed to load encryption keys. Run: estatehelm login");
2930
+ process.exit(1);
2931
+ }
2932
+ await initCache();
2933
+ await syncIfNeeded(client, privateKey, true);
2934
+ const stats = await getCacheStats();
2935
+ console.log(`\u2713 Synced: ${stats.totalEntities} entities, ${stats.entityTypes} types`);
2936
+ await closeCache();
2937
+ } catch (err) {
2938
+ console.error("Sync failed:", err.message);
2939
+ process.exit(1);
2940
+ }
2941
+ });
2942
+ var cacheCommand = program.command("cache").description("Cache management commands");
2943
+ cacheCommand.command("clear").description("Clear local cache").action(async () => {
2944
+ try {
2945
+ await initCache();
2946
+ await clearCache();
2947
+ await closeCache();
2948
+ console.log("\u2713 Cache cleared");
2949
+ } catch (err) {
2950
+ console.error("Failed to clear cache:", err.message);
2951
+ process.exit(1);
2952
+ }
2953
+ });
2954
+ cacheCommand.command("stats").description("Show cache statistics").action(async () => {
2955
+ try {
2956
+ await initCache();
2957
+ const stats = await getCacheStats();
2958
+ console.log("Cache Statistics:");
2959
+ console.log(` Entity types: ${stats.entityTypes}`);
2960
+ console.log(` Total entities: ${stats.totalEntities}`);
2961
+ console.log(` Attachments: ${stats.attachments}`);
2962
+ console.log(` Last sync: ${stats.lastSync || "Never"}`);
2963
+ await closeCache();
2964
+ } catch (err) {
2965
+ console.error("Failed to get cache stats:", err.message);
2966
+ process.exit(1);
2967
+ }
2968
+ });
2969
+ var configCommand = program.command("config").description("Configuration management");
2970
+ configCommand.command("set <key> <value>").description("Set a configuration value").action((key, value) => {
2971
+ try {
2972
+ const config = loadConfig();
2973
+ if (key === "defaultMode") {
2974
+ if (value !== "full" && value !== "safe") {
2975
+ console.error('Invalid mode. Use "full" or "safe".');
2976
+ process.exit(1);
2977
+ }
2978
+ config.defaultMode = value;
2979
+ } else {
2980
+ console.error(`Unknown config key: ${key}`);
2981
+ process.exit(1);
2982
+ }
2983
+ saveConfig(config);
2984
+ console.log(`\u2713 Set ${key} = ${value}`);
2985
+ } catch (err) {
2986
+ console.error("Failed to set config:", err.message);
2987
+ process.exit(1);
2988
+ }
2989
+ });
2990
+ configCommand.command("get [key]").description("Get configuration value(s)").action((key) => {
2991
+ try {
2992
+ const config = loadConfig();
2993
+ if (key) {
2994
+ const value = config[key];
2995
+ if (value === void 0) {
2996
+ console.error(`Unknown config key: ${key}`);
2997
+ process.exit(1);
2998
+ }
2999
+ console.log(value);
3000
+ } else {
3001
+ console.log("Configuration:");
3002
+ for (const [k, v] of Object.entries(config)) {
3003
+ console.log(` ${k}: ${v}`);
3004
+ }
3005
+ }
3006
+ } catch (err) {
3007
+ console.error("Failed to get config:", err.message);
3008
+ process.exit(1);
3009
+ }
3010
+ });
3011
+ program.parse();
3012
+ //# sourceMappingURL=index.js.map