estatehelm 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/login.ts","../../api-client/src/auth.ts","../../api-client/src/types.ts","../../api-client/src/client.ts","../../encryption/src/utils.ts","../../encryption/src/householdKeys.ts","../../encryption/src/entityKeys.ts","../../encryption/src/entityEncryption.ts","../../encryption/src/entityKeyMapping.ts","../../encryption/src/recoveryKey.ts","../../types/src/keys.ts","../../types/src/options.ts","../../types/src/feature-limits.json","../../types/src/plans.ts","../../types/src/files.ts","../../types/src/navColors.ts","../../encryption/src/keyBundle.ts","../../encryption/src/asymmetricWrap.ts","../../encryption/src/webauthnDeviceBound.ts","../src/config.ts","../src/keyStore.ts","../src/server.ts","../src/filter.ts","../../cache-sqlite/src/sqliteStore.ts","../../cache/src/schema.ts","../src/cache.ts"],"sourcesContent":["#!/usr/bin/env node\r\n/**\r\n * EstateHelm CLI\r\n *\r\n * Command-line interface for EstateHelm MCP server and utilities.\r\n *\r\n * Commands:\r\n * login - Authenticate with EstateHelm\r\n * logout - Remove credentials and revoke device\r\n * mcp - Start the MCP server\r\n * status - Show current login status\r\n * sync - Force sync cache from server\r\n * cache - Cache management commands\r\n *\r\n * @module estatehelm\r\n */\r\n\r\nimport { Command } from 'commander'\r\nimport { login, logout, checkLogin } from './login.js'\r\nimport { startServer } from './server.js'\r\nimport {\r\n initCache,\r\n clearCache,\r\n getCacheStats,\r\n syncIfNeeded,\r\n closeCache,\r\n} from './cache.js'\r\nimport { loadConfig, saveConfig, PrivacyMode } from './config.js'\r\nimport { getAuthenticatedClient, getPrivateKey } from './login.js'\r\n\r\nconst program = new Command()\r\n\r\nprogram\r\n .name('estatehelm')\r\n .description('EstateHelm CLI - MCP server for AI assistants')\r\n .version('1.0.0')\r\n\r\n// Login command\r\nprogram\r\n .command('login')\r\n .description('Authenticate with EstateHelm')\r\n .action(async () => {\r\n try {\r\n await login()\r\n } catch (err: any) {\r\n console.error('Login failed:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Logout command\r\nprogram\r\n .command('logout')\r\n .description('Remove credentials and revoke device')\r\n .action(async () => {\r\n try {\r\n await initCache()\r\n await clearCache()\r\n await closeCache()\r\n await logout()\r\n } catch (err: any) {\r\n console.error('Logout failed:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// MCP server command\r\nprogram\r\n .command('mcp')\r\n .description('Start the MCP server')\r\n .option('-m, --mode <mode>', 'Privacy mode: full or safe', 'full')\r\n .option('--poll <interval>', 'Background sync interval (e.g., 15m)')\r\n .action(async (options) => {\r\n try {\r\n const mode = options.mode as PrivacyMode\r\n if (mode !== 'full' && mode !== 'safe') {\r\n console.error('Invalid mode. Use \"full\" or \"safe\".')\r\n process.exit(1)\r\n }\r\n\r\n await startServer(mode)\r\n } catch (err: any) {\r\n console.error('MCP server failed:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Status command\r\nprogram\r\n .command('status')\r\n .description('Show current login status and cache info')\r\n .action(async () => {\r\n try {\r\n const status = await checkLogin()\r\n\r\n if (!status.loggedIn) {\r\n console.log('Not logged in.')\r\n console.log('Run: estatehelm login')\r\n return\r\n }\r\n\r\n console.log('✓ Logged in')\r\n\r\n if (status.households && status.households.length > 0) {\r\n console.log(`✓ Households: ${status.households.map((h) => h.name).join(', ')}`)\r\n }\r\n\r\n // Show cache stats\r\n try {\r\n await initCache()\r\n const stats = await getCacheStats()\r\n console.log(`✓ Cache: ${stats.totalEntities} entities, ${stats.entityTypes} types`)\r\n if (stats.lastSync) {\r\n console.log(`✓ Last sync: ${stats.lastSync}`)\r\n }\r\n await closeCache()\r\n } catch {\r\n console.log('✓ Cache: Not initialized')\r\n }\r\n\r\n // Show config\r\n const config = loadConfig()\r\n console.log(`✓ Default mode: ${config.defaultMode}`)\r\n } catch (err: any) {\r\n console.error('Status check failed:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Sync command\r\nprogram\r\n .command('sync')\r\n .description('Force sync cache from server')\r\n .action(async () => {\r\n try {\r\n console.log('Syncing...')\r\n\r\n const client = await getAuthenticatedClient()\r\n if (!client) {\r\n console.error('Not logged in. Run: estatehelm login')\r\n process.exit(1)\r\n }\r\n\r\n const privateKey = await getPrivateKey()\r\n if (!privateKey) {\r\n console.error('Failed to load encryption keys. Run: estatehelm login')\r\n process.exit(1)\r\n }\r\n\r\n await initCache()\r\n await syncIfNeeded(client, privateKey, true)\r\n\r\n const stats = await getCacheStats()\r\n console.log(`✓ Synced: ${stats.totalEntities} entities, ${stats.entityTypes} types`)\r\n await closeCache()\r\n } catch (err: any) {\r\n console.error('Sync failed:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Cache command\r\nconst cacheCommand = program\r\n .command('cache')\r\n .description('Cache management commands')\r\n\r\ncacheCommand\r\n .command('clear')\r\n .description('Clear local cache')\r\n .action(async () => {\r\n try {\r\n await initCache()\r\n await clearCache()\r\n await closeCache()\r\n console.log('✓ Cache cleared')\r\n } catch (err: any) {\r\n console.error('Failed to clear cache:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\ncacheCommand\r\n .command('stats')\r\n .description('Show cache statistics')\r\n .action(async () => {\r\n try {\r\n await initCache()\r\n const stats = await getCacheStats()\r\n console.log('Cache Statistics:')\r\n console.log(` Entity types: ${stats.entityTypes}`)\r\n console.log(` Total entities: ${stats.totalEntities}`)\r\n console.log(` Attachments: ${stats.attachments}`)\r\n console.log(` Last sync: ${stats.lastSync || 'Never'}`)\r\n await closeCache()\r\n } catch (err: any) {\r\n console.error('Failed to get cache stats:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Config command\r\nconst configCommand = program\r\n .command('config')\r\n .description('Configuration management')\r\n\r\nconfigCommand\r\n .command('set <key> <value>')\r\n .description('Set a configuration value')\r\n .action((key: string, value: string) => {\r\n try {\r\n const config = loadConfig()\r\n\r\n if (key === 'defaultMode') {\r\n if (value !== 'full' && value !== 'safe') {\r\n console.error('Invalid mode. Use \"full\" or \"safe\".')\r\n process.exit(1)\r\n }\r\n config.defaultMode = value as PrivacyMode\r\n } else {\r\n console.error(`Unknown config key: ${key}`)\r\n process.exit(1)\r\n }\r\n\r\n saveConfig(config)\r\n console.log(`✓ Set ${key} = ${value}`)\r\n } catch (err: any) {\r\n console.error('Failed to set config:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\nconfigCommand\r\n .command('get [key]')\r\n .description('Get configuration value(s)')\r\n .action((key?: string) => {\r\n try {\r\n const config = loadConfig()\r\n\r\n if (key) {\r\n const value = (config as any)[key]\r\n if (value === undefined) {\r\n console.error(`Unknown config key: ${key}`)\r\n process.exit(1)\r\n }\r\n console.log(value)\r\n } else {\r\n console.log('Configuration:')\r\n for (const [k, v] of Object.entries(config)) {\r\n console.log(` ${k}: ${v}`)\r\n }\r\n }\r\n } catch (err: any) {\r\n console.error('Failed to get config:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Parse arguments\r\nprogram.parse()\r\n","/**\r\n * Login Flow\r\n *\r\n * Implements OAuth device flow and recovery key authentication.\r\n * Uses existing @hearthcoo/encryption functions for all crypto operations.\r\n *\r\n * @module estatehelm/login\r\n */\r\n\r\nimport * as readline from 'readline'\r\nimport open from 'open'\r\nimport { ApiClient, TokenAuthAdapter } from '@hearthcoo/api-client'\r\n// Use mobile export to avoid frontend-specific dependencies\r\nimport {\r\n parseRecoveryKey,\r\n deriveWrapKey,\r\n decryptPrivateKeyWithWrapKey,\r\n importPrivateKey,\r\n base64Decode,\r\n base64Encode,\r\n} from '@hearthcoo/encryption/mobile'\r\nimport {\r\n API_BASE_URL,\r\n OAUTH_CONFIG,\r\n getDeviceId,\r\n getDevicePlatform,\r\n getDeviceUserAgent,\r\n sanitizeToken,\r\n} from './config.js'\r\nimport {\r\n saveBearerToken,\r\n saveRefreshToken,\r\n saveDeviceCredentials,\r\n getCredentials,\r\n clearCredentials,\r\n getBearerToken,\r\n} from './keyStore.js'\r\n\r\n/**\r\n * Prompt user for input\r\n */\r\nfunction prompt(question: string): Promise<string> {\r\n const rl = readline.createInterface({\r\n input: process.stdin,\r\n output: process.stdout,\r\n })\r\n\r\n return new Promise((resolve) => {\r\n rl.question(question, (answer) => {\r\n rl.close()\r\n resolve(answer)\r\n })\r\n })\r\n}\r\n\r\n/**\r\n * Device code response from server\r\n */\r\ninterface DeviceCodeResponse {\r\n deviceCode: string\r\n userCode: string\r\n verificationUri: string\r\n expiresIn: number\r\n interval: number\r\n}\r\n\r\n/**\r\n * Token response from server\r\n */\r\ninterface TokenResponse {\r\n accessToken: string\r\n refreshToken: string\r\n expiresIn: number\r\n tokenType: string\r\n}\r\n\r\n/**\r\n * WebAuthn initialize response\r\n */\r\ninterface InitializeResponse {\r\n serverWrapSecret: string\r\n}\r\n\r\n/**\r\n * Key bundle from server\r\n */\r\ninterface KeyBundle {\r\n id: string\r\n publicKey: string\r\n encryptedPrivateKey: string\r\n alg: string\r\n}\r\n\r\n/**\r\n * Create an API client with the given token\r\n */\r\nfunction createApiClient(token: string): ApiClient {\r\n return new ApiClient({\r\n baseUrl: API_BASE_URL,\r\n apiVersion: 'v2',\r\n auth: new TokenAuthAdapter(async () => token),\r\n })\r\n}\r\n\r\n/**\r\n * Start device code flow\r\n */\r\nasync function startDeviceCodeFlow(): Promise<DeviceCodeResponse> {\r\n const response = await fetch(`${API_BASE_URL}/api/v2/auth/device-code`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n },\r\n body: JSON.stringify({\r\n clientId: OAUTH_CONFIG.clientId,\r\n scope: 'openid profile email offline_access',\r\n }),\r\n })\r\n\r\n if (!response.ok) {\r\n const error = await response.json().catch(() => ({}))\r\n throw new Error(error.message || `Device code request failed: ${response.status}`)\r\n }\r\n\r\n return response.json()\r\n}\r\n\r\n/**\r\n * Poll for token using device code\r\n */\r\nasync function pollForToken(deviceCode: string, interval: number, expiresIn: number): Promise<TokenResponse> {\r\n const startTime = Date.now()\r\n const expireTime = startTime + expiresIn * 1000\r\n\r\n while (Date.now() < expireTime) {\r\n await new Promise((resolve) => setTimeout(resolve, interval * 1000))\r\n\r\n const response = await fetch(`${API_BASE_URL}/api/v2/auth/device-token`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n },\r\n body: JSON.stringify({\r\n clientId: OAUTH_CONFIG.clientId,\r\n deviceCode,\r\n grantType: 'urn:ietf:params:oauth:grant-type:device_code',\r\n }),\r\n })\r\n\r\n if (response.ok) {\r\n return response.json()\r\n }\r\n\r\n const error = await response.json().catch(() => ({}))\r\n\r\n if (error.error === 'authorization_pending') {\r\n // Keep polling\r\n continue\r\n }\r\n\r\n if (error.error === 'slow_down') {\r\n // Increase interval\r\n interval += 5\r\n continue\r\n }\r\n\r\n throw new Error(error.message || `Token request failed: ${response.status}`)\r\n }\r\n\r\n throw new Error('Device code expired')\r\n}\r\n\r\n/**\r\n * Get server wrap secret for key derivation\r\n */\r\nasync function getServerWrapSecret(client: ApiClient): Promise<Uint8Array> {\r\n const response = await client.post<InitializeResponse>('/webauthn/initialize', {})\r\n return base64Decode(response.serverWrapSecret)\r\n}\r\n\r\n/**\r\n * Get current key bundle\r\n */\r\nasync function getCurrentKeyBundle(client: ApiClient): Promise<KeyBundle> {\r\n return client.get<KeyBundle>('/webauthn/key-bundles/current')\r\n}\r\n\r\n/**\r\n * Register as trusted device\r\n */\r\nasync function registerTrustedDevice(\r\n client: ApiClient,\r\n keyBundleId: string,\r\n privateKeyBytes: Uint8Array\r\n): Promise<{ credentialId: string; encryptedPayload: string }> {\r\n // Generate a new device-specific key to re-encrypt the private key\r\n const deviceKey = crypto.getRandomValues(new Uint8Array(32))\r\n const deviceKeyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n deviceKey,\r\n 'AES-GCM',\r\n false,\r\n ['encrypt', 'decrypt']\r\n )\r\n\r\n // Encrypt private key with device key\r\n const iv = crypto.getRandomValues(new Uint8Array(12))\r\n const ciphertext = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n deviceKeyMaterial,\r\n privateKeyBytes as BufferSource\r\n )\r\n\r\n // Pack: device key + iv + ciphertext\r\n const encryptedPayload = base64Encode(\r\n new Uint8Array([\r\n ...deviceKey,\r\n ...iv,\r\n ...new Uint8Array(ciphertext),\r\n ])\r\n )\r\n\r\n const deviceId = getDeviceId()\r\n\r\n // Register with server\r\n const response = await client.post<{ id: string }>('/webauthn/credentials', {\r\n userKeyBundleId: keyBundleId,\r\n credentialType: 'trusted-device',\r\n credentialId: base64Encode(new TextEncoder().encode(deviceId)),\r\n encryptedPayload,\r\n devicePlatform: getDevicePlatform(),\r\n deviceUserAgent: getDeviceUserAgent(),\r\n })\r\n\r\n return {\r\n credentialId: response.id,\r\n encryptedPayload,\r\n }\r\n}\r\n\r\n/**\r\n * Decrypt device credentials to get private key\r\n */\r\nexport async function decryptDeviceCredentials(\r\n encryptedPayload: string\r\n): Promise<Uint8Array> {\r\n const packed = base64Decode(encryptedPayload)\r\n\r\n // Unpack: device key (32 bytes) + iv (12 bytes) + ciphertext\r\n const deviceKey = packed.slice(0, 32)\r\n const iv = packed.slice(32, 44)\r\n const ciphertext = packed.slice(44)\r\n\r\n // Import device key\r\n const deviceKeyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n deviceKey,\r\n 'AES-GCM',\r\n false,\r\n ['decrypt']\r\n )\r\n\r\n // Decrypt\r\n const plaintext = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv },\r\n deviceKeyMaterial,\r\n ciphertext\r\n )\r\n\r\n return new Uint8Array(plaintext)\r\n}\r\n\r\n/**\r\n * Login with OAuth device flow and recovery key\r\n */\r\nexport async function login(): Promise<void> {\r\n console.log('\\nEstateHelm Login')\r\n console.log('================\\n')\r\n\r\n // Step 1: Start device code flow\r\n console.log('Starting authentication...')\r\n const deviceCodeResponse = await startDeviceCodeFlow()\r\n\r\n console.log(`\\nPlease visit: ${deviceCodeResponse.verificationUri}`)\r\n console.log(`Enter code: ${deviceCodeResponse.userCode}\\n`)\r\n\r\n // Open browser automatically\r\n console.log('Opening browser...')\r\n await open(deviceCodeResponse.verificationUri)\r\n\r\n // Step 2: Poll for token\r\n console.log('Waiting for authentication...')\r\n const tokenResponse = await pollForToken(\r\n deviceCodeResponse.deviceCode,\r\n deviceCodeResponse.interval,\r\n deviceCodeResponse.expiresIn\r\n )\r\n\r\n console.log('Authentication successful!')\r\n console.log(`Token: ${sanitizeToken(tokenResponse.accessToken)}`)\r\n\r\n // Save tokens\r\n await saveBearerToken(tokenResponse.accessToken)\r\n await saveRefreshToken(tokenResponse.refreshToken)\r\n\r\n // Step 3: Get server wrap secret\r\n const client = createApiClient(tokenResponse.accessToken)\r\n console.log('\\nFetching encryption keys...')\r\n const serverWrapSecret = await getServerWrapSecret(client)\r\n\r\n // Step 4: Get current key bundle\r\n const keyBundle = await getCurrentKeyBundle(client)\r\n console.log(`Key bundle: ${keyBundle.id} (${keyBundle.alg})`)\r\n\r\n // Step 5: Prompt for recovery key\r\n console.log('\\nYour Recovery Key is required to decrypt your data.')\r\n console.log('Format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\\n')\r\n const recoveryKeyInput = await prompt('Enter your Recovery Key: ')\r\n\r\n // Parse and validate recovery key\r\n const recoveryKey = parseRecoveryKey(recoveryKeyInput.trim())\r\n console.log('Recovery key validated!')\r\n\r\n // Step 6: Derive wrap key and decrypt private key\r\n const wrapKey = await deriveWrapKey(recoveryKey.bytes, serverWrapSecret)\r\n const privateKeyBytes = await decryptPrivateKeyWithWrapKey(\r\n keyBundle.encryptedPrivateKey,\r\n wrapKey\r\n )\r\n console.log('Private key decrypted!')\r\n\r\n // Step 7: Register as trusted device\r\n console.log('Registering device...')\r\n const credentials = await registerTrustedDevice(client, keyBundle.id, privateKeyBytes)\r\n\r\n // Save device credentials\r\n await saveDeviceCredentials({\r\n credentialId: credentials.credentialId,\r\n encryptedPayload: credentials.encryptedPayload,\r\n privateKeyBytes: base64Encode(privateKeyBytes),\r\n })\r\n\r\n console.log('\\n✓ Login complete!')\r\n console.log('You can now use: estatehelm mcp')\r\n}\r\n\r\n/**\r\n * Check if user is logged in and credentials are valid\r\n */\r\nexport async function checkLogin(): Promise<{\r\n loggedIn: boolean\r\n email?: string\r\n households?: Array<{ id: string; name: string }>\r\n}> {\r\n const credentials = await getCredentials()\r\n if (!credentials) {\r\n return { loggedIn: false }\r\n }\r\n\r\n try {\r\n const client = createApiClient(credentials.bearerToken)\r\n\r\n // Verify token is still valid by fetching user info\r\n const households = await client.getHouseholds()\r\n\r\n return {\r\n loggedIn: true,\r\n households: households.map((h) => ({ id: h.id, name: h.name })),\r\n }\r\n } catch (error) {\r\n // Token might be expired or revoked\r\n return { loggedIn: false }\r\n }\r\n}\r\n\r\n/**\r\n * Logout and clear credentials\r\n */\r\nexport async function logout(): Promise<void> {\r\n console.log('Logging out...')\r\n\r\n const credentials = await getCredentials()\r\n if (credentials) {\r\n try {\r\n // Try to revoke device from server\r\n const client = createApiClient(credentials.bearerToken)\r\n await client.delete(`/webauthn/credentials/${credentials.deviceCredentials.credentialId}`)\r\n console.log('Device revoked from server')\r\n } catch {\r\n // Ignore errors - device might already be revoked\r\n }\r\n }\r\n\r\n await clearCredentials()\r\n console.log('✓ Logged out')\r\n}\r\n\r\n/**\r\n * Get a ready-to-use API client\r\n */\r\nexport async function getAuthenticatedClient(): Promise<ApiClient | null> {\r\n const token = await getBearerToken()\r\n if (!token) return null\r\n return createApiClient(token)\r\n}\r\n\r\n/**\r\n * Get decrypted private key from stored credentials\r\n */\r\nexport async function getPrivateKey(): Promise<CryptoKey | null> {\r\n const credentials = await getCredentials()\r\n if (!credentials) return null\r\n\r\n try {\r\n const privateKeyBytes = await decryptDeviceCredentials(\r\n credentials.deviceCredentials.encryptedPayload\r\n )\r\n return importPrivateKey(privateKeyBytes)\r\n } catch {\r\n return null\r\n }\r\n}\r\n","/**\r\n * Auth Adapter Interface\r\n *\r\n * Platform-specific auth handling is injected via this interface.\r\n * - Web: Uses cookies (credentials: 'include')\r\n * - Mobile: Uses Bearer token from SecureStore\r\n */\r\n\r\nexport interface AuthAdapter {\r\n /**\r\n * Get auth headers to add to requests.\r\n * Returns empty object if no auth available.\r\n */\r\n getAuthHeaders(): Promise<Record<string, string>>\r\n\r\n /**\r\n * Get RequestInit credentials mode.\r\n * - 'include' for cookie-based auth (web)\r\n * - 'same-origin' or 'omit' for token-based (mobile)\r\n */\r\n getCredentials(): RequestCredentials\r\n}\r\n\r\n/**\r\n * Cookie-based auth adapter for web.\r\n * Uses credentials: 'include' to send cookies with requests.\r\n */\r\nexport class CookieAuthAdapter implements AuthAdapter {\r\n async getAuthHeaders(): Promise<Record<string, string>> {\r\n return {}\r\n }\r\n\r\n getCredentials(): RequestCredentials {\r\n return 'include'\r\n }\r\n}\r\n\r\n/**\r\n * Token-based auth adapter for mobile.\r\n * Uses Authorization header with Bearer token.\r\n */\r\nexport class TokenAuthAdapter implements AuthAdapter {\r\n private getToken: () => Promise<string | null>\r\n\r\n constructor(getToken: () => Promise<string | null>) {\r\n this.getToken = getToken\r\n }\r\n\r\n async getAuthHeaders(): Promise<Record<string, string>> {\r\n const token = await this.getToken()\r\n if (token) {\r\n return { Authorization: `Bearer ${token}` }\r\n }\r\n return {}\r\n }\r\n\r\n getCredentials(): RequestCredentials {\r\n return 'same-origin'\r\n }\r\n}\r\n","/**\r\n * @hearthcoo/api-client\r\n *\r\n * Shared TypeScript types for API client.\r\n */\r\n\r\n// ===== REQUEST/RESPONSE TYPES =====\r\n\r\nexport interface RequestOptions {\r\n headers?: Record<string, string>\r\n body?: unknown\r\n}\r\n\r\nexport interface ApiError extends Error {\r\n status?: number\r\n code?: 'FORBIDDEN' | 'VERSION_CONFLICT' | 'NOT_FOUND' | string\r\n error?: unknown\r\n // Version conflict details (when code === 'VERSION_CONFLICT')\r\n currentVersion?: string\r\n expectedVersion?: string\r\n // Set to true when error has been shown to user (prevents duplicate toasts)\r\n handled?: boolean\r\n}\r\n\r\n// ===== HOUSEHOLD TYPES =====\r\n\r\nexport const HOUSEHOLD_MEMBER_ROLES = {\r\n OWNER: 'owner',\r\n MEMBER: 'member',\r\n EXECUTOR: 'executor'\r\n} as const\r\n\r\nexport type HouseholdMemberRole = typeof HOUSEHOLD_MEMBER_ROLES[keyof typeof HOUSEHOLD_MEMBER_ROLES]\r\n\r\nexport function isValidHouseholdRole(role: unknown): role is HouseholdMemberRole {\r\n return Object.values(HOUSEHOLD_MEMBER_ROLES).includes(role as HouseholdMemberRole)\r\n}\r\n\r\nexport interface Household {\r\n id: string\r\n name: string\r\n myRole: string\r\n createdAt: string\r\n subscriptionStatus?: string\r\n planCode?: string\r\n trialEndsAt?: string\r\n entityCount?: number\r\n}\r\n\r\nexport interface ExtendTrialResponse {\r\n trialEndsAt: string\r\n trialDays: number\r\n}\r\n\r\nexport interface HouseholdMember {\r\n id: string\r\n userId: string\r\n email: string\r\n displayName?: string\r\n role: string\r\n joinedAt: string\r\n invitedBy?: string\r\n keyBundleId?: string\r\n keyBundlePublicKey?: string\r\n keyBundleAlg?: string\r\n}\r\n\r\n// ===== USER KEY BUNDLE TYPES =====\r\n\r\nexport interface UserKeyBundle {\r\n id: string\r\n version: number\r\n publicKey: string\r\n encryptedPrivateKey: string\r\n alg: string\r\n hasPRFEnvelope?: boolean\r\n}\r\n\r\n// ===== HOUSEHOLD KEY TYPES =====\r\n\r\nexport interface HouseholdKey {\r\n id?: string\r\n householdId?: string | null\r\n ownerUserId?: string | null\r\n keyType: string\r\n entityId?: string | null\r\n encryptedKey: string\r\n userKeyBundleId: string\r\n keyBundleVersion: number\r\n keyBundlePublicKey: string\r\n keyBundleEncryptedPrivateKey: string\r\n keyBundleEnc: string\r\n keyBundleAlg: string\r\n grantedAt: string\r\n grantedBy: string\r\n}\r\n\r\nexport interface MemberKeyAccessSummary {\r\n userId: string\r\n email: string\r\n displayName?: string\r\n role: string\r\n keyTypes: string[]\r\n}\r\n\r\nexport interface KeyTypeInfo {\r\n keyType: string\r\n isDefault: boolean\r\n memberCount: number\r\n}\r\n\r\n// ===== ENCRYPTED ENTITY TYPES =====\r\n\r\nexport interface EncryptedEntityResponse {\r\n id: string\r\n householdId: string | null\r\n ownerUserId?: string | null\r\n entityType: string\r\n keyType: string\r\n encryptedData: string\r\n cryptoVersion: number\r\n version: number // Optimistic concurrency version (use as If-Match header)\r\n thumbnailData?: string\r\n createdBy: string\r\n createdAt: string\r\n updatedAt: string\r\n deletedAt?: string\r\n}\r\n\r\nexport interface CreateEntityRequest {\r\n id: string\r\n entityType: string\r\n keyType: string\r\n encryptedData: string\r\n cryptoVersion?: number\r\n thumbnailData?: string\r\n}\r\n\r\nexport interface UpdateEntityRequest {\r\n keyType?: string\r\n encryptedData: string\r\n cryptoVersion?: number\r\n thumbnailData?: string\r\n deleteFileIds?: string[] // File IDs to delete atomically with the update\r\n}\r\n\r\nexport interface EntitiesResponse {\r\n total: number\r\n items: EncryptedEntityResponse[]\r\n}\r\n\r\n// ===== CONTACT TYPES =====\r\n\r\nexport interface Contact {\r\n id: string\r\n householdId: string\r\n type: string\r\n companyName?: string\r\n firstName?: string\r\n lastName?: string\r\n email?: string\r\n phonePrimary?: string\r\n phoneSecondary?: string\r\n website?: string\r\n streetAddress?: string\r\n city?: string\r\n state?: string\r\n postalCode?: string\r\n specialty?: string\r\n licenseNumber?: string\r\n rating?: number\r\n notes?: string\r\n isFavorite?: boolean\r\n createdAt: string\r\n updatedAt: string\r\n}\r\n\r\nexport interface CreateContactRequest {\r\n type: string\r\n company_name?: string\r\n first_name?: string\r\n last_name?: string\r\n email?: string\r\n phone_primary?: string\r\n phone_secondary?: string\r\n website?: string\r\n street_address?: string\r\n city?: string\r\n state?: string\r\n postal_code?: string\r\n specialty?: string\r\n license_number?: string\r\n rating?: number\r\n notes?: string\r\n is_favorite?: boolean\r\n}\r\n\r\n// ===== INSURANCE TYPES =====\r\n\r\nexport interface InsurancePolicy {\r\n id: string\r\n householdId: string\r\n provider: string\r\n policyNumber: string\r\n effectiveDate: string\r\n expirationDate: string\r\n type: 'home' | 'auto' | 'umbrella' | 'life' | 'health' | 'pet' | 'collection' | 'other'\r\n propertyId?: string\r\n vehicleId?: string\r\n petId?: string\r\n createdAt: string\r\n updatedAt: string\r\n}\r\n\r\n// ===== KEY BATCH TYPES =====\r\n\r\nexport interface BatchAddKeysRequest {\r\n keyBundleId: string\r\n keys: Array<{\r\n keyType: string\r\n wrappedKey: string\r\n householdId?: string\r\n }>\r\n}\r\n\r\nexport interface BatchAddKeysResponse {\r\n message: string\r\n keys: Array<{\r\n keyType: string\r\n householdId?: string\r\n id: string\r\n created: boolean\r\n }>\r\n}\r\n\r\n// ===== FILE TYPES =====\r\n\r\nexport interface FileMetadata {\r\n id: string\r\n householdId: string\r\n entityId?: string\r\n entityType?: string\r\n fileName: string\r\n fileType?: string\r\n fileSizeBytes: number\r\n storageProvider: string\r\n storagePath: string\r\n keyType: string\r\n cryptoVersion: number\r\n createdBy: string\r\n createdAt: string\r\n deletedAt?: string\r\n thumbnailStoragePath?: string\r\n thumbnailDownloadUrl?: string\r\n downloadUrl?: string\r\n}\r\n\r\nexport interface GenerateUploadUrlRequest {\r\n contentType: string\r\n keyType: string\r\n fileSizeBytes?: number\r\n includeThumbnailUrl?: boolean\r\n}\r\n\r\nexport interface GenerateUploadUrlResponse {\r\n url: string\r\n fileId: string\r\n storagePath: string\r\n expiresAt: string\r\n thumbnailUrl?: string\r\n thumbnailStoragePath?: string\r\n}\r\n\r\nexport interface CreateFileMetadataRequest {\r\n fileId: string\r\n storagePath: string\r\n entityId?: string\r\n entityType?: string\r\n fileName: string\r\n fileType?: string\r\n fileSizeBytes: number\r\n keyType: string\r\n cryptoVersion: number\r\n thumbnailStoragePath?: string\r\n}\r\n\r\n// ===== BILLING TYPES =====\r\n\r\nexport interface CreateCheckoutSessionRequest {\r\n priceId: string\r\n}\r\n\r\nexport interface CheckoutSessionResponse {\r\n sessionId: string\r\n checkoutUrl: string\r\n}\r\n\r\nexport interface CustomerPortalSessionResponse {\r\n portalUrl: string\r\n}\r\n\r\nexport interface AvailablePlan {\r\n priceId: string\r\n planCode: string\r\n billingCycle: 'monthly' | 'annual'\r\n}\r\n\r\nexport interface StartTrialResponse {\r\n planCode: string\r\n trialEndsAt: string\r\n trialDays: number\r\n}\r\n\r\nexport interface UpgradeSubscriptionResponse {\r\n success: boolean\r\n newPriceId: string\r\n subscriptionId: string\r\n}\r\n\r\nexport interface BillingDetailsResponse {\r\n billingCycle: 'monthly' | 'annual' | null\r\n amountCents: number\r\n paymentProvider: 'stripe' | 'apple' | 'google' | null\r\n nextBillingDate?: string\r\n isLifetime: boolean\r\n}\r\n\r\nexport interface PlanLimitExceededError {\r\n error: 'plan_limit_exceeded'\r\n message: string\r\n upgradeRequired: boolean\r\n requiredPlan?: string\r\n}\r\n","/**\r\n * API Client - Platform-Agnostic HTTP Client\r\n *\r\n * Shared between web and mobile apps.\r\n * All API requests go through this client.\r\n */\r\n\r\nimport type { AuthAdapter } from './auth'\r\nimport type {\r\n RequestOptions,\r\n ApiError,\r\n Household,\r\n HouseholdMember,\r\n HouseholdMemberRole,\r\n UserKeyBundle,\r\n HouseholdKey,\r\n MemberKeyAccessSummary,\r\n KeyTypeInfo,\r\n EncryptedEntityResponse,\r\n CreateEntityRequest,\r\n UpdateEntityRequest,\r\n EntitiesResponse,\r\n Contact,\r\n CreateContactRequest,\r\n InsurancePolicy,\r\n BatchAddKeysRequest,\r\n BatchAddKeysResponse,\r\n CheckoutSessionResponse,\r\n CustomerPortalSessionResponse,\r\n AvailablePlan,\r\n StartTrialResponse,\r\n UpgradeSubscriptionResponse,\r\n BillingDetailsResponse,\r\n ExtendTrialResponse,\r\n} from './types'\r\nimport { HOUSEHOLD_MEMBER_ROLES } from './types'\r\n\r\nexport interface ApiClientConfig {\r\n baseUrl: string\r\n apiVersion: string\r\n auth: AuthAdapter\r\n /** Called when a 401 Unauthorized error is received. Use this to trigger re-authentication. */\r\n onAuthError?: () => void\r\n}\r\n\r\n/**\r\n * Batches multiple getEntities calls into a single request.\r\n * Collects requests over one event loop tick, then combines them.\r\n */\r\nclass EntityBatcher {\r\n private pending = new Map<string, { resolve: (items: any[]) => void; reject: (err: any) => void }>()\r\n private householdId: string | null | undefined = null\r\n private includeDeleted = false\r\n private timer: ReturnType<typeof setTimeout> | null = null\r\n private fetchFn: (householdId: string | null | undefined, entityTypes: string[], includeDeleted: boolean) => Promise<any>\r\n\r\n constructor(fetchFn: (householdId: string | null | undefined, entityTypes: string[], includeDeleted: boolean) => Promise<any>) {\r\n this.fetchFn = fetchFn\r\n }\r\n\r\n request(householdId: string | null | undefined, entityType: string, includeDeleted: boolean = false): Promise<any[]> {\r\n // If householdId changes, flush existing batch first\r\n if (this.pending.size > 0 && this.householdId !== householdId) {\r\n this.flush()\r\n }\r\n\r\n this.householdId = householdId\r\n this.includeDeleted = includeDeleted\r\n\r\n return new Promise((resolve, reject) => {\r\n this.pending.set(entityType, { resolve, reject })\r\n\r\n // Schedule flush for next tick if not already scheduled\r\n if (!this.timer) {\r\n this.timer = setTimeout(() => this.flush(), 0)\r\n }\r\n })\r\n }\r\n\r\n private async flush() {\r\n const entityTypes = Array.from(this.pending.keys())\r\n const callbacks = new Map(this.pending)\r\n const householdId = this.householdId\r\n const includeDeleted = this.includeDeleted\r\n\r\n // Reset state\r\n this.pending.clear()\r\n this.timer = null\r\n this.householdId = null\r\n this.includeDeleted = false\r\n\r\n if (entityTypes.length === 0) return\r\n\r\n try {\r\n const response = await this.fetchFn(householdId, entityTypes, includeDeleted)\r\n const items = response.items || []\r\n\r\n // Group items by entityType and distribute to callers\r\n const groupedByType = new Map<string, any[]>()\r\n for (const type of entityTypes) {\r\n groupedByType.set(type, [])\r\n }\r\n\r\n for (const item of items) {\r\n const type = item.entityType\r\n if (groupedByType.has(type)) {\r\n groupedByType.get(type)!.push(item)\r\n }\r\n }\r\n\r\n // Resolve each caller with their specific items\r\n for (const [type, { resolve }] of callbacks) {\r\n resolve(groupedByType.get(type) || [])\r\n }\r\n } catch (err) {\r\n // Reject all pending requests\r\n for (const { reject } of callbacks.values()) {\r\n reject(err)\r\n }\r\n }\r\n }\r\n}\r\n\r\nexport class ApiClient {\r\n private config: ApiClientConfig\r\n // Cache for in-flight GET requests to deduplicate concurrent calls\r\n private inFlightRequests = new Map<string, Promise<any>>()\r\n // Batcher for entity requests\r\n private entityBatcher: EntityBatcher\r\n\r\n constructor(config: ApiClientConfig) {\r\n this.config = config\r\n // Initialize entity batcher with fetch function\r\n this.entityBatcher = new EntityBatcher((householdId, entityTypes, includeDeleted) =>\r\n this.getEntitiesMultiple(householdId, entityTypes, includeDeleted)\r\n )\r\n }\r\n\r\n private getApiUrl(path: string): string {\r\n const cleanPath = path.startsWith('/') ? path : `/${path}`\r\n return `${this.config.baseUrl}/api/${this.config.apiVersion}${cleanPath}`\r\n }\r\n\r\n private async request<T>(\r\n method: string,\r\n path: string,\r\n options?: RequestOptions\r\n ): Promise<T> {\r\n const url = this.getApiUrl(path)\r\n const authHeaders = await this.config.auth.getAuthHeaders()\r\n const headers: Record<string, string> = {\r\n 'Content-Type': 'application/json',\r\n ...authHeaders,\r\n ...options?.headers,\r\n }\r\n\r\n const config: RequestInit = {\r\n method,\r\n headers,\r\n credentials: this.config.auth.getCredentials(),\r\n }\r\n\r\n if (options?.body) {\r\n config.body = JSON.stringify(options.body)\r\n }\r\n\r\n let response: Response\r\n try {\r\n response = await fetch(url, config)\r\n } catch (networkError) {\r\n // Network error (server down, no internet, CORS, etc.)\r\n console.error('Network error:', { url, method, error: networkError })\r\n const error = Object.assign(\r\n new Error('Unable to connect to server. Please check your connection and try again.'),\r\n { status: 0, code: 'NETWORK_ERROR', originalError: networkError }\r\n )\r\n throw error\r\n }\r\n\r\n if (!response.ok) {\r\n const error = await response.json().catch(() => ({\r\n message: 'An error occurred',\r\n }))\r\n\r\n // Use lower log level for client errors (400, 404) as they're often expected\r\n const logLevel = response.status === 400 || response.status === 404 ? console.debug : console.error\r\n logLevel('API Error:', {\r\n url,\r\n method,\r\n status: response.status,\r\n error,\r\n })\r\n\r\n // Handle authentication errors (401 Unauthorized)\r\n if (response.status === 401) {\r\n console.warn('Authentication required')\r\n // Trigger re-authentication callback if configured\r\n this.config.onAuthError?.()\r\n throw Object.assign(new Error('Authentication required'), { status: 401 })\r\n }\r\n\r\n // Handle payment required errors (402 Payment Required - plan limits)\r\n if (response.status === 402) {\r\n throw Object.assign(\r\n new Error(error.message || 'Upgrade required to access this feature'),\r\n { \r\n status: 402, \r\n code: error.error || 'limit_exceeded',\r\n details: error.details,\r\n requiredPlan: error.details?.requiredPlan\r\n }\r\n )\r\n }\r\n\r\n // Handle authorization errors (403 Forbidden)\r\n if (response.status === 403) {\r\n throw Object.assign(\r\n new Error(error.message || 'You do not have permission to perform this action'),\r\n { status: 403, code: 'FORBIDDEN' }\r\n )\r\n }\r\n\r\n // Handle version conflict errors (412 Precondition Failed)\r\n // Note: Don't include user-facing message here - callers should localize based on code\r\n if (response.status === 412) {\r\n throw Object.assign(\r\n new Error('VERSION_CONFLICT'),\r\n {\r\n status: 412,\r\n code: 'VERSION_CONFLICT',\r\n currentVersion: error.details?.currentVersion,\r\n expectedVersion: error.details?.expectedVersion,\r\n }\r\n )\r\n }\r\n\r\n // Extract validation errors if present\r\n let errorMessage = error.error || error.message || error.title || `HTTP ${response.status}`\r\n\r\n // Handle .NET validation errors format\r\n if (error.errors && typeof error.errors === 'object') {\r\n const validationErrors = Object.entries(error.errors)\r\n .map(([field, messages]) => {\r\n const msgs = Array.isArray(messages) ? messages : [messages]\r\n return `${field}: ${msgs.join(', ')}`\r\n })\r\n .join('; ')\r\n errorMessage = validationErrors || errorMessage\r\n }\r\n\r\n const apiError: ApiError = Object.assign(new Error(errorMessage), {\r\n status: response.status,\r\n error,\r\n })\r\n throw apiError\r\n }\r\n\r\n // Handle 204 No Content\r\n if (response.status === 204) {\r\n return {} as T\r\n }\r\n\r\n return response.json()\r\n }\r\n\r\n async get<T>(path: string, options?: RequestOptions): Promise<T> {\r\n // Deduplicate certain GET requests that may be called multiple times during bootstrap\r\n const deduplicatePaths = ['/webauthn/credentials', '/webauthn/key-bundles/current']\r\n const shouldDeduplicate = deduplicatePaths.some(p => path.includes(p))\r\n \r\n if (shouldDeduplicate) {\r\n const cacheKey = `get:${path}`\r\n const inFlight = this.inFlightRequests.get(cacheKey)\r\n if (inFlight) {\r\n return inFlight\r\n }\r\n \r\n const request = this.request<T>('GET', path, options)\r\n .finally(() => this.inFlightRequests.delete(cacheKey))\r\n \r\n this.inFlightRequests.set(cacheKey, request)\r\n return request\r\n }\r\n \r\n return this.request<T>('GET', path, options)\r\n }\r\n\r\n async post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {\r\n return this.request<T>('POST', path, { ...options, body })\r\n }\r\n\r\n async put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {\r\n return this.request<T>('PUT', path, { ...options, body })\r\n }\r\n\r\n async patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {\r\n return this.request<T>('PATCH', path, { ...options, body })\r\n }\r\n\r\n async delete<T>(path: string, options?: RequestOptions): Promise<T> {\r\n return this.request<T>('DELETE', path, options)\r\n }\r\n\r\n /**\r\n * Fetch binary data as a Blob (for downloading files like PDFs).\r\n */\r\n async getBlob(path: string, options?: RequestOptions): Promise<Blob> {\r\n const url = this.getApiUrl(path)\r\n const authHeaders = await this.config.auth.getAuthHeaders()\r\n const headers: Record<string, string> = {\r\n ...authHeaders,\r\n ...options?.headers,\r\n }\r\n\r\n const config: RequestInit = {\r\n method: 'GET',\r\n headers,\r\n credentials: this.config.auth.getCredentials(),\r\n }\r\n\r\n let response: Response\r\n try {\r\n response = await fetch(url, config)\r\n } catch (networkError) {\r\n console.error('Network error:', { url, error: networkError })\r\n const error = Object.assign(\r\n new Error('Unable to connect to server. Please check your connection and try again.'),\r\n { status: 0, code: 'NETWORK_ERROR', originalError: networkError }\r\n )\r\n throw error\r\n }\r\n\r\n if (!response.ok) {\r\n const error = await response.json().catch(() => ({\r\n message: 'An error occurred',\r\n }))\r\n const apiError: ApiError = Object.assign(new Error(error.message || `HTTP ${response.status}`), {\r\n status: response.status,\r\n error,\r\n })\r\n throw apiError\r\n }\r\n\r\n return response.blob()\r\n }\r\n\r\n // ===== HOUSEHOLD METHODS =====\r\n\r\n async getHouseholds(): Promise<Household[]> {\r\n return this.get<Household[]>('/households')\r\n }\r\n\r\n async createHousehold(data: { name: string; planCode?: string }): Promise<Household> {\r\n return this.post<Household>('/households', data)\r\n }\r\n\r\n async updateHousehold(id: string, data: { name: string }): Promise<void> {\r\n await this.put(`/households/${id}`, data)\r\n }\r\n\r\n async deleteHousehold(id: string): Promise<void> {\r\n await this.delete(`/households/${id}`)\r\n }\r\n\r\n /**\r\n * Extend trial for households with less than 5 entities.\r\n * Allows users who haven't fully explored to continue without subscribing.\r\n */\r\n async extendTrial(householdId: string): Promise<ExtendTrialResponse> {\r\n return this.post<ExtendTrialResponse>(`/households/${householdId}/extend-trial`, {})\r\n }\r\n\r\n /**\r\n * Permanently delete the current user's account and all personal data.\r\n * This includes: key bundles, WebAuthn credentials, personal vault entities,\r\n * member key access records, legal consents, user profile, and authentication identity.\r\n *\r\n * WARNING: This action is irreversible. The user must re-register to use the service again.\r\n */\r\n async deleteCurrentUser(): Promise<void> {\r\n await this.delete('/users/me')\r\n }\r\n\r\n async checkUserHasHousehold(): Promise<{ hasHousehold: boolean; households?: Household[] }> {\r\n const households = await this.getHouseholds()\r\n return {\r\n hasHousehold: households.length > 0,\r\n households: households.length > 0 ? households : undefined\r\n }\r\n }\r\n\r\n // ===== HOUSEHOLD KEYS METHODS =====\r\n\r\n async getHouseholdKeys(householdId: string): Promise<HouseholdKey[]> {\r\n const cacheKey = `getHouseholdKeys:${householdId}`\r\n \r\n // Return in-flight request if one exists (deduplication)\r\n const inFlight = this.inFlightRequests.get(cacheKey)\r\n if (inFlight) {\r\n return inFlight\r\n }\r\n \r\n // Create new request and cache it\r\n const request = this.get<HouseholdKey[]>(`/households/${householdId}/keys`)\r\n .finally(() => {\r\n // Remove from cache when complete (success or failure)\r\n this.inFlightRequests.delete(cacheKey)\r\n })\r\n \r\n this.inFlightRequests.set(cacheKey, request)\r\n return request\r\n }\r\n\r\n async grantHouseholdKey(householdId: string, data: {\r\n userId: string\r\n keyType: string\r\n encryptedKey: string\r\n }): Promise<void> {\r\n await this.post(`/households/${householdId}/keys/grant`, data)\r\n }\r\n\r\n async batchGrantHouseholdKeys(householdId: string, data: {\r\n userId: string\r\n keys: Array<{\r\n keyType: string\r\n encryptedKey: string\r\n userKeyBundleId: string\r\n }>\r\n }): Promise<void> {\r\n await this.post(`/households/${householdId}/keys/grant/batch`, data)\r\n }\r\n\r\n async revokeHouseholdKey(householdId: string, userId: string, keyType: string): Promise<void> {\r\n await this.delete(`/households/${householdId}/keys/${userId}/${keyType}`)\r\n }\r\n\r\n async getMembersKeyAccess(householdId: string): Promise<MemberKeyAccessSummary[]> {\r\n return this.get<MemberKeyAccessSummary[]>(`/households/${householdId}/keys/access`)\r\n }\r\n\r\n async getKeyTypes(householdId: string): Promise<KeyTypeInfo[]> {\r\n return this.get<KeyTypeInfo[]>(`/households/${householdId}/keys/types`)\r\n }\r\n\r\n async deleteKeyType(householdId: string, keyType: string): Promise<void> {\r\n await this.delete(`/households/${householdId}/keys/types/${keyType}`)\r\n }\r\n\r\n // ===== ENCRYPTED ENTITIES METHODS =====\r\n\r\n /**\r\n * Fetch entities for a single type. When batched=true (default), requests are\r\n * automatically combined with other concurrent calls into a single HTTP request.\r\n */\r\n async getEntities(householdId: string | null | undefined, params?: {\r\n entityType?: string\r\n limit?: number\r\n offset?: number\r\n includeDeleted?: boolean\r\n batched?: boolean // Default true - batch with other concurrent requests\r\n }): Promise<EntitiesResponse> {\r\n const batched = params?.batched ?? true\r\n\r\n // Use batcher for single entity type requests (most common case)\r\n if (batched && params?.entityType && !params?.limit && !params?.offset) {\r\n const items = await this.entityBatcher.request(\r\n householdId,\r\n params.entityType,\r\n params.includeDeleted ?? false\r\n )\r\n return { items, total: items.length }\r\n }\r\n\r\n // Fall back to direct request for non-batchable cases\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n if (params?.entityType) queryParams.set('entityType', params.entityType)\r\n if (params?.limit) queryParams.set('limit', params.limit.toString())\r\n if (params?.offset) queryParams.set('offset', params.offset.toString())\r\n if (params?.includeDeleted) queryParams.set('includeDeleted', params.includeDeleted.toString())\r\n\r\n const query = queryParams.toString()\r\n const path = `/entities${query ? `?${query}` : ''}`\r\n\r\n return this.get<EntitiesResponse>(path)\r\n }\r\n\r\n /**\r\n * Internal method to fetch multiple entity types in one request.\r\n * Used by the EntityBatcher.\r\n */\r\n private async getEntitiesMultiple(\r\n householdId: string | null | undefined,\r\n entityTypes: string[],\r\n includeDeleted: boolean\r\n ): Promise<EntitiesResponse> {\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n if (entityTypes.length > 0) queryParams.set('entityTypes', entityTypes.join(','))\r\n if (includeDeleted) queryParams.set('includeDeleted', 'true')\r\n\r\n const query = queryParams.toString()\r\n const path = `/entities${query ? `?${query}` : ''}`\r\n\r\n return this.get<EntitiesResponse>(path)\r\n }\r\n\r\n async getEntity(householdId: string | null | undefined, entityId: string): Promise<EncryptedEntityResponse> {\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n const query = queryParams.toString()\r\n const path = `/entities/${entityId}${query ? `?${query}` : ''}`\r\n\r\n return this.get<EncryptedEntityResponse>(path)\r\n }\r\n\r\n async createEntity(householdId: string | null | undefined, data: CreateEntityRequest): Promise<EncryptedEntityResponse> {\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n const query = queryParams.toString()\r\n const path = `/entities${query ? `?${query}` : ''}`\r\n\r\n return this.post<EncryptedEntityResponse>(path, data)\r\n }\r\n\r\n async updateEntity(\r\n householdId: string | null | undefined,\r\n entityId: string,\r\n data: UpdateEntityRequest,\r\n version: number\r\n ): Promise<EncryptedEntityResponse> {\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n const query = queryParams.toString()\r\n const path = `/entities/${entityId}${query ? `?${query}` : ''}`\r\n\r\n return this.put<EncryptedEntityResponse>(path, data, {\r\n headers: { 'If-Match': `\"${version}\"` }\r\n })\r\n }\r\n\r\n async deleteEntity(\r\n householdId: string | null | undefined,\r\n entityId: string,\r\n version: number\r\n ): Promise<void> {\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n const query = queryParams.toString()\r\n const path = `/entities/${entityId}${query ? `?${query}` : ''}`\r\n await this.delete(path, {\r\n headers: { 'If-Match': `\"${version}\"` }\r\n })\r\n }\r\n\r\n // ===== HOUSEHOLD MEMBERS METHODS =====\r\n\r\n async getHouseholdMembers(householdId: string): Promise<HouseholdMember[]> {\r\n return this.get<HouseholdMember[]>(`/households/${householdId}/members`)\r\n }\r\n\r\n async inviteHouseholdMember(\r\n householdId: string,\r\n email: string,\r\n role: HouseholdMemberRole = HOUSEHOLD_MEMBER_ROLES.MEMBER\r\n ): Promise<HouseholdMember> {\r\n return this.post<HouseholdMember>(`/households/${householdId}/members/invite`, { email, role })\r\n }\r\n\r\n async updateMemberRole(householdId: string, userId: string, data: {\r\n role: HouseholdMemberRole\r\n keysToGrant?: Array<{\r\n keyType: string\r\n encryptedKey: string\r\n userKeyBundleId: string\r\n }>\r\n keysToRevoke?: string[]\r\n }): Promise<void> {\r\n await this.put(`/households/${householdId}/members/${userId}`, data)\r\n }\r\n\r\n async removeHouseholdMember(householdId: string, userId: string): Promise<void> {\r\n await this.delete(`/households/${householdId}/members/${userId}`)\r\n }\r\n\r\n async getHouseholdPeople(householdId: string): Promise<{ people: HouseholdMember[] }> {\r\n const members = await this.getHouseholdMembers(householdId)\r\n return { people: members }\r\n }\r\n\r\n // DEPRECATED: Legacy invitation methods - will be removed\r\n async createHouseholdInvitation(\r\n householdId: string,\r\n role: HouseholdMemberRole = HOUSEHOLD_MEMBER_ROLES.MEMBER,\r\n residentId?: string,\r\n expiresIn?: number,\r\n oneTimeUse?: boolean\r\n ): Promise<unknown> {\r\n return this.post(`/households/${householdId}/invitations`, {\r\n role,\r\n residentId,\r\n expiresIn,\r\n oneTimeUse\r\n })\r\n }\r\n\r\n async revokeInvitation(invitationToken: string): Promise<void> {\r\n await this.delete(`/households/invitations/${invitationToken}`)\r\n }\r\n\r\n // ===== CONTACTS =====\r\n\r\n async getContacts(householdId: string): Promise<Contact[]> {\r\n return this.get<Contact[]>(`/contacts?householdId=${householdId}`)\r\n }\r\n\r\n async createContact(householdId: string, data: CreateContactRequest): Promise<Contact> {\r\n return this.post<Contact>('/contacts', { ...data, household_id: householdId })\r\n }\r\n\r\n async updateContact(id: string, data: Partial<CreateContactRequest>): Promise<Contact> {\r\n return this.put<Contact>(`/contacts/${id}`, data)\r\n }\r\n\r\n async deleteContact(id: string): Promise<void> {\r\n await this.delete<void>(`/contacts/${id}`)\r\n }\r\n\r\n // ===== INSURANCE POLICIES =====\r\n\r\n async getInsurancePolicies(householdId: string): Promise<InsurancePolicy[]> {\r\n return this.get<InsurancePolicy[]>(`/insurance-policies?householdId=${householdId}`)\r\n }\r\n\r\n async createInsurancePolicy(householdId: string, data: {\r\n provider: string\r\n policy_number: string\r\n effective_date: string\r\n expiration_date: string\r\n property_id?: string\r\n vehicle_id?: string\r\n pet_id?: string\r\n }): Promise<InsurancePolicy> {\r\n return this.post<InsurancePolicy>('/insurance-policies', { ...data, household_id: householdId })\r\n }\r\n\r\n async updateInsurancePolicy(id: string, data: {\r\n provider: string\r\n policy_number: string\r\n effective_date: string\r\n expiration_date: string\r\n property_id?: string\r\n vehicle_id?: string\r\n pet_id?: string\r\n type: InsurancePolicy['type']\r\n }, householdId: string): Promise<InsurancePolicy> {\r\n return this.put<InsurancePolicy>(`/insurance-policies/${id}`, { ...data, household_id: householdId })\r\n }\r\n\r\n async deleteInsurancePolicy(id: string): Promise<void> {\r\n await this.delete(`/insurance-policies/${id}`)\r\n }\r\n\r\n // ===== GENERIC RESOURCE METHODS =====\r\n // These are used by entity contexts and can be extended\r\n\r\n async getResources<T>(path: string, householdId: string): Promise<T[]> {\r\n return this.get<T[]>(`${path}?householdId=${householdId}`)\r\n }\r\n\r\n async createResource<T>(path: string, householdId: string, data: object): Promise<T> {\r\n return this.post<T>(path, { ...data, household_id: householdId })\r\n }\r\n\r\n async updateResource<T>(path: string, id: string, data: object): Promise<T> {\r\n return this.put<T>(`${path}/${id}`, data)\r\n }\r\n\r\n async deleteResource(path: string, id: string): Promise<void> {\r\n await this.delete(`${path}/${id}`)\r\n }\r\n\r\n // ===== USER KEY METHODS =====\r\n\r\n async batchAddKeys(data: BatchAddKeysRequest): Promise<BatchAddKeysResponse> {\r\n return this.post<BatchAddKeysResponse>('/users/batch-add-keys', data)\r\n }\r\n\r\n /**\r\n * Get the current user's key bundle (public key, encrypted private key, etc.)\r\n * This is deduplicated automatically - concurrent calls return the same promise.\r\n */\r\n async getCurrentKeyBundle(): Promise<UserKeyBundle> {\r\n return this.get<UserKeyBundle>('/webauthn/key-bundles/current')\r\n }\r\n\r\n // ===== BILLING METHODS =====\r\n\r\n /**\r\n * Create a Stripe Checkout session to start a subscription.\r\n * Only household owners can call this.\r\n */\r\n async createCheckoutSession(householdId: string, planCode: string): Promise<CheckoutSessionResponse> {\r\n return this.post<CheckoutSessionResponse>(`/billing/${householdId}/checkout`, { planCode })\r\n }\r\n\r\n /**\r\n * Create a Stripe Customer Portal session for managing subscription.\r\n * Only household owners can call this.\r\n */\r\n async createPortalSession(householdId: string): Promise<CustomerPortalSessionResponse> {\r\n return this.post<CustomerPortalSessionResponse>(`/billing/${householdId}/portal`, {})\r\n }\r\n\r\n /**\r\n * Get available subscription plans.\r\n * This endpoint is public (no auth required).\r\n */\r\n async getAvailablePlans(): Promise<AvailablePlan[]> {\r\n return this.get<AvailablePlan[]>('/billing/plans')\r\n }\r\n\r\n /**\r\n * Start a free trial for a household.\r\n * No credit card required.\r\n */\r\n async startTrial(householdId: string, tier?: 'household' | 'estate'): Promise<StartTrialResponse> {\r\n return this.post<StartTrialResponse>(`/billing/${householdId}/start-trial`, { tier })\r\n }\r\n\r\n /**\r\n * Upgrade an existing subscription to a new plan.\r\n * This updates the subscription in place instead of creating a new one.\r\n */\r\n async upgradeSubscription(householdId: string, planCode: string): Promise<UpgradeSubscriptionResponse> {\r\n return this.post<UpgradeSubscriptionResponse>(`/billing/${householdId}/upgrade`, { planCode })\r\n }\r\n\r\n /**\r\n * Get billing details for subscription sync.\r\n * Used by frontend to sync EstateHelm subscription entity.\r\n */\r\n async getBillingDetails(householdId: string): Promise<BillingDetailsResponse> {\r\n return this.get<BillingDetailsResponse>(`/billing/${householdId}/details`)\r\n }\r\n\r\n // ===== MOBILE BILLING METHODS (Apple IAP / Google Play) =====\r\n\r\n /**\r\n * Verify an Apple In-App Purchase transaction.\r\n * Called by iOS app after a successful StoreKit purchase.\r\n */\r\n async verifyApplePurchase(householdId: string, data: {\r\n transactionId?: string\r\n signedTransaction?: string\r\n }): Promise<MobilePurchaseVerificationResponse> {\r\n return this.post<MobilePurchaseVerificationResponse>(`/apple-billing/${householdId}/verify-transaction`, data)\r\n }\r\n\r\n /**\r\n * Sync Apple subscription status with the server.\r\n */\r\n async syncAppleSubscription(householdId: string): Promise<MobileSubscriptionSyncResponse> {\r\n return this.post<MobileSubscriptionSyncResponse>(`/apple-billing/${householdId}/sync`, {})\r\n }\r\n\r\n /**\r\n * Get Apple subscription status for a household.\r\n */\r\n async getAppleSubscriptionStatus(householdId: string): Promise<MobileSubscriptionStatusResponse> {\r\n return this.get<MobileSubscriptionStatusResponse>(`/apple-billing/${householdId}/status`)\r\n }\r\n\r\n /**\r\n * Get available Apple products (for StoreKit).\r\n */\r\n async getAppleProducts(): Promise<MobileProductInfo[]> {\r\n return this.get<MobileProductInfo[]>('/apple-billing/products')\r\n }\r\n\r\n /**\r\n * Verify a Google Play purchase.\r\n * Called by Android app after a successful Google Play Billing purchase.\r\n */\r\n async verifyGooglePurchase(householdId: string, data: {\r\n productId: string\r\n purchaseToken: string\r\n }): Promise<MobilePurchaseVerificationResponse> {\r\n return this.post<MobilePurchaseVerificationResponse>(`/google-billing/${householdId}/verify-purchase`, data)\r\n }\r\n\r\n /**\r\n * Sync Google Play subscription status with the server.\r\n */\r\n async syncGoogleSubscription(householdId: string): Promise<MobileSubscriptionSyncResponse> {\r\n return this.post<MobileSubscriptionSyncResponse>(`/google-billing/${householdId}/sync`, {})\r\n }\r\n\r\n /**\r\n * Get Google Play subscription status for a household.\r\n */\r\n async getGoogleSubscriptionStatus(householdId: string): Promise<MobileSubscriptionStatusResponse> {\r\n return this.get<MobileSubscriptionStatusResponse>(`/google-billing/${householdId}/status`)\r\n }\r\n\r\n /**\r\n * Get available Google Play products.\r\n */\r\n async getGoogleProducts(): Promise<MobileProductInfo[]> {\r\n return this.get<MobileProductInfo[]>('/google-billing/products')\r\n }\r\n}\r\n\r\n// Mobile billing response types\r\n\r\nexport interface MobilePurchaseVerificationResponse {\r\n success: boolean\r\n planCode?: string\r\n tier?: string\r\n expiresAt?: string\r\n originalTransactionId?: string // Apple\r\n orderId?: string // Google\r\n acknowledged?: boolean // Google\r\n}\r\n\r\nexport interface MobileSubscriptionSyncResponse {\r\n success: boolean\r\n status?: string\r\n message?: string\r\n expiresAt?: string\r\n autoRenewEnabled?: boolean\r\n productId?: string\r\n}\r\n\r\nexport interface PurchaserInfo {\r\n id: string\r\n email?: string\r\n name?: string\r\n}\r\n\r\nexport interface MobileSubscriptionStatusResponse {\r\n paymentProvider: 'none' | 'stripe' | 'apple' | 'google'\r\n subscriptionStatus?: string\r\n planCode?: string\r\n trialEndsAt?: string\r\n currentPeriodEndsAt?: string\r\n appleProductId?: string\r\n appleAutoRenewEnabled?: boolean\r\n hasAppleSubscription?: boolean\r\n googleProductId?: string\r\n googleAutoRenewEnabled?: boolean\r\n hasGoogleSubscription?: boolean\r\n purchaserUserId?: string\r\n purchaserInfo?: PurchaserInfo\r\n isCurrentUserPurchaser?: boolean\r\n}\r\n\r\nexport interface MobileProductInfo {\r\n productId: string\r\n planCode: string\r\n tier: string\r\n billingCycle: 'monthly' | 'annual'\r\n}\r\n","/**\r\n * Utility functions for encryption operations\r\n * \r\n * This module provides helper functions for encoding/decoding and crypto operations.\r\n * \r\n * @module encryption/utils\r\n */\r\n\r\n/**\r\n * Converts a Uint8Array to a base64 string\r\n * \r\n * @param bytes - The byte array to encode\r\n * @returns Base64-encoded string\r\n * \r\n * @example\r\n * ```ts\r\n * const bytes = new Uint8Array([72, 101, 108, 108, 111]);\r\n * const base64 = base64Encode(bytes);\r\n * console.log(base64); // \"SGVsbG8=\"\r\n * ```\r\n */\r\nexport function base64Encode(bytes: Uint8Array): string {\r\n const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('');\r\n return btoa(binString);\r\n}\r\n\r\n/**\r\n * Converts a base64 string to a Uint8Array\r\n * \r\n * @param base64 - The base64 string to decode\r\n * @returns Decoded byte array\r\n * @throws {Error} If the base64 string is invalid\r\n * \r\n * @example\r\n * ```ts\r\n * const base64 = \"SGVsbG8=\";\r\n * const bytes = base64Decode(base64);\r\n * console.log(bytes); // Uint8Array [72, 101, 108, 108, 111]\r\n * ```\r\n */\r\nexport function base64Decode(base64: string): Uint8Array {\r\n try {\r\n // Strip any whitespace/newlines that PostgreSQL might add\r\n const cleanBase64 = base64.replace(/\\s/g, '');\r\n const binString = atob(cleanBase64);\r\n return Uint8Array.from(binString, (char) => char.codePointAt(0)!);\r\n } catch (error) {\r\n throw new Error(`Failed to decode base64: ${error instanceof Error ? error.message : 'Unknown error'}`);\r\n }\r\n}\r\n\r\n/**\r\n * Converts a Uint8Array to a base64url string (URL-safe, no padding)\r\n *\r\n * Base64url uses '-' instead of '+', '_' instead of '/', and no '=' padding.\r\n * This is the format required by WebAuthn.\r\n *\r\n * @param bytes - The byte array to encode\r\n * @returns Base64url-encoded string\r\n */\r\nexport function base64UrlEncode(bytes: Uint8Array): string {\r\n const base64 = base64Encode(bytes);\r\n return base64\r\n .replace(/\\+/g, '-')\r\n .replace(/\\//g, '_')\r\n .replace(/=/g, '');\r\n}\r\n\r\n/**\r\n * Converts a base64url string to a Uint8Array\r\n *\r\n * Handles both base64url (no padding) and standard base64 formats.\r\n *\r\n * @param base64url - The base64url string to decode\r\n * @returns Decoded byte array\r\n */\r\nexport function base64UrlDecode(base64url: string): Uint8Array {\r\n // Convert base64url to standard base64\r\n let base64 = base64url\r\n .replace(/-/g, '+')\r\n .replace(/_/g, '/');\r\n\r\n // Add padding if needed\r\n const pad = base64.length % 4;\r\n if (pad) {\r\n base64 += '='.repeat(4 - pad);\r\n }\r\n\r\n return base64Decode(base64);\r\n}\r\n\r\n/**\r\n * Generates cryptographically secure random bytes\r\n * \r\n * @param length - Number of random bytes to generate\r\n * @returns Uint8Array of random bytes\r\n * \r\n * @example\r\n * ```ts\r\n * const salt = generateRandomBytes(32); // 32-byte salt\r\n * const key = generateRandomBytes(32); // 256-bit key\r\n * ```\r\n */\r\nexport function generateRandomBytes(length: number): Uint8Array {\r\n return crypto.getRandomValues(new Uint8Array(length));\r\n}\r\n\r\n/**\r\n * Converts a string to a Uint8Array using UTF-8 encoding\r\n * \r\n * @param str - The string to encode\r\n * @returns UTF-8 encoded byte array\r\n * \r\n * @example\r\n * ```ts\r\n * const bytes = stringToBytes(\"Hello, 世界\");\r\n * ```\r\n */\r\nexport function stringToBytes(str: string): Uint8Array {\r\n return new TextEncoder().encode(str);\r\n}\r\n\r\n/**\r\n * Converts a Uint8Array to a string using UTF-8 decoding\r\n * \r\n * @param bytes - The byte array to decode\r\n * @returns Decoded string\r\n * \r\n * @example\r\n * ```ts\r\n * const str = bytesToString(new Uint8Array([72, 101, 108, 108, 111]));\r\n * console.log(str); // \"Hello\"\r\n * ```\r\n */\r\nexport function bytesToString(bytes: Uint8Array): string {\r\n return new TextDecoder().decode(bytes);\r\n}\r\n\r\n/**\r\n * Validates that a passphrase meets minimum security requirements\r\n * \r\n * @param passphrase - The passphrase to validate\r\n * @param minLength - Minimum required length (default: 16)\r\n * @throws {Error} If passphrase doesn't meet requirements\r\n * \r\n * @example\r\n * ```ts\r\n * validatePassphrase(\"short\"); // throws Error\r\n * validatePassphrase(\"this-is-a-secure-passphrase\"); // ok\r\n * ```\r\n */\r\nexport function validatePassphrase(passphrase: string, minLength: number = 16): void {\r\n if (!passphrase || passphrase.length < minLength) {\r\n throw new Error(`Passphrase must be at least ${minLength} characters long`);\r\n }\r\n \r\n // Optional: Add additional requirements (uppercase, lowercase, numbers, etc.)\r\n // For now, we just check length since the architecture doc only requires 16+ chars\r\n}\r\n\r\n/**\r\n * Securely compares two byte arrays in constant time to prevent timing attacks\r\n * \r\n * @param a - First byte array\r\n * @param b - Second byte array\r\n * @returns true if arrays are equal, false otherwise\r\n * \r\n * @example\r\n * ```ts\r\n * const key1 = new Uint8Array([1, 2, 3]);\r\n * const key2 = new Uint8Array([1, 2, 3]);\r\n * console.log(constantTimeEqual(key1, key2)); // true\r\n * ```\r\n */\r\nexport function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\r\n if (a.length !== b.length) {\r\n return false;\r\n }\r\n \r\n let result = 0;\r\n for (let i = 0; i < a.length; i++) {\r\n result |= a[i] ^ b[i];\r\n }\r\n \r\n return result === 0;\r\n}\r\n\r\n/**\r\n * Generates a cryptographically secure UUID v4\r\n * \r\n * @returns UUID string in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\r\n * \r\n * @example\r\n * ```ts\r\n * const id = generateUUID();\r\n * console.log(id); // \"550e8400-e29b-41d4-a716-446655440000\"\r\n * ```\r\n */\r\nexport function generateUUID(): string {\r\n return crypto.randomUUID();\r\n}\r\n\r\n/**\r\n * Calibrates PBKDF2 iterations to target duration\r\n * \r\n * This function tests different iteration counts to find the value\r\n * that results in approximately the target duration (default: 500ms).\r\n * \r\n * @param targetMs - Target duration in milliseconds (default: 500)\r\n * @param testSalt - Optional salt for testing (generates random if not provided)\r\n * @returns Recommended iteration count\r\n * \r\n * @example\r\n * ```ts\r\n * const iterations = await calibratePBKDF2Iterations(500);\r\n * console.log(`Use ${iterations} iterations for ~500ms`);\r\n * ```\r\n */\r\nexport async function calibratePBKDF2Iterations(\r\n targetMs: number = 500,\r\n testSalt?: Uint8Array\r\n): Promise<number> {\r\n const salt = testSalt || generateRandomBytes(32);\r\n const testPassphrase = 'test-passphrase-for-calibration';\r\n \r\n // Start with a baseline\r\n let iterations = 100000;\r\n \r\n const testDerivation = async (iters: number): Promise<number> => {\r\n const start = performance.now();\r\n \r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n stringToBytes(testPassphrase) as BufferSource,\r\n 'PBKDF2',\r\n false,\r\n ['deriveBits']\r\n );\r\n \r\n await crypto.subtle.deriveBits(\r\n {\r\n name: 'PBKDF2',\r\n salt: salt as BufferSource,\r\n iterations: iters,\r\n hash: 'SHA-256'\r\n },\r\n keyMaterial,\r\n 256\r\n );\r\n \r\n return performance.now() - start;\r\n };\r\n \r\n // Test initial iterations\r\n const duration = await testDerivation(iterations);\r\n \r\n // Adjust based on result\r\n if (duration < targetMs) {\r\n // Too fast, increase iterations\r\n iterations = Math.floor(iterations * (targetMs / duration));\r\n } else if (duration > targetMs * 1.5) {\r\n // Too slow, decrease iterations\r\n iterations = Math.floor(iterations * (targetMs / duration));\r\n }\r\n \r\n // Round to nearest 10000 for cleaner numbers\r\n return Math.max(100000, Math.round(iterations / 10000) * 10000);\r\n}\r\n","/**\r\n * Household Keys Module\r\n * \r\n * This module handles the generation and encryption of household keys.\r\n * Each household has three keys that encrypt different categories of data:\r\n * \r\n * - General: Properties, Pets, Vehicles, Access Codes (WiFi, door, gate)\r\n * - Financial: Bank Accounts, Tax Docs, Estate Planning\r\n * - Security: Safe Combinations, Alarm Master Codes, Password Manager\r\n * \r\n * Household keys are encrypted with the user's master key and stored locally.\r\n * \r\n * @module encryption/householdKeys\r\n */\r\n\r\nimport { base64Encode, base64Decode, generateRandomBytes } from './utils';\r\nimport type { HouseholdKeyType, EncryptedHouseholdKey } from './types';\r\n\r\n/**\r\n * Household key size in bytes (256-bit AES)\r\n */\r\nexport const HOUSEHOLD_KEY_SIZE = 32;\r\n\r\n/**\r\n * IV size for AES-GCM (96 bits / 12 bytes)\r\n */\r\nexport const IV_SIZE = 12;\r\n\r\n/**\r\n * Generated household keys (unencrypted)\r\n * Map of key type to raw key bytes\r\n */\r\nexport type GeneratedHouseholdKeys = Map<HouseholdKeyType, Uint8Array>;\r\n\r\n/**\r\n * Generates household keys for the specified key types\r\n * \r\n * These keys are generated using cryptographically secure random bytes.\r\n * Each key is 256 bits (32 bytes) for AES-256-GCM encryption.\r\n * \r\n * @param keyTypes - Array of key type identifiers (e.g., ['general', 'financial', 'security'])\r\n * @returns Map of key types to raw key bytes\r\n * \r\n * @example\r\n * ```ts\r\n * // Generate keys when creating a new household\r\n * const keyTypes = ['general', 'financial', 'security'];\r\n * const householdKeys = generateHouseholdKeys(keyTypes);\r\n * \r\n * // Encrypt them with the user's master key\r\n * const encrypted = await encryptAllHouseholdKeys(householdKeys, masterKey);\r\n * \r\n * // Store encrypted keys locally\r\n * await storeEncryptedKeys(householdId, encrypted);\r\n * ```\r\n */\r\nexport function generateHouseholdKeys(keyTypes: HouseholdKeyType[]): GeneratedHouseholdKeys {\r\n const keys = new Map<HouseholdKeyType, Uint8Array>();\r\n \r\n for (const keyType of keyTypes) {\r\n keys.set(keyType, generateRandomBytes(HOUSEHOLD_KEY_SIZE));\r\n }\r\n \r\n return keys;\r\n}\r\n\r\n/**\r\n * Encrypts a single household key with the user's master key\r\n * \r\n * Uses AES-256-GCM for authenticated encryption.\r\n * Each encryption generates a new random IV.\r\n * \r\n * @param householdKey - Raw household key bytes to encrypt\r\n * @param masterKey - User's master key (CryptoKey)\r\n * @param keyType - Type of household key being encrypted\r\n * @param version - Version number for key rotation (default: 1)\r\n * @returns Encrypted household key with metadata\r\n * @throws {Error} If encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * const generalKey = generateRandomBytes(32);\r\n * const encrypted = await encryptHouseholdKey(\r\n * generalKey,\r\n * masterKey,\r\n * 'general',\r\n * 1\r\n * );\r\n * ```\r\n */\r\nexport async function encryptHouseholdKey(\r\n householdKey: Uint8Array,\r\n masterKey: CryptoKey,\r\n keyType: HouseholdKeyType,\r\n version: number = 1\r\n): Promise<EncryptedHouseholdKey> {\r\n if (householdKey.length !== HOUSEHOLD_KEY_SIZE) {\r\n throw new Error(`Invalid household key size: expected ${HOUSEHOLD_KEY_SIZE} bytes, got ${householdKey.length}`);\r\n }\r\n \r\n try {\r\n // Generate random IV\r\n const iv = generateRandomBytes(IV_SIZE);\r\n \r\n // Encrypt household key with master key\r\n const ciphertext = await crypto.subtle.encrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n },\r\n masterKey,\r\n householdKey as BufferSource\r\n );\r\n \r\n // Pack into format: [1-byte version][12-byte IV][ciphertext+auth tag]\r\n const ciphertextArray = new Uint8Array(ciphertext);\r\n const packed = new Uint8Array(1 + IV_SIZE + ciphertextArray.length);\r\n packed[0] = version;\r\n packed.set(iv, 1);\r\n packed.set(ciphertextArray, 1 + IV_SIZE);\r\n \r\n return {\r\n keyType,\r\n encryptedKey: base64Encode(packed),\r\n iv: base64Encode(iv), // Keep for backwards compatibility\r\n version\r\n };\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to encrypt ${keyType} household key: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Encrypts all household keys with the user's master key\r\n * \r\n * @param householdKeys - Generated household keys\r\n * @param masterKey - User's master key (CryptoKey)\r\n * @param version - Version number for key rotation (default: 1)\r\n * @returns Array of encrypted household keys\r\n * @throws {Error} If any encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * const keys = generateHouseholdKeys();\r\n * const encrypted = await encryptAllHouseholdKeys(keys, masterKey);\r\n * \r\n * // Store in local database\r\n * for (const encKey of encrypted) {\r\n * await localDB.insert('household_keys', {\r\n * household_id: householdId,\r\n * key_type: encKey.keyType,\r\n * encrypted_key: encKey.encryptedKey,\r\n * iv: encKey.iv,\r\n * version: encKey.version\r\n * });\r\n * }\r\n * ```\r\n */\r\nexport async function encryptAllHouseholdKeys(\r\n householdKeys: GeneratedHouseholdKeys,\r\n masterKey: CryptoKey,\r\n version: number = 1\r\n): Promise<EncryptedHouseholdKey[]> {\r\n const encrypted: EncryptedHouseholdKey[] = [];\r\n \r\n for (const [keyType, keyBytes] of householdKeys) {\r\n const encryptedKey = await encryptHouseholdKey(\r\n keyBytes,\r\n masterKey,\r\n keyType,\r\n version\r\n );\r\n encrypted.push(encryptedKey);\r\n }\r\n \r\n return encrypted;\r\n}\r\n\r\n/**\r\n * Decrypts a household key using the user's master key\r\n * \r\n * @param encryptedKey - Encrypted household key from storage\r\n * @param masterKey - User's master key (CryptoKey)\r\n * @returns Decrypted household key bytes\r\n * @throws {Error} If decryption fails (wrong key, corrupted data, etc.)\r\n * \r\n * @example\r\n * ```ts\r\n * // Retrieve encrypted key from local database\r\n * const encKey = await localDB.get('household_keys', {\r\n * household_id: householdId,\r\n * key_type: 'general'\r\n * });\r\n * \r\n * // Decrypt it\r\n * const householdKey = await decryptHouseholdKey(encKey, masterKey);\r\n * \r\n * // Use for entity encryption/decryption\r\n * const entityKey = await deriveEntityKey(householdKey, entityId, entityType);\r\n * ```\r\n */\r\nexport async function decryptHouseholdKey(\r\n encryptedKey: EncryptedHouseholdKey,\r\n masterKey: CryptoKey\r\n): Promise<Uint8Array> {\r\n try {\r\n const encryptedData = base64Decode(encryptedKey.encryptedKey);\r\n \r\n // Unpack format: [1-byte version][12-byte IV][ciphertext+auth tag]\r\n if (encryptedData.length < 1 + IV_SIZE) {\r\n throw new Error('Invalid encrypted key format: too short');\r\n }\r\n \r\n const version = encryptedData[0];\r\n const iv = encryptedData.slice(1, 1 + IV_SIZE);\r\n const ciphertext = encryptedData.slice(1 + IV_SIZE);\r\n \r\n if (version !== 1) {\r\n throw new Error(`Unsupported crypto version: ${version}`);\r\n }\r\n \r\n // Decrypt\r\n const plaintext = await crypto.subtle.decrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n },\r\n masterKey,\r\n ciphertext as BufferSource\r\n );\r\n \r\n return new Uint8Array(plaintext);\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to decrypt ${encryptedKey.keyType} household key: ${error instanceof Error ? error.message : 'Unknown error'}. This may indicate a wrong master key or corrupted data.`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Re-encrypts a household key with a new master key\r\n * \r\n * This is used when:\r\n * - User changes their passphrase\r\n * - User accepts an invitation and needs to re-encrypt with their own master key\r\n * - Migrating between security levels\r\n * \r\n * @param encryptedKey - Currently encrypted household key\r\n * @param oldMasterKey - Current master key\r\n * @param newMasterKey - New master key to encrypt with\r\n * @param newVersion - Optional new version number\r\n * @returns Re-encrypted household key\r\n * @throws {Error} If decryption or encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * // User changes passphrase\r\n * const oldMasterKey = await importMasterKey(oldKeyBytes);\r\n * const newMasterKey = await importMasterKey(newKeyBytes);\r\n * \r\n * // Re-encrypt all household keys\r\n * for (const encKey of encryptedKeys) {\r\n * const reEncrypted = await reEncryptHouseholdKey(\r\n * encKey,\r\n * oldMasterKey,\r\n * newMasterKey\r\n * );\r\n * \r\n * await localDB.update('household_keys', reEncrypted, {\r\n * household_id: householdId,\r\n * key_type: encKey.keyType\r\n * });\r\n * }\r\n * ```\r\n */\r\nexport async function reEncryptHouseholdKey(\r\n encryptedKey: EncryptedHouseholdKey,\r\n oldMasterKey: CryptoKey,\r\n newMasterKey: CryptoKey,\r\n newVersion?: number\r\n): Promise<EncryptedHouseholdKey> {\r\n // Decrypt with old master key\r\n const householdKey = await decryptHouseholdKey(encryptedKey, oldMasterKey);\r\n \r\n // Re-encrypt with new master key\r\n return await encryptHouseholdKey(\r\n householdKey,\r\n newMasterKey,\r\n encryptedKey.keyType,\r\n newVersion ?? encryptedKey.version\r\n );\r\n}\r\n\r\n/**\r\n * Imports a household key for use in entity encryption/decryption\r\n * \r\n * @param keyBytes - Raw household key bytes\r\n * @returns CryptoKey ready for HKDF key derivation\r\n * @throws {Error} If import fails\r\n * \r\n * @example\r\n * ```ts\r\n * const keyBytes = await decryptHouseholdKey(encryptedKey, masterKey);\r\n * const householdKey = await importHouseholdKeyForDerivation(keyBytes);\r\n * \r\n * // Now use for HKDF\r\n * const derivedBits = await crypto.subtle.deriveBits(\r\n * { name: 'HKDF', ... },\r\n * householdKey,\r\n * 256\r\n * );\r\n * ```\r\n */\r\nexport async function importHouseholdKeyForDerivation(\r\n keyBytes: Uint8Array\r\n): Promise<CryptoKey> {\r\n if (keyBytes.length !== HOUSEHOLD_KEY_SIZE) {\r\n throw new Error(`Invalid household key size: expected ${HOUSEHOLD_KEY_SIZE} bytes, got ${keyBytes.length}`);\r\n }\r\n \r\n try {\r\n return await crypto.subtle.importKey(\r\n 'raw',\r\n keyBytes as BufferSource,\r\n 'HKDF',\r\n false,\r\n ['deriveBits']\r\n );\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to import household key for derivation: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n\r\n","/**\r\n * Entity Key Derivation Module\r\n * \r\n * This module handles the deterministic derivation of entity-specific encryption keys\r\n * from household keys using HKDF (HMAC-based Key Derivation Function).\r\n * \r\n * Key benefits of deterministic per-entity keys:\r\n * - Enables granular sharing (share one property without sharing household key)\r\n * - No storage overhead (keys derived on-demand from household key + metadata)\r\n * - Simpler than random per-entity keys (no encrypted_entity_key storage)\r\n * - Service can help with sharing (owner shares derived key for specific entity)\r\n * \r\n * Key derivation:\r\n * Entity Key = HKDF(household_key, info: entity_type + \":\" + entity_id)\r\n * \r\n * @module encryption/entityKeys\r\n */\r\n\r\nimport { stringToBytes } from './utils';\r\nimport { importHouseholdKeyForDerivation } from './householdKeys';\r\nimport type { EntityType } from './types';\r\n\r\n/**\r\n * Entity key size in bytes (256-bit AES)\r\n */\r\nexport const ENTITY_KEY_SIZE = 32;\r\n\r\n/**\r\n * Derives a deterministic entity-specific encryption key from a household key\r\n * \r\n * This function uses HKDF (HMAC-based Key Derivation Function) to derive a unique\r\n * encryption key for each entity. The same household key + entity metadata will\r\n * always produce the same entity key, enabling:\r\n * \r\n * 1. Deterministic re-derivation (no need to store entity keys)\r\n * 2. Granular sharing (share derived key for one entity without sharing household key)\r\n * 3. No storage overhead (derive on-demand)\r\n * \r\n * The derived key is unique per entity because the info parameter includes both\r\n * the entity type and entity ID, ensuring different entities get different keys\r\n * even if they share the same household key.\r\n * \r\n * @param householdKey - Raw household key bytes (32 bytes)\r\n * @param entityId - Unique identifier for the entity (UUID)\r\n * @param entityType - Type of entity (property, pet, etc.)\r\n * @returns Derived entity key bytes (32 bytes)\r\n * @throws {Error} If derivation fails or inputs are invalid\r\n * \r\n * @example\r\n * ```ts\r\n * // Encrypt a property\r\n * const householdKey = await decryptHouseholdKey(encryptedKey, masterKey);\r\n * const propertyKey = await deriveEntityKey(\r\n * householdKey,\r\n * 'property-uuid-123',\r\n * 'property'\r\n * );\r\n * \r\n * // Later: decrypt the same property (same inputs = same key)\r\n * const sameKey = await deriveEntityKey(\r\n * householdKey,\r\n * 'property-uuid-123',\r\n * 'property'\r\n * );\r\n * // propertyKey === sameKey ✓\r\n * ```\r\n */\r\nexport async function deriveEntityKey(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType\r\n): Promise<Uint8Array> {\r\n if (!entityId || entityId.trim().length === 0) {\r\n throw new Error('Entity ID cannot be empty');\r\n }\r\n\r\n if (!entityType || entityType.trim().length === 0) {\r\n throw new Error('Entity type cannot be empty');\r\n }\r\n\r\n const infoString = `${entityType}:${entityId}`;\r\n\r\n try {\r\n // Import household key for HKDF\r\n const keyMaterial = await importHouseholdKeyForDerivation(householdKey);\r\n\r\n // Create info parameter: \"entity_type:entity_id\"\r\n // This ensures each entity gets a unique derived key\r\n const info = stringToBytes(infoString);\r\n \r\n // Derive entity key using HKDF\r\n // - No salt (empty salt) because we want deterministic derivation\r\n // - Info parameter provides the uniqueness\r\n const derivedBits = await crypto.subtle.deriveBits(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: new Uint8Array(0) as BufferSource, // Empty salt for deterministic derivation\r\n info: info as BufferSource\r\n },\r\n keyMaterial,\r\n ENTITY_KEY_SIZE * 8 // 256 bits\r\n );\r\n \r\n return new Uint8Array(derivedBits);\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to derive entity key for ${entityType}:${entityId}: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Imports a derived entity key for use in AES-GCM encryption/decryption\r\n * \r\n * @param entityKeyBytes - Derived entity key bytes (32 bytes)\r\n * @returns CryptoKey ready for AES-GCM operations\r\n * @throws {Error} If import fails or key size is invalid\r\n * \r\n * @example\r\n * ```ts\r\n * const entityKeyBytes = await deriveEntityKey(householdKey, entityId, entityType);\r\n * const entityKey = await importEntityKey(entityKeyBytes);\r\n * \r\n * // Use for encryption\r\n * const encrypted = await crypto.subtle.encrypt(\r\n * { name: 'AES-GCM', iv },\r\n * entityKey,\r\n * dataBytes\r\n * );\r\n * ```\r\n */\r\nexport async function importEntityKey(entityKeyBytes: Uint8Array): Promise<CryptoKey> {\r\n if (entityKeyBytes.length !== ENTITY_KEY_SIZE) {\r\n throw new Error(`Invalid entity key size: expected ${ENTITY_KEY_SIZE} bytes, got ${entityKeyBytes.length}`);\r\n }\r\n \r\n try {\r\n return await crypto.subtle.importKey(\r\n 'raw',\r\n entityKeyBytes as BufferSource,\r\n {\r\n name: 'AES-GCM',\r\n length: ENTITY_KEY_SIZE * 8\r\n },\r\n false, // Not extractable (security best practice)\r\n ['encrypt', 'decrypt']\r\n );\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to import entity key: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Derives and imports an entity key in one step\r\n * \r\n * Convenience function that combines deriveEntityKey and importEntityKey.\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - Unique identifier for the entity\r\n * @param entityType - Type of entity\r\n * @returns CryptoKey ready for encryption/decryption\r\n * @throws {Error} If derivation or import fails\r\n * \r\n * @example\r\n * ```ts\r\n * const entityKey = await deriveAndImportEntityKey(\r\n * householdKey,\r\n * 'property-uuid-123',\r\n * 'property'\r\n * );\r\n * \r\n * // Use directly for encryption\r\n * const encrypted = await crypto.subtle.encrypt(\r\n * { name: 'AES-GCM', iv },\r\n * entityKey,\r\n * dataBytes\r\n * );\r\n * ```\r\n */\r\nexport async function deriveAndImportEntityKey(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType\r\n): Promise<CryptoKey> {\r\n const entityKeyBytes = await deriveEntityKey(householdKey, entityId, entityType);\r\n return await importEntityKey(entityKeyBytes);\r\n}\r\n\r\n/**\r\n * Batch derives multiple entity keys for the same household\r\n * \r\n * This is useful for:\r\n * - Initial sync (decrypt many entities at once)\r\n * - Bulk operations (encrypt/decrypt multiple entities)\r\n * - Pre-derivation for offline use\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param entities - Array of entity metadata (id and type)\r\n * @returns Map of entity IDs to derived key bytes\r\n * @throws {Error} If any derivation fails\r\n * \r\n * @example\r\n * ```ts\r\n * const entities = [\r\n * { id: 'prop-1', type: 'property' },\r\n * { id: 'prop-2', type: 'property' },\r\n * { id: 'pet-1', type: 'pet' }\r\n * ];\r\n * \r\n * const keys = await deriveEntityKeysBatch(householdKey, entities);\r\n * \r\n * // Decrypt all entities\r\n * for (const entity of entities) {\r\n * const key = keys.get(entity.id);\r\n * const decrypted = await decryptEntity(entity, key);\r\n * }\r\n * ```\r\n */\r\nexport async function deriveEntityKeysBatch(\r\n householdKey: Uint8Array,\r\n entities: Array<{ id: string; type: EntityType }>\r\n): Promise<Map<string, Uint8Array>> {\r\n const keys = new Map<string, Uint8Array>();\r\n \r\n // Derive keys in parallel for better performance\r\n await Promise.all(\r\n entities.map(async (entity) => {\r\n const key = await deriveEntityKey(householdKey, entity.id, entity.type);\r\n keys.set(entity.id, key);\r\n })\r\n );\r\n \r\n return keys;\r\n}\r\n\r\n/**\r\n * Verifies that a derived entity key is correct by re-deriving it\r\n * \r\n * This can be used to:\r\n * - Validate cached entity keys\r\n * - Verify that household key hasn't changed\r\n * - Test key derivation implementation\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - Entity identifier\r\n * @param entityType - Entity type\r\n * @param expectedKey - Expected derived key bytes\r\n * @returns true if keys match, false otherwise\r\n * \r\n * @example\r\n * ```ts\r\n * const cachedKey = getCachedEntityKey(entityId);\r\n * const isValid = await verifyEntityKey(\r\n * householdKey,\r\n * entityId,\r\n * 'property',\r\n * cachedKey\r\n * );\r\n * \r\n * if (!isValid) {\r\n * // Re-derive and update cache\r\n * const freshKey = await deriveEntityKey(householdKey, entityId, 'property');\r\n * updateCache(entityId, freshKey);\r\n * }\r\n * ```\r\n */\r\nexport async function verifyEntityKey(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType,\r\n expectedKey: Uint8Array\r\n): Promise<boolean> {\r\n try {\r\n const derivedKey = await deriveEntityKey(householdKey, entityId, entityType);\r\n \r\n // Constant-time comparison\r\n if (derivedKey.length !== expectedKey.length) {\r\n return false;\r\n }\r\n \r\n let result = 0;\r\n for (let i = 0; i < derivedKey.length; i++) {\r\n result |= derivedKey[i] ^ expectedKey[i];\r\n }\r\n \r\n return result === 0;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n","/**\r\n * Entity Encryption Module\r\n * \r\n * This module handles the encryption and decryption of entity data using\r\n * AES-256-GCM authenticated encryption.\r\n * \r\n * Entity encryption flow:\r\n * 1. Derive entity-specific key from household key (HKDF)\r\n * 2. Serialize entity data to JSON\r\n * 3. Encrypt with AES-256-GCM using derived key\r\n * 4. Return ciphertext + IV + metadata\r\n * \r\n * Entity decryption flow:\r\n * 1. Derive same entity key from household key\r\n * 2. Decrypt ciphertext with AES-256-GCM\r\n * 3. Deserialize JSON to entity object\r\n * \r\n * @module encryption/entityEncryption\r\n */\r\n\r\nimport { base64Encode, base64Decode, stringToBytes, bytesToString, generateRandomBytes } from './utils';\r\nimport { deriveEntityKey, importEntityKey } from './entityKeys';\r\nimport type { EntityType, EncryptedData, EncryptedEntity, HouseholdKeyType } from './types';\r\n\r\n/**\r\n * IV size for AES-GCM (96 bits / 12 bytes)\r\n */\r\nexport const IV_SIZE = 12;\r\n\r\n/**\r\n * Auth tag size for AES-GCM (128 bits / 16 bytes)\r\n */\r\nexport const AUTH_TAG_SIZE = 16;\r\n\r\n/**\r\n * Pack encrypted data into blob format: [1-byte version][12-byte IV][ciphertext][16-byte auth tag]\r\n * Note: AES-GCM already includes the auth tag in the ciphertext, so we just prepend version + IV\r\n */\r\nexport function packEncryptedBlob(version: number, iv: Uint8Array, ciphertext: Uint8Array): string {\r\n const blob = new Uint8Array(1 + iv.length + ciphertext.length);\r\n blob[0] = version;\r\n blob.set(iv, 1);\r\n blob.set(ciphertext, 1 + iv.length);\r\n return base64Encode(blob);\r\n}\r\n\r\n/**\r\n * Unpack encrypted blob into version, IV, and ciphertext\r\n * Format: [1-byte version][12-byte IV][ciphertext][16-byte auth tag]\r\n */\r\nexport function unpackEncryptedBlob(packedBlob: string): { version: number; iv: Uint8Array; ciphertext: Uint8Array } {\r\n const blob = base64Decode(packedBlob);\r\n \r\n if (blob.length < 1 + IV_SIZE + AUTH_TAG_SIZE) {\r\n throw new Error('Invalid encrypted blob: too short');\r\n }\r\n \r\n const version = blob[0];\r\n const iv = blob.slice(1, 1 + IV_SIZE);\r\n const ciphertext = blob.slice(1 + IV_SIZE);\r\n \r\n return { version, iv, ciphertext };\r\n}\r\n\r\n/**\r\n * Options for entity encryption\r\n */\r\nexport interface EncryptEntityOptions {\r\n /** Additional authenticated data (not encrypted, but integrity-protected) */\r\n additionalData?: Uint8Array;\r\n}\r\n\r\n/**\r\n * Options for decrypting entity data\r\n */\r\nexport interface DecryptEntityOptions {\r\n /** Additional authenticated data (must match what was used during encryption) */\r\n additionalData?: Uint8Array;\r\n \r\n /** Expected entity type (for validation) */\r\n expectedType?: EntityType;\r\n \r\n /** Pre-derived entity key (skips re-derivation for performance) */\r\n entityKey?: Uint8Array;\r\n}\r\n\r\n/**\r\n * Encrypts entity data with a derived entity-specific key\r\n * \r\n * This function:\r\n * 1. Derives a unique key for this specific entity using HKDF\r\n * 2. Serializes the entity data to JSON\r\n * 3. Encrypts using AES-256-GCM (provides both confidentiality and authenticity)\r\n * 4. Returns the encrypted data with metadata\r\n * \r\n * The encrypted data can only be decrypted with:\r\n * - The same household key\r\n * - The same entity ID and type\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - Unique identifier for the entity\r\n * @param entityType - Type of entity being encrypted\r\n * @param keyType - Which household key type was used (for metadata)\r\n * @param entityData - Entity data to encrypt (will be JSON serialized)\r\n * @param options - Optional encryption settings\r\n * @returns Encrypted entity with metadata\r\n * @throws {Error} If encryption fails or inputs are invalid\r\n * \r\n * @example\r\n * ```ts\r\n * const pet = {\r\n * name: 'Fluffy',\r\n * species: 'cat',\r\n * breed: 'Persian',\r\n * microchipId: '123456789'\r\n * };\r\n * \r\n * const encrypted = await encryptEntity(\r\n * householdKeys.get('general')!, // Household key bytes\r\n * 'a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d', // Entity ID (UUID)\r\n * 'pet', // Entity type\r\n * 'general', // Key type (metadata)\r\n * pet\r\n * );\r\n * \r\n * // Now you have:\r\n * // - encrypted.ciphertext (upload to server)\r\n * // - encrypted.derivedEntityKey (cache for performance)\r\n * \r\n * // Upload to server\r\n * await api.post('/encrypted-entities', {\r\n * id: encrypted.entityId,\r\n * entity_type: encrypted.entityType,\r\n * encrypted_data: encrypted.ciphertext,\r\n * iv: encrypted.iv,\r\n * key_type: encrypted.keyType\r\n * });\r\n * ```\r\n */\r\nexport async function encryptEntity<T = unknown>(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType,\r\n keyType: HouseholdKeyType,\r\n entityData: T,\r\n options: EncryptEntityOptions = {}\r\n): Promise<EncryptedEntity> {\r\n try {\r\n // Derive entity-specific key\r\n const entityKeyBytes = await deriveEntityKey(householdKey, entityId, entityType);\r\n const entityKey = await importEntityKey(entityKeyBytes);\r\n \r\n // Serialize entity data to JSON\r\n const jsonData = JSON.stringify(entityData);\r\n const dataBytes = stringToBytes(jsonData);\r\n \r\n // Generate random IV\r\n const iv = generateRandomBytes(IV_SIZE);\r\n \r\n // Prepare encryption parameters\r\n const encryptParams: AesGcmParams = {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n };\r\n \r\n // Add additional authenticated data if provided\r\n if (options.additionalData) {\r\n encryptParams.additionalData = options.additionalData as BufferSource;\r\n }\r\n \r\n // Encrypt with AES-GCM\r\n const ciphertext = await crypto.subtle.encrypt(\r\n encryptParams,\r\n entityKey,\r\n dataBytes as BufferSource\r\n );\r\n \r\n const result: EncryptedEntity = {\r\n entityId,\r\n entityType,\r\n keyType,\r\n ciphertext: base64Encode(new Uint8Array(ciphertext)),\r\n iv: base64Encode(iv),\r\n encryptedAt: new Date(),\r\n derivedEntityKey: entityKeyBytes\r\n };\r\n \r\n return result;\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to encrypt ${entityType} entity ${entityId}: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Decrypts entity data encrypted with encryptEntity\r\n * \r\n * This function:\r\n * 1. Re-derives the same entity key using HKDF\r\n * 2. Decrypts the ciphertext using AES-256-GCM\r\n * 3. Deserializes the JSON data\r\n * 4. Returns the original entity object\r\n * \r\n * @param householdKey - Raw household key bytes (must be the same as used for encryption)\r\n * @param encrypted - Encrypted entity data\r\n * @param options - Optional decryption settings\r\n * @returns Decrypted entity data\r\n * @throws {Error} If decryption fails (wrong key, corrupted data, tampered data, etc.)\r\n * \r\n * @example\r\n * ```ts\r\n * // Fetch from server\r\n * const encryptedEntity = await api.get(`/encrypted-entities/${entityId}`);\r\n * \r\n * // Decrypt\r\n * const property = await decryptEntity(householdKey, {\r\n * entityId: encryptedEntity.id,\r\n * entityType: encryptedEntity.entity_type,\r\n * keyType: encryptedEntity.key_type,\r\n * ciphertext: encryptedEntity.encrypted_data,\r\n * iv: encryptedEntity.iv,\r\n * encryptedAt: new Date(encryptedEntity.created_at)\r\n * });\r\n * \r\n * console.log(property.address); // '123 Main St'\r\n * ```\r\n */\r\nexport async function decryptEntity<T = unknown>(\r\n householdKey: Uint8Array,\r\n encrypted: EncryptedEntity,\r\n options: DecryptEntityOptions = {}\r\n): Promise<T> {\r\n // Validate entity type if provided\r\n if (options.expectedType && encrypted.entityType !== options.expectedType) {\r\n throw new Error(\r\n `Entity type mismatch: expected ${options.expectedType}, got ${encrypted.entityType}`\r\n );\r\n }\r\n\r\n try {\r\n // Use cached entity key if provided, otherwise derive it\r\n const entityKeyBytes = options.entityKey\r\n ? options.entityKey\r\n : await deriveEntityKey(householdKey, encrypted.entityId, encrypted.entityType);\r\n\r\n const entityKey = await importEntityKey(entityKeyBytes);\r\n \r\n // Decode ciphertext and IV\r\n const ciphertext = base64Decode(encrypted.ciphertext);\r\n const iv = base64Decode(encrypted.iv);\r\n \r\n // Prepare decryption parameters\r\n const decryptParams: AesGcmParams = {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n };\r\n \r\n // Add additional authenticated data if provided\r\n if (options.additionalData) {\r\n decryptParams.additionalData = options.additionalData as BufferSource;\r\n }\r\n \r\n // Decrypt with AES-GCM\r\n const plaintext = await crypto.subtle.decrypt(\r\n decryptParams,\r\n entityKey,\r\n ciphertext as BufferSource\r\n );\r\n \r\n // Convert to string and parse JSON\r\n const jsonData = bytesToString(new Uint8Array(plaintext));\r\n return JSON.parse(jsonData) as T;\r\n } catch (error) {\r\n // Provide helpful error messages\r\n if (error instanceof Error && error.name === 'OperationError') {\r\n throw new Error(\r\n `Failed to decrypt ${encrypted.entityType} entity ${encrypted.entityId}: ` +\r\n 'Authentication failed. This may indicate a wrong household key, corrupted data, or tampered ciphertext.'\r\n );\r\n }\r\n \r\n throw new Error(\r\n `Failed to decrypt ${encrypted.entityType} entity ${encrypted.entityId}: ` +\r\n `${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Batch encrypts multiple entities with the same household key\r\n * \r\n * This is more efficient than encrypting one at a time because:\r\n * - Keys can be derived in parallel\r\n * - Reduces overhead of multiple function calls\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param entities - Array of entities to encrypt\r\n * @returns Array of encrypted entities\r\n * @throws {Error} If any encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * const properties = [\r\n * { id: 'prop-1', address: '123 Main St', value: 500000 },\r\n * { id: 'prop-2', address: '456 Oak Ave', value: 750000 }\r\n * ];\r\n * \r\n * const encrypted = await encryptEntitiesBatch(\r\n * householdKey,\r\n * properties.map(p => ({\r\n * id: p.id,\r\n * type: 'property' as const,\r\n * data: p\r\n * }))\r\n * );\r\n * \r\n * // Upload all at once\r\n * await api.post('/encrypted-entities/batch', encrypted);\r\n * ```\r\n */\r\nexport async function encryptEntitiesBatch<T = unknown>(\r\n householdKey: Uint8Array,\r\n entities: Array<{\r\n id: string;\r\n type: EntityType;\r\n keyType: HouseholdKeyType;\r\n data: T;\r\n options?: EncryptEntityOptions;\r\n }>\r\n): Promise<EncryptedEntity[]> {\r\n // Encrypt all entities in parallel\r\n return await Promise.all(\r\n entities.map((entity) =>\r\n encryptEntity(\r\n householdKey,\r\n entity.id,\r\n entity.type,\r\n entity.keyType,\r\n entity.data,\r\n entity.options\r\n )\r\n )\r\n );\r\n}\r\n\r\n/**\r\n * Batch decrypts multiple entities with the same household key\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param encryptedEntities - Array of encrypted entities\r\n * @param options - Optional decryption settings (applied to all)\r\n * @returns Array of decrypted entity data\r\n * @throws {Error} If any decryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * // Fetch from server\r\n * const encryptedEntities = await api.get('/encrypted-entities', {\r\n * household_id: householdId\r\n * });\r\n * \r\n * // Decrypt all at once\r\n * const properties = await decryptEntitiesBatch(\r\n * householdKey,\r\n * encryptedEntities.map(e => ({\r\n * entityId: e.id,\r\n * entityType: e.entity_type,\r\n * keyType: e.key_type,\r\n * ciphertext: e.encrypted_data,\r\n * iv: e.iv,\r\n * encryptedAt: new Date(e.created_at)\r\n * }))\r\n * );\r\n * ```\r\n */\r\nexport async function decryptEntitiesBatch<T = unknown>(\r\n householdKey: Uint8Array,\r\n encryptedEntities: EncryptedEntity[],\r\n options: DecryptEntityOptions = {}\r\n): Promise<T[]> {\r\n // Decrypt all entities in parallel\r\n return await Promise.all(\r\n encryptedEntities.map((entity) => decryptEntity<T>(householdKey, entity, options))\r\n );\r\n}\r\n\r\n/**\r\n * Re-encrypts an entity with a new household key\r\n * \r\n * This is used during key rotation:\r\n * 1. Decrypt with old household key\r\n * 2. Re-encrypt with new household key\r\n * 3. Upload updated ciphertext\r\n * \r\n * Note: Entity keys are deterministic, so they don't need to change.\r\n * Only the household key changes, which means we need to re-derive the entity key.\r\n * \r\n * @param oldHouseholdKey - Current household key\r\n * @param newHouseholdKey - New household key (after rotation)\r\n * @param encrypted - Currently encrypted entity\r\n * @returns Re-encrypted entity with new ciphertext\r\n * @throws {Error} If decryption or encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * // During key rotation\r\n * const entities = await fetchEntities(householdId);\r\n * \r\n * for (const entity of entities) {\r\n * const reEncrypted = await reEncryptEntity(\r\n * oldHouseholdKey,\r\n * newHouseholdKey,\r\n * entity\r\n * );\r\n * \r\n * await api.patch(`/encrypted-entities/${entity.entityId}`, {\r\n * encrypted_data: reEncrypted.ciphertext,\r\n * iv: reEncrypted.iv\r\n * });\r\n * }\r\n * ```\r\n */\r\nexport async function reEncryptEntity(\r\n oldHouseholdKey: Uint8Array,\r\n newHouseholdKey: Uint8Array,\r\n encrypted: EncryptedEntity\r\n): Promise<EncryptedEntity> {\r\n // Decrypt with old key\r\n const decryptedData = await decryptEntity(oldHouseholdKey, encrypted);\r\n \r\n // Re-encrypt with new key\r\n return await encryptEntity(\r\n newHouseholdKey,\r\n encrypted.entityId,\r\n encrypted.entityType,\r\n encrypted.keyType,\r\n decryptedData\r\n );\r\n}\r\n\r\n/**\r\n * Encrypts just the data payload without entity metadata\r\n * \r\n * This is a lower-level function useful for:\r\n * - Encrypting arbitrary data (not full entities)\r\n * - Custom encryption flows\r\n * - Testing\r\n * \r\n * @param key - CryptoKey for encryption\r\n * @param data - Data to encrypt (will be JSON serialized)\r\n * @param additionalData - Optional authenticated data\r\n * @returns Encrypted data with IV\r\n * @throws {Error} If encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * const entityKey = await deriveAndImportEntityKey(householdKey, id, type);\r\n * const encrypted = await encryptData(entityKey, { secret: 'value' });\r\n * ```\r\n */\r\nexport async function encryptData<T = unknown>(\r\n key: CryptoKey,\r\n data: T,\r\n additionalData?: Uint8Array\r\n): Promise<EncryptedData> {\r\n try {\r\n const jsonData = JSON.stringify(data);\r\n const dataBytes = stringToBytes(jsonData);\r\n const iv = generateRandomBytes(IV_SIZE);\r\n \r\n const encryptParams: AesGcmParams = {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n };\r\n \r\n if (additionalData) {\r\n encryptParams.additionalData = additionalData as BufferSource;\r\n }\r\n \r\n const ciphertext = await crypto.subtle.encrypt(\r\n encryptParams,\r\n key,\r\n dataBytes as BufferSource\r\n );\r\n \r\n return {\r\n ciphertext: base64Encode(new Uint8Array(ciphertext)),\r\n iv: base64Encode(iv)\r\n };\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to encrypt data: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Decrypts just the data payload without entity metadata\r\n * \r\n * @param key - CryptoKey for decryption\r\n * @param encrypted - Encrypted data\r\n * @param additionalData - Optional authenticated data (must match encryption)\r\n * @returns Decrypted data\r\n * @throws {Error} If decryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * const entityKey = await deriveAndImportEntityKey(householdKey, id, type);\r\n * const decrypted = await decryptData(entityKey, encrypted);\r\n * ```\r\n */\r\nexport async function decryptData<T = unknown>(\r\n key: CryptoKey,\r\n encrypted: EncryptedData,\r\n additionalData?: Uint8Array\r\n): Promise<T> {\r\n try {\r\n const ciphertext = base64Decode(encrypted.ciphertext);\r\n const iv = base64Decode(encrypted.iv);\r\n \r\n const decryptParams: AesGcmParams = {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n };\r\n \r\n if (additionalData) {\r\n decryptParams.additionalData = additionalData as BufferSource;\r\n }\r\n \r\n const plaintext = await crypto.subtle.decrypt(\r\n decryptParams,\r\n key,\r\n ciphertext as BufferSource\r\n );\r\n \r\n const jsonData = bytesToString(new Uint8Array(plaintext));\r\n return JSON.parse(jsonData) as T;\r\n } catch (error) {\r\n if (error instanceof Error && error.name === 'OperationError') {\r\n throw new Error('Decryption failed: Authentication error (wrong key or tampered data)');\r\n }\r\n \r\n throw new Error(\r\n `Failed to decrypt data: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n","/**\r\n * Entity Type to Key Type Mapping\r\n *\r\n * Single source of truth for which household/personal key encrypts which entity types.\r\n *\r\n * Key Types:\r\n * - general: Properties, Pets, Vehicles, Access Codes (WiFi, door, gate)\r\n * - financial: Bank Accounts, Tax Docs, Estate Planning\r\n * - health: Medical records, prescriptions, health data\r\n * - subscription: Subscriptions\r\n * - access_code: Access Codes (WiFi, door, gate)\r\n * - identity: Digital identities (Apple ID, Google Account, etc.) - Personal vault\r\n */\r\n\r\nimport type { EntityType, HouseholdKeyType } from './types';\r\n\r\n/**\r\n * Map entity types to their corresponding household key types\r\n */\r\nexport const ENTITY_KEY_TYPE_MAP: Record<EntityType, HouseholdKeyType> = {\r\n // General household items\r\n 'property': 'general',\r\n 'maintenance_task': 'task',\r\n 'pet': 'general',\r\n 'vehicle': 'general',\r\n 'device': 'general',\r\n 'valuable': 'general',\r\n 'valuables': 'general', // Route alias\r\n 'access_code': 'access_code',\r\n 'contact': 'general',\r\n 'service': 'general',\r\n 'document': 'general',\r\n 'travel': 'general',\r\n 'resident': 'general',\r\n 'home_improvement': 'general', // Property improvements\r\n 'vehicle_maintenance': 'general', // Vehicle maintenance history\r\n 'vehicle_service_visit': 'general', // Vehicle service visits\r\n 'pet_vet_visit': 'general', // Pet vet visits (like vehicle_service_visit for vehicles)\r\n 'pet_health': 'general', // Pet health records (simple single records)\r\n 'military_record': 'general', // Military service records\r\n 'education_record': 'general', // Education records (diplomas, transcripts, etc.)\r\n 'credential': 'general', // Credentials (professional licenses, government IDs, etc.)\r\n 'credentials': 'general', // Route alias\r\n 'membership_record': 'general', // Membership records (airline, hotel, retail loyalty programs)\r\n\r\n // Health records (sensitive - requires health key)\r\n 'health_record': 'health',\r\n\r\n // Financial\r\n 'bank_account': 'financial',\r\n 'investment': 'financial',\r\n 'tax_document': 'financial',\r\n 'tax_year': 'financial',\r\n 'taxes': 'financial', // Route alias\r\n 'financial_account': 'financial',\r\n 'financial': 'financial', // Route alias\r\n\r\n // Legal (owner-only)\r\n 'legal': 'legal',\r\n\r\n // Insurance (both names for compatibility)\r\n 'insurance': 'general',\r\n 'insurance_policy': 'general',\r\n\r\n 'subscription': 'subscription',\r\n\r\n // Passwords (shared household credentials)\r\n 'password': 'password',\r\n\r\n // Identity (Personal Vault)\r\n 'identity': 'identity',\r\n\r\n // Calendar Connections (Personal Vault - user's OAuth tokens for calendar sync)\r\n 'calendar_connection': 'identity',\r\n\r\n // Continuity (Personal Vault - shared messages for beneficiaries)\r\n 'continuity': 'continuity',\r\n\r\n // Emergency (household-scoped emergency info, encrypted with general key so all members can decrypt)\r\n 'emergency': 'general',\r\n};\r\n\r\n/**\r\n * Get the household key type for a given entity type\r\n * \r\n * @param entityType - The entity type\r\n * @returns The household key type to use for encryption\r\n * @throws {Error} If entity type is not mapped\r\n */\r\nexport function getKeyTypeForEntity(entityType: EntityType): HouseholdKeyType {\r\n const keyType = ENTITY_KEY_TYPE_MAP[entityType];\r\n \r\n if (!keyType) {\r\n throw new Error(\r\n `No key type mapping found for entity type: ${entityType}. ` +\r\n `Please add it to ENTITY_KEY_TYPE_MAP in entityKeyMapping.ts`\r\n );\r\n }\r\n \r\n return keyType;\r\n}\r\n\r\n/**\r\n * Current crypto version for new encryptions\r\n */\r\nexport const CURRENT_CRYPTO_VERSION = 1;\r\n\r\n/**\r\n * Crypto version descriptions for reference\r\n */\r\nexport const CRYPTO_VERSION_INFO = {\r\n 1: {\r\n algorithm: 'AES-256-GCM',\r\n format: '[1-byte version][12-byte IV][ciphertext][16-byte auth tag]',\r\n description: 'Initial version with IV packed in blob'\r\n }\r\n} as const;\r\n","/**\r\n * Recovery Key Generation and Formatting\r\n *\r\n * Generates and formats recovery keys in the format: ABCD-EFGH-IJKL-MNOP-QRST-UVWX\r\n * Recovery keys are 128-bit random values encoded in base32 (for readability).\r\n *\r\n * Recovery Key + server_wrap_secret = WrapKey (via HKDF)\r\n * WrapKey decrypts user_key_bundles.encrypted_private_key\r\n *\r\n * Used for:\r\n * - New device setup (before PRF enrolled)\r\n * - Non-Apple ecosystem devices\r\n * - Backup recovery if all devices lost\r\n *\r\n * @module encryption/recoveryKey\r\n */\r\n\r\nimport { base64Encode, generateRandomBytes } from './utils';\r\n\r\n/**\r\n * Recovery key size in bytes (128 bits = 16 bytes)\r\n * Provides sufficient entropy while remaining manageable for users\r\n */\r\nexport const RECOVERY_KEY_SIZE = 16;\r\n\r\n/**\r\n * Number of characters per group in formatted recovery key\r\n */\r\nconst GROUP_SIZE = 4;\r\n\r\n/**\r\n * Base32 alphabet (RFC 4648) - uses uppercase letters and digits 2-7\r\n * Excludes 0, 1, 8, 9 to avoid confusion with O, I, B, g\r\n */\r\nconst BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\r\n\r\n/**\r\n * Encode bytes to base32 string\r\n */\r\nfunction encodeBase32(bytes: Uint8Array): string {\r\n let bits = '';\r\n\r\n // Convert bytes to binary string\r\n for (const byte of bytes) {\r\n bits += byte.toString(2).padStart(8, '0');\r\n }\r\n\r\n // Pad to multiple of 5 bits\r\n while (bits.length % 5 !== 0) {\r\n bits += '0';\r\n }\r\n\r\n // Convert 5-bit chunks to base32 characters\r\n let result = '';\r\n for (let i = 0; i < bits.length; i += 5) {\r\n const chunk = bits.substring(i, i + 5);\r\n const index = parseInt(chunk, 2);\r\n result += BASE32_ALPHABET[index];\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Decode base32 string to bytes\r\n */\r\nfunction decodeBase32(base32: string): Uint8Array {\r\n const cleaned = base32.toUpperCase().replace(/[^A-Z2-7]/g, '');\r\n\r\n let bits = '';\r\n\r\n // Convert base32 characters to binary string\r\n for (const char of cleaned) {\r\n const index = BASE32_ALPHABET.indexOf(char);\r\n if (index === -1) {\r\n throw new Error(`Invalid base32 character: ${char}`);\r\n }\r\n bits += index.toString(2).padStart(5, '0');\r\n }\r\n\r\n // Convert binary string to bytes\r\n const bytes: number[] = [];\r\n for (let i = 0; i < bits.length - (bits.length % 8); i += 8) {\r\n const byte = parseInt(bits.substring(i, i + 8), 2);\r\n bytes.push(byte);\r\n }\r\n\r\n return new Uint8Array(bytes);\r\n}\r\n\r\n/**\r\n * Format recovery key with hyphens between groups\r\n *\r\n * Input: \"ABCDEFGHIJKLMNOPQRSTUVWX\"\r\n * Output: \"ABCD-EFGH-IJKL-MNOP-QRST-UVWX\"\r\n */\r\nexport function formatRecoveryKey(base32: string): string {\r\n const groups: string[] = [];\r\n for (let i = 0; i < base32.length; i += GROUP_SIZE) {\r\n groups.push(base32.substring(i, i + GROUP_SIZE));\r\n }\r\n return groups.join('-');\r\n}\r\n\r\n/**\r\n * Convert recovery key bytes to formatted display string\r\n *\r\n * @param bytes - Recovery key bytes (16 bytes)\r\n * @returns Formatted recovery key (e.g., \"ABCD-EFGH-IJKL-MNOP-QRST-UVWX\")\r\n */\r\nexport function bytesToFormattedRecoveryKey(bytes: Uint8Array): string {\r\n const base32 = encodeBase32(bytes);\r\n return formatRecoveryKey(base32);\r\n}\r\n\r\n/**\r\n * Remove formatting from recovery key\r\n */\r\nfunction unformatRecoveryKey(formatted: string): string {\r\n return formatted.toUpperCase().replace(/[^A-Z2-7]/g, '');\r\n}\r\n\r\n/**\r\n * Validate recovery key format\r\n */\r\nexport function validateRecoveryKey(recoveryKey: string): boolean {\r\n try {\r\n const unformatted = unformatRecoveryKey(recoveryKey);\r\n\r\n // Check length (should be ~26 characters for 128-bit key)\r\n if (unformatted.length < 20 || unformatted.length > 30) {\r\n return false;\r\n }\r\n\r\n // Check all characters are valid base32\r\n for (const char of unformatted) {\r\n if (!BASE32_ALPHABET.includes(char)) {\r\n return false;\r\n }\r\n }\r\n\r\n // Try to decode\r\n const bytes = decodeBase32(unformatted);\r\n\r\n // Should decode to approximately RECOVERY_KEY_SIZE bytes\r\n return bytes.length >= RECOVERY_KEY_SIZE - 2 && bytes.length <= RECOVERY_KEY_SIZE + 2;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Generate a new recovery key\r\n *\r\n * @returns Object containing raw bytes and formatted key\r\n *\r\n * @example\r\n * ```ts\r\n * const { bytes, formatted, base64 } = generateRecoveryKey();\r\n * console.log(formatted); // \"ABCD-EFGH-IJKL-MNOP-QRST-UVWX\"\r\n *\r\n * // Show to user for download/printing\r\n * showRecoveryKeyToUser(formatted);\r\n *\r\n * // Use bytes for key derivation\r\n * const wrapKey = await deriveWrapKey(bytes, serverWrapSecret);\r\n * ```\r\n */\r\nexport function generateRecoveryKey(): {\r\n bytes: Uint8Array;\r\n formatted: string;\r\n base64: string;\r\n} {\r\n const bytes = generateRandomBytes(RECOVERY_KEY_SIZE);\r\n const base32 = encodeBase32(bytes);\r\n const formatted = formatRecoveryKey(base32);\r\n const base64 = base64Encode(bytes);\r\n\r\n return {\r\n bytes,\r\n formatted,\r\n base64\r\n };\r\n}\r\n\r\n/**\r\n * Parse recovery key from user input\r\n *\r\n * @param recoveryKey - User-entered recovery key (with or without hyphens)\r\n * @returns Parsed recovery key bytes and formatted version\r\n * @throws {Error} If recovery key is invalid\r\n *\r\n * @example\r\n * ```ts\r\n * const userInput = \"ABCD-EFGH-IJKL-MNOP-QRST-UVWX\";\r\n * const { bytes, formatted } = parseRecoveryKey(userInput);\r\n *\r\n * // Use bytes for key derivation\r\n * const wrapKey = await deriveWrapKey(bytes, serverWrapSecret);\r\n * ```\r\n */\r\nexport function parseRecoveryKey(recoveryKey: string): {\r\n bytes: Uint8Array;\r\n formatted: string;\r\n base64: string;\r\n} {\r\n if (!validateRecoveryKey(recoveryKey)) {\r\n throw new Error('Invalid recovery key format');\r\n }\r\n\r\n const unformatted = unformatRecoveryKey(recoveryKey);\r\n const bytes = decodeBase32(unformatted);\r\n\r\n // Ensure exact size (trim or pad if needed)\r\n const normalizedBytes = new Uint8Array(RECOVERY_KEY_SIZE);\r\n normalizedBytes.set(bytes.slice(0, RECOVERY_KEY_SIZE));\r\n\r\n const base32 = encodeBase32(normalizedBytes);\r\n const formatted = formatRecoveryKey(base32);\r\n const base64 = base64Encode(normalizedBytes);\r\n\r\n return {\r\n bytes: normalizedBytes,\r\n formatted,\r\n base64\r\n };\r\n}\r\n\r\n/**\r\n * Derive WrapKey from Recovery Key + server_wrap_secret\r\n *\r\n * WrapKey = HKDF(recoveryKey, serverWrapSecret, info)\r\n *\r\n * @param recoveryKeyBytes - Recovery key bytes (16 bytes)\r\n * @param serverWrapSecret - Server-side secret (32 bytes)\r\n * @param info - Context info string\r\n * @returns WrapKey for decrypting private key\r\n *\r\n * @example\r\n * ```ts\r\n * // On login with recovery key\r\n * const { bytes } = parseRecoveryKey(userInput);\r\n * const serverSecret = await fetchServerWrapSecret(userId);\r\n * const wrapKey = await deriveWrapKey(bytes, serverSecret);\r\n *\r\n * // Decrypt private key\r\n * const privateKey = await decryptPrivateKey(wrapKey, encryptedPrivateKey);\r\n * ```\r\n */\r\nexport async function deriveWrapKey(\r\n recoveryKeyBytes: Uint8Array,\r\n serverWrapSecret: Uint8Array,\r\n info: string = 'hearthcoo-wrap-key-v1'\r\n): Promise<CryptoKey> {\r\n if (recoveryKeyBytes.length !== RECOVERY_KEY_SIZE) {\r\n throw new Error(`Recovery key must be ${RECOVERY_KEY_SIZE} bytes`);\r\n }\r\n\r\n if (serverWrapSecret.length !== 32) {\r\n throw new Error('Server wrap secret must be 32 bytes');\r\n }\r\n\r\n // Import recovery key as key material\r\n const recoveryKeyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n recoveryKeyBytes as BufferSource,\r\n 'HKDF',\r\n false,\r\n ['deriveKey']\r\n );\r\n\r\n // Derive WrapKey using HKDF\r\n const wrapKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: serverWrapSecret as BufferSource,\r\n info: new TextEncoder().encode(info)\r\n },\r\n recoveryKeyMaterial,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false,\r\n ['encrypt', 'decrypt']\r\n );\r\n\r\n return wrapKey;\r\n}\r\n","/**\r\n * Key Type Constants\r\n *\r\n * Single source of truth for all key type definitions in the application.\r\n */\r\n\r\n/**\r\n * Default algorithm for key bundles (ECDH with P-521 curve).\r\n */\r\nexport const DEFAULT_KEY_BUNDLE_ALG = 'ECDH-P-521'\r\n\r\n/**\r\n * Supported key bundle algorithms.\r\n */\r\nexport type KeyBundleAlgorithm = 'ECDH-ES' | 'RSA-OAEP' | 'ECDH-P-521'\r\n\r\n/**\r\n * Household key types that should exist for every household.\r\n * These are category-level keys that encrypt different types of household data.\r\n */\r\nexport const HOUSEHOLD_KEY_TYPES = ['general', 'financial', 'health', 'legal', 'task', 'subscription', 'access_code', 'password', 'sharing'] as const\r\nexport type HouseholdKeyType = typeof HOUSEHOLD_KEY_TYPES[number]\r\n\r\n/**\r\n * Personal vault key types that should exist for every user.\r\n * These are category-level keys for personal (non-household) data.\r\n */\r\nexport const PERSONAL_KEY_TYPES = ['identity', 'personal'] as const\r\nexport type PersonalKeyType = typeof PERSONAL_KEY_TYPES[number]\r\n\r\n/**\r\n * All possible key types (union of household and personal).\r\n */\r\nexport type AllKeyTypes = HouseholdKeyType | PersonalKeyType\r\n\r\n/**\r\n * Get which household key types a role should have access to by default.\r\n *\r\n * @param role - The household member role (owner, member, executor)\r\n * @returns Array of key types the role should have access to by default\r\n */\r\nexport function getKeyTypesForRole(role: string | undefined | null): HouseholdKeyType[] {\r\n if (!role) return ['general', 'task']\r\n switch (role.toLowerCase()) {\r\n case 'owner':\r\n return ['general', 'financial', 'health', 'legal', 'task', 'subscription', 'access_code', 'password', 'sharing']\r\n case 'member':\r\n return ['general', 'task', 'subscription', 'access_code']\r\n case 'executor':\r\n // Executors have no default key access (can be customized during invitation)\r\n return []\r\n default:\r\n return ['general', 'task']\r\n }\r\n}\r\n\r\n/**\r\n * Friendly names and descriptions for key types.\r\n * Description maps to navigation items in the left sidebar for clarity.\r\n */\r\nexport const KEY_TYPE_INFO: Record<HouseholdKeyType, { name: string; description: string }> = {\r\n general: { name: 'General', description: 'Properties, Pets, Vehicles, Contacts, Insurance, Devices, Valuables, Services' },\r\n financial: { name: 'Financial', description: 'Financial Accounts, Taxes' },\r\n health: { name: 'Health', description: 'Medical Records, Prescriptions' },\r\n legal: { name: 'Legal', description: 'Legal Documents' },\r\n task: { name: 'Tasks', description: 'Maintenance Tasks' },\r\n subscription: { name: 'Subscriptions', description: 'Subscriptions' },\r\n access_code: { name: 'Access Codes', description: 'Access Codes' },\r\n password: { name: 'Passwords', description: 'Passwords' },\r\n sharing: { name: 'Sharing', description: 'Share Packages' }\r\n}\r\n","/**\r\n * Entity Options\r\n *\r\n * Central definitions for all dropdown/select options used in forms.\r\n * Import these in both web and mobile to ensure consistency.\r\n */\r\n\r\n// =============================================================================\r\n// Person/Resident Entity Type Options\r\n// =============================================================================\r\n\r\nexport const PERSON_ENTITY_TYPES = ['individual', 'trust', 'llc'] as const\r\nexport type PersonEntityType = typeof PERSON_ENTITY_TYPES[number]\r\n\r\nexport const PERSON_ENTITY_TYPE_OPTIONS = [\r\n { label: 'Individual', value: 'individual' },\r\n { label: 'Trust', value: 'trust' },\r\n { label: 'LLC', value: 'llc' },\r\n] as const\r\n\r\n/**\r\n * Maps person entity types to Lucide icon names\r\n */\r\nexport const PERSON_ENTITY_TYPE_ICONS: Record<PersonEntityType, string> = {\r\n individual: 'User',\r\n trust: 'Shield',\r\n llc: 'Building2',\r\n}\r\n\r\n// =============================================================================\r\n// Pet Options\r\n// =============================================================================\r\n\r\nexport const PET_SPECIES = ['dog', 'cat', 'bird', 'fish', 'reptile', 'small_mammal', 'rabbit', 'hamster', 'guinea_pig', 'other'] as const\r\nexport type PetSpecies = typeof PET_SPECIES[number]\r\n\r\nexport const PET_SPECIES_OPTIONS = [\r\n { label: 'Dog', value: 'dog' },\r\n { label: 'Cat', value: 'cat' },\r\n { label: 'Bird', value: 'bird' },\r\n { label: 'Fish', value: 'fish' },\r\n { label: 'Reptile', value: 'reptile' },\r\n { label: 'Small Mammal', value: 'small_mammal' },\r\n { label: 'Rabbit', value: 'rabbit' },\r\n { label: 'Hamster', value: 'hamster' },\r\n { label: 'Guinea Pig', value: 'guinea_pig' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const PET_SEXES = ['male', 'female', 'unknown'] as const\r\nexport type PetSex = typeof PET_SEXES[number]\r\n\r\nexport const PET_SEX_OPTIONS = [\r\n { label: 'Male', value: 'male' },\r\n { label: 'Female', value: 'female' },\r\n { label: 'Unknown', value: 'unknown' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Vehicle Options\r\n// =============================================================================\r\n\r\nexport const VEHICLE_TYPES = ['car', 'truck', 'suv', 'motorcycle', 'boat', 'rv', 'bicycle', 'other'] as const\r\nexport type VehicleType = typeof VEHICLE_TYPES[number]\r\n\r\nexport const VEHICLE_TYPE_OPTIONS = [\r\n { label: 'Car', value: 'car' },\r\n { label: 'Truck', value: 'truck' },\r\n { label: 'SUV', value: 'suv' },\r\n { label: 'Motorcycle', value: 'motorcycle' },\r\n { label: 'Boat', value: 'boat' },\r\n { label: 'RV', value: 'rv' },\r\n { label: 'Bicycle', value: 'bicycle' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const VEHICLE_STATUSES = ['owned', 'leased', 'financed', 'sold', 'other'] as const\r\nexport type VehicleStatus = typeof VEHICLE_STATUSES[number]\r\n\r\nexport const VEHICLE_STATUS_OPTIONS = [\r\n { label: 'Owned', value: 'owned' },\r\n { label: 'Leased', value: 'leased' },\r\n { label: 'Financed', value: 'financed' },\r\n { label: 'Sold', value: 'sold' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const VEHICLE_DOCUMENT_TYPES = ['registration', 'title', 'lease', 'bill_of_sale', 'loan', 'other'] as const\r\nexport type VehicleDocumentType = typeof VEHICLE_DOCUMENT_TYPES[number]\r\n\r\nexport const VEHICLE_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'Registration', value: 'registration' },\r\n { label: 'Title', value: 'title' },\r\n { label: 'Lease Agreement', value: 'lease' },\r\n { label: 'Bill of Sale', value: 'bill_of_sale' },\r\n { label: 'Loan Documents', value: 'loan' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps vehicle document types to Lucide icon names\r\n */\r\nexport const VEHICLE_DOCUMENT_TYPE_ICONS: Record<VehicleDocumentType, string> = {\r\n registration: 'FileCheck',\r\n title: 'ScrollText',\r\n lease: 'FileSignature',\r\n bill_of_sale: 'Receipt',\r\n loan: 'FileText',\r\n other: 'File',\r\n}\r\n\r\n// =============================================================================\r\n// Contact Options\r\n// =============================================================================\r\n\r\nexport const CONTACT_TYPES = [\r\n 'accountant', 'babysitter', 'car_dealership', 'chiropractor', 'contractor', 'dentist', 'doctor',\r\n 'electrician', 'emergency', 'financial_advisor', 'friend_family', 'handyman',\r\n 'home_organizer', 'house_cleaner', 'hvac_technician', 'insurance_agent', 'landscaper', 'lawyer',\r\n 'mechanic', 'notary', 'pest_control', 'pet_service', 'pharmacist', 'physical_therapist',\r\n 'plumber', 'pool_service', 'professional', 'real_estate_agent', 'service_provider',\r\n 'therapist', 'tutor', 'vendor', 'vet_clinic', 'veterinarian', 'other'\r\n] as const\r\nexport type ContactType = typeof CONTACT_TYPES[number]\r\n\r\nexport const CONTACT_TYPE_OPTIONS = [\r\n { label: 'Accountant', value: 'accountant' },\r\n { label: 'Babysitter/Nanny', value: 'babysitter' },\r\n { label: 'Car Dealership', value: 'car_dealership' },\r\n { label: 'Chiropractor', value: 'chiropractor' },\r\n { label: 'Contractor', value: 'contractor' },\r\n { label: 'Dentist', value: 'dentist' },\r\n { label: 'Doctor', value: 'doctor' },\r\n { label: 'Electrician', value: 'electrician' },\r\n { label: 'Emergency', value: 'emergency' },\r\n { label: 'Financial Advisor', value: 'financial_advisor' },\r\n { label: 'Friend/Family', value: 'friend_family' },\r\n { label: 'Handyman', value: 'handyman' },\r\n { label: 'Home Organizer', value: 'home_organizer' },\r\n { label: 'House Cleaner', value: 'house_cleaner' },\r\n { label: 'HVAC Technician', value: 'hvac_technician' },\r\n { label: 'Insurance Agent', value: 'insurance_agent' },\r\n { label: 'Landscaper/Gardener', value: 'landscaper' },\r\n { label: 'Lawyer', value: 'lawyer' },\r\n { label: 'Mechanic', value: 'mechanic' },\r\n { label: 'Notary', value: 'notary' },\r\n { label: 'Pest Control', value: 'pest_control' },\r\n { label: 'Pet Service', value: 'pet_service' },\r\n { label: 'Pharmacist', value: 'pharmacist' },\r\n { label: 'Physical Therapist', value: 'physical_therapist' },\r\n { label: 'Plumber', value: 'plumber' },\r\n { label: 'Pool Service', value: 'pool_service' },\r\n { label: 'Professional', value: 'professional' },\r\n { label: 'Real Estate Agent', value: 'real_estate_agent' },\r\n { label: 'Service Provider', value: 'service_provider' },\r\n { label: 'Therapist/Counselor', value: 'therapist' },\r\n { label: 'Tutor', value: 'tutor' },\r\n { label: 'Vendor', value: 'vendor' },\r\n { label: 'Vet Clinic/Hospital', value: 'vet_clinic' },\r\n { label: 'Veterinarian', value: 'veterinarian' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Access Code Options\r\n// =============================================================================\r\n\r\nexport const ACCESS_CODE_TYPES = ['garage', 'alarm', 'gate', 'door_lock', 'door', 'safe', 'wifi', 'elevator', 'mailbox', 'storage', 'other'] as const\r\nexport type AccessCodeType = typeof ACCESS_CODE_TYPES[number]\r\n\r\nexport const ACCESS_CODE_TYPE_OPTIONS = [\r\n { label: 'Garage', value: 'garage' },\r\n { label: 'Alarm', value: 'alarm' },\r\n { label: 'Gate', value: 'gate' },\r\n { label: 'Door Lock', value: 'door_lock' },\r\n { label: 'Door', value: 'door' },\r\n { label: 'Safe', value: 'safe' },\r\n { label: 'WiFi', value: 'wifi' },\r\n { label: 'Elevator', value: 'elevator' },\r\n { label: 'Mailbox', value: 'mailbox' },\r\n { label: 'Storage', value: 'storage' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Insurance Options\r\n// =============================================================================\r\n\r\nexport const INSURANCE_TYPES = ['home', 'auto', 'umbrella', 'life', 'health', 'pet', 'collection', 'other'] as const\r\nexport type InsuranceType = typeof INSURANCE_TYPES[number]\r\n\r\nexport const INSURANCE_TYPE_OPTIONS = [\r\n { label: 'Home', value: 'home' },\r\n { label: 'Auto', value: 'auto' },\r\n { label: 'Umbrella', value: 'umbrella' },\r\n { label: 'Life', value: 'life' },\r\n { label: 'Health', value: 'health' },\r\n { label: 'Pet', value: 'pet' },\r\n { label: 'Collection', value: 'collection' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Service Options\r\n// =============================================================================\r\n\r\nexport const SERVICE_TYPES = ['cleaning', 'gardening', 'lawn_care', 'pool', 'pest_control', 'hvac', 'plumbing', 'electrical', 'security', 'other'] as const\r\nexport type ServiceType = typeof SERVICE_TYPES[number]\r\n\r\nexport const SERVICE_TYPE_OPTIONS = [\r\n { label: 'Cleaning', value: 'cleaning' },\r\n { label: 'Gardening', value: 'gardening' },\r\n { label: 'Lawn Care', value: 'lawn_care' },\r\n { label: 'Pool', value: 'pool' },\r\n { label: 'Pest Control', value: 'pest_control' },\r\n { label: 'HVAC', value: 'hvac' },\r\n { label: 'Plumbing', value: 'plumbing' },\r\n { label: 'Electrical', value: 'electrical' },\r\n { label: 'Security', value: 'security' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Schedule/Frequency Options\r\n// =============================================================================\r\n\r\nexport const SCHEDULE_FREQUENCIES = ['daily', 'weekly', 'biweekly', 'monthly', 'bimonthly', 'quarterly', 'yearly', 'one_time'] as const\r\nexport type ScheduleFrequency = typeof SCHEDULE_FREQUENCIES[number]\r\n\r\nexport const SCHEDULE_FREQUENCY_OPTIONS = [\r\n { label: 'Daily', value: 'daily' },\r\n { label: 'Weekly', value: 'weekly' },\r\n { label: 'Biweekly', value: 'biweekly' },\r\n { label: 'Monthly', value: 'monthly' },\r\n { label: 'Every 2 Months', value: 'bimonthly' },\r\n { label: 'Quarterly', value: 'quarterly' },\r\n { label: 'Yearly', value: 'yearly' },\r\n { label: 'One-Time', value: 'one_time' },\r\n] as const\r\n\r\nexport const SCHEDULE_STATUSES = ['active', 'paused', 'completed', 'cancelled'] as const\r\nexport type ScheduleStatus = typeof SCHEDULE_STATUSES[number]\r\n\r\nexport const SCHEDULE_STATUS_OPTIONS = [\r\n { label: 'Active', value: 'active' },\r\n { label: 'Paused', value: 'paused' },\r\n { label: 'Completed', value: 'completed' },\r\n { label: 'Cancelled', value: 'cancelled' },\r\n] as const\r\n\r\nexport const DAYS_OF_WEEK = [\r\n { label: 'Sunday', value: 0 },\r\n { label: 'Monday', value: 1 },\r\n { label: 'Tuesday', value: 2 },\r\n { label: 'Wednesday', value: 3 },\r\n { label: 'Thursday', value: 4 },\r\n { label: 'Friday', value: 5 },\r\n { label: 'Saturday', value: 6 },\r\n] as const\r\n\r\n// =============================================================================\r\n// Subscription Options\r\n// =============================================================================\r\n\r\nexport const SUBSCRIPTION_TYPES = ['streaming', 'software', 'membership', 'utility', 'other'] as const\r\nexport type SubscriptionType = typeof SUBSCRIPTION_TYPES[number]\r\n\r\nexport const SUBSCRIPTION_TYPE_OPTIONS = [\r\n { label: 'Streaming', value: 'streaming' },\r\n { label: 'Software', value: 'software' },\r\n { label: 'Membership', value: 'membership' },\r\n { label: 'Utility', value: 'utility' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const BILLING_CYCLES = ['monthly', 'quarterly', 'annual'] as const\r\nexport type BillingCycle = typeof BILLING_CYCLES[number]\r\n\r\nexport const BILLING_CYCLE_OPTIONS = [\r\n { label: 'Monthly', value: 'monthly' },\r\n { label: 'Quarterly', value: 'quarterly' },\r\n { label: 'Annual', value: 'annual' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Maintenance Task Options\r\n// =============================================================================\r\n\r\nexport const MAINTENANCE_TASK_TYPES = ['scheduled', 'one_time'] as const\r\nexport type MaintenanceTaskType = typeof MAINTENANCE_TASK_TYPES[number]\r\n\r\nexport const MAINTENANCE_TASK_TYPE_OPTIONS = [\r\n { label: 'Scheduled (Recurring)', value: 'scheduled' },\r\n { label: 'One-Time', value: 'one_time' },\r\n] as const\r\n\r\nexport const MAINTENANCE_CATEGORIES = ['hvac', 'plumbing', 'electrical', 'appliance', 'appliances', 'exterior', 'interior', 'vehicle', 'safety', 'roof', 'pool', 'landscaping', 'solar', 'other'] as const\r\nexport type MaintenanceCategory = typeof MAINTENANCE_CATEGORIES[number]\r\n\r\nexport const MAINTENANCE_CATEGORY_OPTIONS = [\r\n { label: 'HVAC', value: 'hvac' },\r\n { label: 'Plumbing', value: 'plumbing' },\r\n { label: 'Electrical', value: 'electrical' },\r\n { label: 'Appliance', value: 'appliance' },\r\n { label: 'Appliances', value: 'appliances' },\r\n { label: 'Exterior', value: 'exterior' },\r\n { label: 'Interior', value: 'interior' },\r\n { label: 'Vehicle', value: 'vehicle' },\r\n { label: 'Safety', value: 'safety' },\r\n { label: 'Roof', value: 'roof' },\r\n { label: 'Pool', value: 'pool' },\r\n { label: 'Landscaping', value: 'landscaping' },\r\n { label: 'Solar', value: 'solar' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const PRIORITY_LEVELS = ['low', 'medium', 'high'] as const\r\nexport type PriorityLevel = typeof PRIORITY_LEVELS[number]\r\n\r\nexport const PRIORITY_OPTIONS = [\r\n { label: 'Low', value: 'low' },\r\n { label: 'Medium', value: 'medium' },\r\n { label: 'High', value: 'high' },\r\n] as const\r\n\r\nexport const TASK_STATUSES = ['pending', 'in_progress', 'completed'] as const\r\nexport type TaskStatus = typeof TASK_STATUSES[number]\r\n\r\nexport const TASK_STATUS_OPTIONS = [\r\n { label: 'Pending', value: 'pending' },\r\n { label: 'In Progress', value: 'in_progress' },\r\n { label: 'Completed', value: 'completed' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Password Category Options\r\n// =============================================================================\r\n\r\nexport const PASSWORD_CATEGORIES = [\r\n 'identity', 'social', 'financial', 'shopping', 'streaming', 'gaming', 'work', 'email',\r\n 'utilities', 'government', 'healthcare', 'education', 'travel', 'device', 'fitness', 'dev_tools', 'other'\r\n] as const\r\nexport type PasswordCategory = typeof PASSWORD_CATEGORIES[number]\r\n\r\nexport const PASSWORD_CATEGORY_OPTIONS = [\r\n { label: 'Identity', value: 'identity' },\r\n { label: 'Social Media', value: 'social' },\r\n { label: 'Financial', value: 'financial' },\r\n { label: 'Shopping', value: 'shopping' },\r\n { label: 'Streaming', value: 'streaming' },\r\n { label: 'Gaming', value: 'gaming' },\r\n { label: 'Work', value: 'work' },\r\n { label: 'Email', value: 'email' },\r\n { label: 'Utilities', value: 'utilities' },\r\n { label: 'Government', value: 'government' },\r\n { label: 'Healthcare', value: 'healthcare' },\r\n { label: 'Education', value: 'education' },\r\n { label: 'Travel', value: 'travel' },\r\n { label: 'Device', value: 'device' },\r\n { label: 'Fitness', value: 'fitness' },\r\n { label: 'Dev Tools', value: 'dev_tools' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps password categories to Lucide icon names\r\n */\r\nexport const PASSWORD_CATEGORY_ICONS: Record<PasswordCategory, string> = {\r\n identity: 'Fingerprint',\r\n social: 'Users',\r\n financial: 'Landmark',\r\n shopping: 'ShoppingCart',\r\n streaming: 'Play',\r\n gaming: 'Gamepad2',\r\n work: 'Briefcase',\r\n email: 'Mail',\r\n utilities: 'Plug',\r\n government: 'Building',\r\n healthcare: 'HeartPulse',\r\n education: 'GraduationCap',\r\n travel: 'Plane',\r\n device: 'Router',\r\n fitness: 'Dumbbell',\r\n dev_tools: 'Github',\r\n other: 'Key',\r\n}\r\n\r\n// =============================================================================\r\n// Digital Identity Options\r\n// =============================================================================\r\n\r\nexport const DIGITAL_IDENTITY_TYPES = ['website', 'app', 'email', 'financial', 'social', 'other'] as const\r\nexport type DigitalIdentityType = typeof DIGITAL_IDENTITY_TYPES[number]\r\n\r\nexport const DIGITAL_IDENTITY_TYPE_OPTIONS = [\r\n { label: 'Website', value: 'website' },\r\n { label: 'App', value: 'app' },\r\n { label: 'Email', value: 'email' },\r\n { label: 'Financial', value: 'financial' },\r\n { label: 'Social', value: 'social' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Icon Mappings\r\n// =============================================================================\r\n\r\n/**\r\n * Maps contact types to Lucide icon names\r\n */\r\nexport const CONTACT_TYPE_ICONS: Record<ContactType, string> = {\r\n accountant: 'Calculator',\r\n babysitter: 'Baby',\r\n car_dealership: 'Car',\r\n chiropractor: 'Bone',\r\n contractor: 'HardHat',\r\n dentist: 'Smile',\r\n doctor: 'Stethoscope',\r\n electrician: 'Zap',\r\n emergency: 'AlertTriangle',\r\n financial_advisor: 'Landmark',\r\n friend_family: 'User',\r\n handyman: 'Hammer',\r\n home_organizer: 'LayoutGrid',\r\n house_cleaner: 'Sparkles',\r\n hvac_technician: 'Thermometer',\r\n insurance_agent: 'Shield',\r\n landscaper: 'TreeDeciduous',\r\n lawyer: 'Scale',\r\n mechanic: 'Wrench',\r\n notary: 'Stamp',\r\n pest_control: 'Bug',\r\n pet_service: 'PawPrint',\r\n pharmacist: 'Pill',\r\n physical_therapist: 'Activity',\r\n plumber: 'Droplet',\r\n pool_service: 'Waves',\r\n professional: 'User',\r\n real_estate_agent: 'Home',\r\n service_provider: 'Wrench',\r\n therapist: 'Brain',\r\n tutor: 'GraduationCap',\r\n vendor: 'Building2',\r\n vet_clinic: 'Building2',\r\n veterinarian: 'Heart',\r\n other: 'UserCircle',\r\n}\r\n\r\n/**\r\n * Maps contact types to colors for UI display\r\n */\r\nexport const CONTACT_TYPE_COLORS: Record<ContactType, string> = {\r\n accountant: 'blue',\r\n babysitter: 'pink',\r\n car_dealership: 'slate',\r\n chiropractor: 'teal',\r\n contractor: 'orange',\r\n dentist: 'cyan',\r\n doctor: 'green',\r\n electrician: 'yellow',\r\n emergency: 'red',\r\n financial_advisor: 'indigo',\r\n friend_family: 'purple',\r\n handyman: 'amber',\r\n home_organizer: 'teal',\r\n house_cleaner: 'lime',\r\n hvac_technician: 'orange',\r\n insurance_agent: 'blue',\r\n landscaper: 'green',\r\n lawyer: 'slate',\r\n mechanic: 'gray',\r\n notary: 'violet',\r\n pest_control: 'amber',\r\n pet_service: 'pink',\r\n pharmacist: 'teal',\r\n physical_therapist: 'cyan',\r\n plumber: 'blue',\r\n pool_service: 'sky',\r\n professional: 'slate',\r\n real_estate_agent: 'emerald',\r\n service_provider: 'gray',\r\n therapist: 'purple',\r\n tutor: 'indigo',\r\n vendor: 'slate',\r\n vet_clinic: 'green',\r\n veterinarian: 'green',\r\n other: 'gray',\r\n}\r\n\r\n/**\r\n * Maps pet species to Lucide icon names\r\n */\r\nexport const PET_SPECIES_ICONS: Record<PetSpecies, string> = {\r\n dog: 'Dog',\r\n cat: 'Cat',\r\n bird: 'Bird',\r\n fish: 'Fish',\r\n reptile: 'Bug',\r\n small_mammal: 'Rabbit',\r\n rabbit: 'Rabbit',\r\n hamster: 'Rat',\r\n guinea_pig: 'Rat',\r\n other: 'PawPrint',\r\n}\r\n\r\n/**\r\n * Maps vehicle types to Lucide icon names\r\n */\r\nexport const VEHICLE_TYPE_ICONS: Record<VehicleType, string> = {\r\n car: 'Car',\r\n truck: 'Truck',\r\n suv: 'CarFront',\r\n motorcycle: 'Bike',\r\n boat: 'Ship',\r\n rv: 'Caravan',\r\n bicycle: 'Bike',\r\n other: 'CircleDot',\r\n}\r\n\r\n/**\r\n * Maps service types to Lucide icon names\r\n */\r\nexport const SERVICE_TYPE_ICONS: Record<ServiceType, string> = {\r\n cleaning: 'Sparkles',\r\n gardening: 'Flower2',\r\n lawn_care: 'TreeDeciduous',\r\n pool: 'Waves',\r\n pest_control: 'Bug',\r\n hvac: 'Wind',\r\n plumbing: 'Droplet',\r\n electrical: 'Zap',\r\n security: 'Shield',\r\n other: 'Wrench',\r\n}\r\n\r\n/**\r\n * Maps access code types to Lucide icon names\r\n */\r\nexport const ACCESS_CODE_TYPE_ICONS: Record<AccessCodeType, string> = {\r\n garage: 'Warehouse',\r\n alarm: 'Bell',\r\n gate: 'DoorOpen',\r\n door_lock: 'Lock',\r\n door: 'DoorClosed',\r\n safe: 'Lock',\r\n wifi: 'Wifi',\r\n elevator: 'ArrowUpDown',\r\n mailbox: 'Mail',\r\n storage: 'Archive',\r\n other: 'Key',\r\n}\r\n\r\n/**\r\n * Maps insurance types to Lucide icon names\r\n */\r\nexport const INSURANCE_TYPE_ICONS: Record<InsuranceType, string> = {\r\n home: 'Home',\r\n auto: 'Car',\r\n umbrella: 'Umbrella',\r\n life: 'Heart',\r\n health: 'Activity',\r\n pet: 'PawPrint',\r\n collection: 'Gem',\r\n other: 'Shield',\r\n}\r\n\r\n/**\r\n * Maps subscription types to Lucide icon names\r\n */\r\nexport const SUBSCRIPTION_TYPE_ICONS: Record<SubscriptionType, string> = {\r\n streaming: 'Play',\r\n software: 'Laptop',\r\n membership: 'CreditCard',\r\n utility: 'Plug',\r\n other: 'Receipt',\r\n}\r\n\r\n/**\r\n * Maps maintenance categories to Lucide icon names\r\n */\r\nexport const MAINTENANCE_CATEGORY_ICONS: Record<MaintenanceCategory, string> = {\r\n hvac: 'Wind',\r\n plumbing: 'Droplet',\r\n electrical: 'Zap',\r\n appliance: 'Microwave',\r\n appliances: 'Microwave',\r\n exterior: 'TreeDeciduous',\r\n interior: 'Sofa',\r\n vehicle: 'Car',\r\n safety: 'ShieldCheck',\r\n roof: 'Home',\r\n pool: 'Waves',\r\n landscaping: 'Leaf',\r\n solar: 'Sun',\r\n other: 'Wrench',\r\n}\r\n\r\n/**\r\n * Maps digital identity types to Lucide icon names\r\n */\r\nexport const DIGITAL_IDENTITY_TYPE_ICONS: Record<DigitalIdentityType, string> = {\r\n website: 'Globe',\r\n app: 'Smartphone',\r\n email: 'Mail',\r\n financial: 'Landmark',\r\n social: 'Users',\r\n other: 'Key',\r\n}\r\n\r\n// =============================================================================\r\n// Financial Account Options\r\n// =============================================================================\r\n\r\nexport const FINANCIAL_ACCOUNT_TYPES = ['bank', 'credit_card', 'mortgage', 'loan', 'investment', 'retirement', 'insurance', 'crypto', 'hsa', 'education_529', 'custodial', 'daf', 'fund', 'other'] as const\r\nexport type FinancialAccountType = typeof FINANCIAL_ACCOUNT_TYPES[number]\r\n\r\nexport const FINANCIAL_ACCOUNT_TYPE_OPTIONS = [\r\n { label: 'Bank Account', value: 'bank' },\r\n { label: 'Credit Card', value: 'credit_card' },\r\n { label: 'Mortgage', value: 'mortgage' },\r\n { label: 'Loan', value: 'loan' },\r\n { label: 'Investment', value: 'investment' },\r\n { label: 'Retirement', value: 'retirement' },\r\n { label: 'Insurance', value: 'insurance' },\r\n { label: 'Crypto', value: 'crypto' },\r\n { label: 'Health Savings (HSA)', value: 'hsa' },\r\n { label: '529 Education Savings', value: 'education_529' },\r\n { label: 'Custodial (UGMA/UTMA)', value: 'custodial' },\r\n { label: 'Donor-Advised Fund (DAF)', value: 'daf' },\r\n { label: 'Fund (LP/PE/VC)', value: 'fund' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// Crypto wallet types (for crypto accounts)\r\nexport const CRYPTO_WALLET_TYPES = ['hardware', 'software', 'exchange', 'paper'] as const\r\nexport type CryptoWalletType = typeof CRYPTO_WALLET_TYPES[number]\r\n\r\nexport const CRYPTO_WALLET_TYPE_OPTIONS = [\r\n { label: 'Hardware Wallet', value: 'hardware' },\r\n { label: 'Software Wallet', value: 'software' },\r\n { label: 'Exchange', value: 'exchange' },\r\n { label: 'Paper Wallet', value: 'paper' },\r\n] as const\r\n\r\n/**\r\n * Maps crypto wallet types to Lucide icon names\r\n */\r\nexport const CRYPTO_WALLET_TYPE_ICONS: Record<CryptoWalletType, string> = {\r\n hardware: 'HardDrive',\r\n software: 'Smartphone',\r\n exchange: 'Building2',\r\n paper: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Legal Document Options\r\n// =============================================================================\r\n\r\n// Personal legal document types (for individuals)\r\nexport const PERSONAL_LEGAL_DOCUMENT_TYPES = [\r\n 'birth_certificate', 'citizenship_certificate', 'divorce_decree',\r\n 'healthcare_directive', 'living_will', 'marriage_certificate',\r\n 'power_of_attorney', 'social_security_card', 'will',\r\n // Shared types\r\n 'property_deed', 'vehicle_title', 'other'\r\n] as const\r\n\r\n// Business/LLC legal document types\r\nexport const LLC_LEGAL_DOCUMENT_TYPES = [\r\n 'llc_formation', 'operating_agreement', 'annual_report',\r\n 'sales_tax_registration', 'business_license', 'registered_agent',\r\n 'dba_certificate', 'business_insurance_certificate',\r\n // Shared types\r\n 'ein_certificate', 'property_deed', 'vehicle_title', 'other'\r\n] as const\r\n\r\n// Trust legal document types\r\nexport const TRUST_LEGAL_DOCUMENT_TYPES = [\r\n 'trust_agreement', 'certificate_of_trust', 'trust_amendment',\r\n 'schedule_of_assets', 'trustee_acceptance', 'trustee_resignation',\r\n 'pour_over_will', 'trust_restatement', 'beneficiary_designation',\r\n // Shared types\r\n 'ein_certificate', 'property_deed', 'vehicle_title', 'other'\r\n] as const\r\n\r\n// Combined type for all legal documents\r\nexport const LEGAL_DOCUMENT_TYPES = [\r\n // Personal\r\n 'birth_certificate', 'citizenship_certificate', 'divorce_decree',\r\n 'healthcare_directive', 'living_will', 'marriage_certificate',\r\n 'power_of_attorney', 'social_security_card', 'will',\r\n // LLC/Business\r\n 'llc_formation', 'operating_agreement', 'annual_report',\r\n 'sales_tax_registration', 'business_license', 'registered_agent',\r\n 'dba_certificate', 'business_insurance_certificate',\r\n // Trust\r\n 'trust_agreement', 'certificate_of_trust', 'trust_amendment',\r\n 'schedule_of_assets', 'trustee_acceptance', 'trustee_resignation',\r\n 'pour_over_will', 'trust_restatement', 'beneficiary_designation',\r\n // Shared\r\n 'ein_certificate', 'property_deed', 'vehicle_title', 'other',\r\n // Legacy (kept for backwards compatibility)\r\n 'trust',\r\n] as const\r\nexport type LegalDocumentType = typeof LEGAL_DOCUMENT_TYPES[number]\r\n\r\nexport const PERSONAL_LEGAL_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'Birth Certificate', value: 'birth_certificate' },\r\n { label: 'Citizenship Certificate', value: 'citizenship_certificate' },\r\n { label: 'Divorce Decree', value: 'divorce_decree' },\r\n { label: 'Healthcare Directive', value: 'healthcare_directive' },\r\n { label: 'Living Will', value: 'living_will' },\r\n { label: 'Marriage Certificate', value: 'marriage_certificate' },\r\n { label: 'Power of Attorney', value: 'power_of_attorney' },\r\n { label: 'Social Security Card', value: 'social_security_card' },\r\n { label: 'Will', value: 'will' },\r\n { label: 'Property Deed', value: 'property_deed' },\r\n { label: 'Vehicle Title', value: 'vehicle_title' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const LLC_LEGAL_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'LLC Formation Certificate', value: 'llc_formation' },\r\n { label: 'Operating Agreement', value: 'operating_agreement' },\r\n { label: 'Annual Report', value: 'annual_report' },\r\n { label: 'Sales Tax Registration', value: 'sales_tax_registration' },\r\n { label: 'Business License', value: 'business_license' },\r\n { label: 'Registered Agent Certificate', value: 'registered_agent' },\r\n { label: 'DBA Certificate', value: 'dba_certificate' },\r\n { label: 'Business Insurance Certificate', value: 'business_insurance_certificate' },\r\n { label: 'EIN Certificate', value: 'ein_certificate' },\r\n { label: 'Property Deed', value: 'property_deed' },\r\n { label: 'Vehicle Title', value: 'vehicle_title' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const TRUST_LEGAL_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'Trust Agreement', value: 'trust_agreement' },\r\n { label: 'Certificate of Trust', value: 'certificate_of_trust' },\r\n { label: 'Trust Amendment', value: 'trust_amendment' },\r\n { label: 'Schedule of Assets', value: 'schedule_of_assets' },\r\n { label: 'Trustee Acceptance', value: 'trustee_acceptance' },\r\n { label: 'Trustee Resignation', value: 'trustee_resignation' },\r\n { label: 'Pour-Over Will', value: 'pour_over_will' },\r\n { label: 'Trust Restatement', value: 'trust_restatement' },\r\n { label: 'Beneficiary Designation', value: 'beneficiary_designation' },\r\n { label: 'EIN Certificate', value: 'ein_certificate' },\r\n { label: 'Property Deed', value: 'property_deed' },\r\n { label: 'Vehicle Title', value: 'vehicle_title' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// Alias for backwards compatibility\r\nexport const BUSINESS_LEGAL_DOCUMENT_TYPE_OPTIONS = LLC_LEGAL_DOCUMENT_TYPE_OPTIONS\r\n\r\n// Combined for backwards compatibility\r\nexport const LEGAL_DOCUMENT_TYPE_OPTIONS = [\r\n ...PERSONAL_LEGAL_DOCUMENT_TYPE_OPTIONS.filter(o => o.value !== 'other'),\r\n ...LLC_LEGAL_DOCUMENT_TYPE_OPTIONS.filter(o => !['property_deed', 'vehicle_title', 'other', 'ein_certificate'].includes(o.value)),\r\n ...TRUST_LEGAL_DOCUMENT_TYPE_OPTIONS.filter(o => !['property_deed', 'vehicle_title', 'other', 'ein_certificate'].includes(o.value)),\r\n { label: 'EIN Certificate', value: 'ein_certificate' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps legal document types to Lucide icon names\r\n */\r\nexport const LEGAL_DOCUMENT_TYPE_ICONS: Record<LegalDocumentType, string> = {\r\n // Personal documents\r\n birth_certificate: 'Baby',\r\n citizenship_certificate: 'Flag',\r\n divorce_decree: 'FileX',\r\n healthcare_directive: 'Stethoscope',\r\n living_will: 'Heart',\r\n marriage_certificate: 'Heart',\r\n power_of_attorney: 'FileSignature',\r\n social_security_card: 'CreditCard',\r\n will: 'ScrollText',\r\n // LLC/Business documents\r\n llc_formation: 'FileCheck',\r\n operating_agreement: 'FileText',\r\n annual_report: 'FileSpreadsheet',\r\n sales_tax_registration: 'Receipt',\r\n business_license: 'BadgeCheck',\r\n registered_agent: 'UserCheck',\r\n dba_certificate: 'FileSignature',\r\n business_insurance_certificate: 'Shield',\r\n // Trust documents\r\n trust_agreement: 'ScrollText',\r\n certificate_of_trust: 'FileCheck',\r\n trust_amendment: 'FilePen',\r\n schedule_of_assets: 'ClipboardList',\r\n trustee_acceptance: 'UserCheck',\r\n trustee_resignation: 'UserMinus',\r\n pour_over_will: 'ScrollText',\r\n trust_restatement: 'FileText',\r\n beneficiary_designation: 'Users',\r\n // Shared documents\r\n ein_certificate: 'Building2',\r\n property_deed: 'Home',\r\n vehicle_title: 'Car',\r\n other: 'FileText',\r\n // Legacy\r\n trust: 'Briefcase',\r\n}\r\n\r\n/**\r\n * Maps financial account types to Lucide icon names\r\n */\r\nexport const FINANCIAL_ACCOUNT_TYPE_ICONS: Record<FinancialAccountType, string> = {\r\n bank: 'Landmark',\r\n credit_card: 'CreditCard',\r\n investment: 'TrendingUp',\r\n retirement: 'PiggyBank',\r\n hsa: 'HeartPulse',\r\n education_529: 'GraduationCap',\r\n custodial: 'Baby',\r\n daf: 'HeartHandshake',\r\n fund: 'Briefcase',\r\n loan: 'Banknote',\r\n mortgage: 'Home',\r\n insurance: 'Shield',\r\n crypto: 'Bitcoin',\r\n other: 'Wallet',\r\n}\r\n\r\n// =============================================================================\r\n// Valuable Options\r\n// =============================================================================\r\n\r\nexport const VALUABLE_CATEGORIES = ['art_other', 'collectible', 'electronics', 'jewelry', 'major_appliance', 'painting', 'photography_equipment', 'sculpture', 'watch', 'other'] as const\r\nexport type ValuableCategory = typeof VALUABLE_CATEGORIES[number]\r\n\r\nexport const VALUABLE_CATEGORY_OPTIONS = [\r\n { label: 'Art', value: 'art_other' },\r\n { label: 'Collectible', value: 'collectible' },\r\n { label: 'Electronics', value: 'electronics' },\r\n { label: 'Jewelry', value: 'jewelry' },\r\n { label: 'Major Appliance', value: 'major_appliance' },\r\n { label: 'Painting', value: 'painting' },\r\n { label: 'Photography Equipment', value: 'photography_equipment' },\r\n { label: 'Sculpture', value: 'sculpture' },\r\n { label: 'Watch', value: 'watch' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const VALUABLE_DOCUMENT_TYPES = ['photo', 'certificate_of_authenticity', 'receipt', 'appraisal'] as const\r\nexport type ValuableDocumentType = typeof VALUABLE_DOCUMENT_TYPES[number]\r\n\r\nexport const VALUABLE_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'Photo', value: 'photo' },\r\n { label: 'Certificate of Authenticity', value: 'certificate_of_authenticity' },\r\n { label: 'Receipt', value: 'receipt' },\r\n { label: 'Appraisal', value: 'appraisal' },\r\n] as const\r\n\r\n/**\r\n * Maps valuable categories to Lucide icon names\r\n */\r\nexport const VALUABLE_CATEGORY_ICONS: Record<ValuableCategory, string> = {\r\n art_other: 'Palette',\r\n collectible: 'Trophy',\r\n electronics: 'Smartphone',\r\n jewelry: 'Gem',\r\n major_appliance: 'Refrigerator',\r\n painting: 'Frame',\r\n photography_equipment: 'Camera',\r\n sculpture: 'Cuboid',\r\n watch: 'Watch',\r\n other: 'Package',\r\n}\r\n\r\n/**\r\n * Maps valuable document types to Lucide icon names\r\n */\r\nexport const VALUABLE_DOCUMENT_TYPE_ICONS: Record<ValuableDocumentType, string> = {\r\n photo: 'Image',\r\n certificate_of_authenticity: 'Award',\r\n receipt: 'Receipt',\r\n appraisal: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Device Options\r\n// =============================================================================\r\n\r\nexport const DEVICE_TYPES = ['router', 'camera', 'smart_light', 'thermostat', 'doorbell', 'lock', 'speaker', 'tv', 'hub', 'sensor', 'appliance', 'other'] as const\r\nexport type DeviceType = typeof DEVICE_TYPES[number]\r\n\r\nexport const DEVICE_TYPE_OPTIONS = [\r\n { label: 'Router/Network', value: 'router' },\r\n { label: 'Camera', value: 'camera' },\r\n { label: 'Smart Light', value: 'smart_light' },\r\n { label: 'Thermostat', value: 'thermostat' },\r\n { label: 'Doorbell', value: 'doorbell' },\r\n { label: 'Smart Lock', value: 'lock' },\r\n { label: 'Speaker/Assistant', value: 'speaker' },\r\n { label: 'Smart TV', value: 'tv' },\r\n { label: 'Hub/Bridge', value: 'hub' },\r\n { label: 'Sensor', value: 'sensor' },\r\n { label: 'Smart Appliance', value: 'appliance' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps device types to Lucide icon names\r\n */\r\nexport const DEVICE_TYPE_ICONS: Record<DeviceType, string> = {\r\n router: 'Wifi',\r\n camera: 'Camera',\r\n smart_light: 'Lightbulb',\r\n thermostat: 'Thermometer',\r\n doorbell: 'BellRing',\r\n lock: 'Lock',\r\n speaker: 'Speaker',\r\n tv: 'Tv',\r\n hub: 'CircuitBoard',\r\n sensor: 'Radio',\r\n appliance: 'Refrigerator',\r\n other: 'Smartphone',\r\n}\r\n\r\n// =============================================================================\r\n// Tax Options\r\n// =============================================================================\r\n\r\nexport const TAX_COUNTRIES = ['USA', 'CAN'] as const\r\nexport type TaxCountry = typeof TAX_COUNTRIES[number]\r\n\r\nexport const TAX_COUNTRY_OPTIONS = [\r\n { label: 'United States', value: 'USA' },\r\n { label: 'Canada', value: 'CAN' },\r\n] as const\r\n\r\n// USA Tax Document Types\r\nexport const USA_TAX_DOCUMENT_TYPES = ['w2', '1099', '1040', 'k1', 'other'] as const\r\nexport type UsaTaxDocumentType = typeof USA_TAX_DOCUMENT_TYPES[number]\r\n\r\nexport const USA_TAX_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'W-2', value: 'w2' },\r\n { label: '1099', value: '1099' },\r\n { label: '1040', value: '1040' },\r\n { label: 'K-1', value: 'k1' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// Canada Tax Document Types\r\nexport const CAN_TAX_DOCUMENT_TYPES = ['t4', 't5', 't3', 't1', 'other'] as const\r\nexport type CanTaxDocumentType = typeof CAN_TAX_DOCUMENT_TYPES[number]\r\n\r\nexport const CAN_TAX_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'T4 (Employment Income)', value: 't4' },\r\n { label: 'T5 (Investment Income)', value: 't5' },\r\n { label: 'T3 (Trust Income)', value: 't3' },\r\n { label: 'T1 (Tax Return)', value: 't1' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport type TaxDocumentType = UsaTaxDocumentType | CanTaxDocumentType\r\n\r\n/**\r\n * Maps tax document types to Lucide icon names\r\n */\r\nexport const TAX_DOCUMENT_TYPE_ICONS: Record<TaxDocumentType, string> = {\r\n // USA\r\n w2: 'FileText',\r\n '1099': 'FileText',\r\n '1040': 'FileCheck',\r\n k1: 'FileSpreadsheet',\r\n // Canada\r\n t4: 'FileText',\r\n t5: 'FileText',\r\n t3: 'FileSpreadsheet',\r\n t1: 'FileCheck',\r\n // Shared\r\n other: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Home Improvement Options\r\n// =============================================================================\r\n\r\nexport const HOME_IMPROVEMENT_TYPES = [\r\n 'roof', 'electrical', 'plumbing', 'hvac', 'water_heater', 'windows', 'doors',\r\n 'flooring', 'painting', 'siding', 'foundation', 'insulation', 'landscaping',\r\n 'fencing', 'pool', 'deck_patio', 'garage', 'bathroom', 'kitchen', 'basement', 'addition',\r\n 'solar', 'security', 'smart_home', 'other'\r\n] as const\r\nexport type HomeImprovementType = typeof HOME_IMPROVEMENT_TYPES[number]\r\n\r\nexport const HOME_IMPROVEMENT_TYPE_OPTIONS = [\r\n { label: 'Roof', value: 'roof' },\r\n { label: 'Electrical', value: 'electrical' },\r\n { label: 'Plumbing', value: 'plumbing' },\r\n { label: 'HVAC', value: 'hvac' },\r\n { label: 'Water Heater', value: 'water_heater' },\r\n { label: 'Windows', value: 'windows' },\r\n { label: 'Doors', value: 'doors' },\r\n { label: 'Flooring', value: 'flooring' },\r\n { label: 'Painting', value: 'painting' },\r\n { label: 'Siding', value: 'siding' },\r\n { label: 'Foundation', value: 'foundation' },\r\n { label: 'Insulation', value: 'insulation' },\r\n { label: 'Landscaping', value: 'landscaping' },\r\n { label: 'Fencing', value: 'fencing' },\r\n { label: 'Pool', value: 'pool' },\r\n { label: 'Deck/Patio', value: 'deck_patio' },\r\n { label: 'Garage', value: 'garage' },\r\n { label: 'Bathroom Remodel', value: 'bathroom' },\r\n { label: 'Kitchen Remodel', value: 'kitchen' },\r\n { label: 'Basement', value: 'basement' },\r\n { label: 'Addition', value: 'addition' },\r\n { label: 'Solar', value: 'solar' },\r\n { label: 'Security System', value: 'security' },\r\n { label: 'Smart Home', value: 'smart_home' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps home improvement types to Lucide icon names\r\n */\r\nexport const HOME_IMPROVEMENT_TYPE_ICONS: Record<HomeImprovementType, string> = {\r\n roof: 'Home',\r\n electrical: 'Zap',\r\n plumbing: 'Droplet',\r\n hvac: 'Wind',\r\n water_heater: 'Flame',\r\n windows: 'LayoutGrid',\r\n doors: 'DoorOpen',\r\n flooring: 'Grid3x3',\r\n painting: 'PaintBucket',\r\n siding: 'PanelTop',\r\n foundation: 'Layers',\r\n insulation: 'Thermometer',\r\n landscaping: 'TreeDeciduous',\r\n fencing: 'Fence',\r\n pool: 'Waves',\r\n deck_patio: 'Fence',\r\n garage: 'Warehouse',\r\n bathroom: 'Bath',\r\n kitchen: 'ChefHat',\r\n basement: 'ArrowDown',\r\n addition: 'Plus',\r\n solar: 'Sun',\r\n security: 'Shield',\r\n smart_home: 'Wifi',\r\n other: 'Hammer',\r\n}\r\n\r\nexport const HOME_IMPROVEMENT_DOCUMENT_TYPES = ['permit', 'inspection', 'warranty', 'invoice', 'contract', 'photo', 'other'] as const\r\nexport type HomeImprovementDocumentType = typeof HOME_IMPROVEMENT_DOCUMENT_TYPES[number]\r\n\r\nexport const HOME_IMPROVEMENT_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'Permit', value: 'permit' },\r\n { label: 'Inspection', value: 'inspection' },\r\n { label: 'Warranty', value: 'warranty' },\r\n { label: 'Invoice', value: 'invoice' },\r\n { label: 'Contract', value: 'contract' },\r\n { label: 'Photo', value: 'photo' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps home improvement document types to Lucide icon names\r\n */\r\nexport const HOME_IMPROVEMENT_DOCUMENT_TYPE_ICONS: Record<HomeImprovementDocumentType, string> = {\r\n permit: 'FileCheck',\r\n inspection: 'ClipboardCheck',\r\n warranty: 'Shield',\r\n invoice: 'Receipt',\r\n contract: 'FileText',\r\n photo: 'Image',\r\n other: 'File',\r\n}\r\n\r\n// =============================================================================\r\n// Vehicle Maintenance Options\r\n// =============================================================================\r\n\r\nexport const VEHICLE_MAINTENANCE_TYPES = [\r\n 'oil_change', 'tire_rotation', 'tire_replacement', 'brake_pads', 'brake_rotors',\r\n 'battery', 'transmission', 'coolant', 'air_filter', 'cabin_filter', 'spark_plugs',\r\n 'timing_belt', 'suspension', 'alignment', 'exhaust', 'electrical', 'body_work',\r\n 'windshield', 'inspection', 'emissions', 'recall', 'warranty_repair',\r\n 'accident_repair', 'detailing', 'other'\r\n] as const\r\nexport type VehicleMaintenanceType = typeof VEHICLE_MAINTENANCE_TYPES[number]\r\n\r\nexport const VEHICLE_MAINTENANCE_TYPE_OPTIONS = [\r\n { label: 'Oil Change', value: 'oil_change' },\r\n { label: 'Tire Rotation', value: 'tire_rotation' },\r\n { label: 'Tire Replacement', value: 'tire_replacement' },\r\n { label: 'Brake Pads', value: 'brake_pads' },\r\n { label: 'Brake Rotors', value: 'brake_rotors' },\r\n { label: 'Battery', value: 'battery' },\r\n { label: 'Transmission Service', value: 'transmission' },\r\n { label: 'Coolant Flush', value: 'coolant' },\r\n { label: 'Air Filter', value: 'air_filter' },\r\n { label: 'Cabin Air Filter', value: 'cabin_filter' },\r\n { label: 'Spark Plugs', value: 'spark_plugs' },\r\n { label: 'Timing Belt', value: 'timing_belt' },\r\n { label: 'Suspension', value: 'suspension' },\r\n { label: 'Wheel Alignment', value: 'alignment' },\r\n { label: 'Exhaust', value: 'exhaust' },\r\n { label: 'Electrical', value: 'electrical' },\r\n { label: 'Body Work', value: 'body_work' },\r\n { label: 'Windshield', value: 'windshield' },\r\n { label: 'State Inspection', value: 'inspection' },\r\n { label: 'Emissions Test', value: 'emissions' },\r\n { label: 'Recall Service', value: 'recall' },\r\n { label: 'Warranty Repair', value: 'warranty_repair' },\r\n { label: 'Accident Repair', value: 'accident_repair' },\r\n { label: 'Detailing', value: 'detailing' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps vehicle maintenance types to Lucide icon names\r\n */\r\nexport const VEHICLE_MAINTENANCE_TYPE_ICONS: Record<VehicleMaintenanceType, string> = {\r\n oil_change: 'Droplet',\r\n tire_rotation: 'CircleDot',\r\n tire_replacement: 'CircleDot',\r\n brake_pads: 'CircleSlash',\r\n brake_rotors: 'CircleSlash',\r\n battery: 'Battery',\r\n transmission: 'Cog',\r\n coolant: 'Thermometer',\r\n air_filter: 'Wind',\r\n cabin_filter: 'Wind',\r\n spark_plugs: 'Zap',\r\n timing_belt: 'Timer',\r\n suspension: 'ArrowUpDown',\r\n alignment: 'Crosshair',\r\n exhaust: 'CloudCog',\r\n electrical: 'Zap',\r\n body_work: 'Car',\r\n windshield: 'LayoutGrid',\r\n inspection: 'ClipboardCheck',\r\n emissions: 'CloudCog',\r\n recall: 'AlertTriangle',\r\n warranty_repair: 'Shield',\r\n accident_repair: 'AlertOctagon',\r\n detailing: 'Sparkles',\r\n other: 'Wrench',\r\n}\r\n\r\n// =============================================================================\r\n// Health Record Options\r\n// =============================================================================\r\n\r\nexport const HEALTH_RECORD_TYPES_PERSON = [\r\n 'vaccination', 'checkup', 'physical', 'dental', 'vision', 'procedure', 'surgery',\r\n 'lab_work', 'imaging', 'prescription', 'therapy', 'mental_health', 'specialist',\r\n 'emergency', 'hospitalization', 'allergy_test', 'screening', 'other'\r\n] as const\r\nexport type HealthRecordTypePerson = typeof HEALTH_RECORD_TYPES_PERSON[number]\r\n\r\nexport const HEALTH_RECORD_TYPE_PERSON_OPTIONS = [\r\n { label: 'Vaccination', value: 'vaccination' },\r\n { label: 'Checkup', value: 'checkup' },\r\n { label: 'Physical Exam', value: 'physical' },\r\n { label: 'Dental', value: 'dental' },\r\n { label: 'Vision/Eye Exam', value: 'vision' },\r\n { label: 'Procedure', value: 'procedure' },\r\n { label: 'Surgery', value: 'surgery' },\r\n { label: 'Lab Work', value: 'lab_work' },\r\n { label: 'Imaging', value: 'imaging' },\r\n { label: 'Prescription', value: 'prescription' },\r\n { label: 'Physical/Occupational Therapy', value: 'therapy' },\r\n { label: 'Mental Health', value: 'mental_health' },\r\n { label: 'Specialist Visit', value: 'specialist' },\r\n { label: 'Emergency/Urgent Care', value: 'emergency' },\r\n { label: 'Hospitalization', value: 'hospitalization' },\r\n { label: 'Allergy Test', value: 'allergy_test' },\r\n { label: 'Screening', value: 'screening' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const HEALTH_RECORD_TYPES_PET = [\r\n 'vaccination', 'checkup', 'dental', 'surgery', 'grooming', 'microchip',\r\n 'spay_neuter', 'lab_work', 'imaging', 'prescription', 'emergency', 'specialist',\r\n 'parasite_prevention', 'boarding_exam', 'other'\r\n] as const\r\nexport type HealthRecordTypePet = typeof HEALTH_RECORD_TYPES_PET[number]\r\n\r\nexport const HEALTH_RECORD_TYPE_PET_OPTIONS = [\r\n { label: 'Vaccination', value: 'vaccination' },\r\n { label: 'Wellness Exam', value: 'checkup' },\r\n { label: 'Dental Cleaning', value: 'dental' },\r\n { label: 'Surgery', value: 'surgery' },\r\n { label: 'Grooming', value: 'grooming' },\r\n { label: 'Microchip', value: 'microchip' },\r\n { label: 'Spay/Neuter', value: 'spay_neuter' },\r\n { label: 'Lab Work', value: 'lab_work' },\r\n { label: 'Imaging', value: 'imaging' },\r\n { label: 'Prescription', value: 'prescription' },\r\n { label: 'Emergency', value: 'emergency' },\r\n { label: 'Specialist', value: 'specialist' },\r\n { label: 'Parasite Prevention', value: 'parasite_prevention' },\r\n { label: 'Boarding Exam', value: 'boarding_exam' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport type HealthRecordType = HealthRecordTypePerson | HealthRecordTypePet\r\n\r\n/**\r\n * Maps health record types to Lucide icon names\r\n */\r\nexport const HEALTH_RECORD_TYPE_ICONS: Record<HealthRecordType, string> = {\r\n vaccination: 'Syringe',\r\n checkup: 'Stethoscope',\r\n physical: 'Activity',\r\n dental: 'Smile',\r\n vision: 'Eye',\r\n procedure: 'Scissors',\r\n surgery: 'Scissors',\r\n lab_work: 'TestTube',\r\n imaging: 'Scan',\r\n prescription: 'Pill',\r\n therapy: 'Activity',\r\n mental_health: 'Brain',\r\n specialist: 'UserCog',\r\n emergency: 'AlertTriangle',\r\n hospitalization: 'Building2',\r\n allergy_test: 'Flower2',\r\n screening: 'Search',\r\n grooming: 'Scissors',\r\n microchip: 'Cpu',\r\n spay_neuter: 'Scissors',\r\n parasite_prevention: 'Bug',\r\n boarding_exam: 'ClipboardCheck',\r\n other: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Pet Procedure Options (for multi-procedure vet visits)\r\n// =============================================================================\r\n\r\n/**\r\n * Pet procedure types for multi-procedure vet visits\r\n * Parallel to VehicleMaintenanceType for service invoices\r\n */\r\nexport const PET_PROCEDURE_TYPES = [\r\n 'vaccination', 'checkup', 'dental', 'surgery', 'grooming', 'microchip',\r\n 'spay_neuter', 'lab_work', 'imaging', 'prescription', 'emergency',\r\n 'specialist', 'parasite_prevention', 'boarding_exam', 'euthanasia', 'other'\r\n] as const\r\n\r\nexport const PET_PROCEDURE_TYPE_OPTIONS = [\r\n { label: 'Vaccination', value: 'vaccination' },\r\n { label: 'Wellness Exam', value: 'checkup' },\r\n { label: 'Dental Cleaning', value: 'dental' },\r\n { label: 'Surgery', value: 'surgery' },\r\n { label: 'Grooming', value: 'grooming' },\r\n { label: 'Microchip', value: 'microchip' },\r\n { label: 'Spay/Neuter', value: 'spay_neuter' },\r\n { label: 'Lab Work', value: 'lab_work' },\r\n { label: 'Imaging/X-Ray', value: 'imaging' },\r\n { label: 'Prescription', value: 'prescription' },\r\n { label: 'Emergency', value: 'emergency' },\r\n { label: 'Specialist Visit', value: 'specialist' },\r\n { label: 'Parasite Prevention', value: 'parasite_prevention' },\r\n { label: 'Boarding Exam', value: 'boarding_exam' },\r\n { label: 'Euthanasia', value: 'euthanasia' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps pet procedure types to Lucide icon names\r\n */\r\nexport const PET_PROCEDURE_TYPE_ICONS: Record<string, string> = {\r\n vaccination: 'Syringe',\r\n checkup: 'Stethoscope',\r\n dental: 'Smile',\r\n surgery: 'Scissors',\r\n grooming: 'Scissors',\r\n microchip: 'Cpu',\r\n spay_neuter: 'Scissors',\r\n lab_work: 'TestTube',\r\n imaging: 'Scan',\r\n prescription: 'Pill',\r\n emergency: 'AlertTriangle',\r\n specialist: 'UserCog',\r\n parasite_prevention: 'Bug',\r\n boarding_exam: 'ClipboardCheck',\r\n euthanasia: 'Heart',\r\n other: 'FileText',\r\n}\r\n\r\n/**\r\n * Valid icon background colors for UI display\r\n * Used by UniversalListItem and other components\r\n */\r\nexport type IconBgColor = 'blue' | 'green' | 'purple' | 'red' | 'orange' | 'gray' | 'yellow' | 'pink' | 'cyan' | 'teal' | 'indigo' | 'violet' | 'amber' | 'lime' | 'slate' | 'emerald' | 'sky' | 'zinc'\r\n\r\n/**\r\n * Maps pet procedure types to colors for UI display\r\n */\r\nexport const PET_PROCEDURE_TYPE_COLORS: Record<string, IconBgColor> = {\r\n vaccination: 'blue',\r\n checkup: 'green',\r\n dental: 'cyan',\r\n surgery: 'red',\r\n grooming: 'pink',\r\n microchip: 'purple',\r\n spay_neuter: 'orange',\r\n lab_work: 'indigo',\r\n imaging: 'slate',\r\n prescription: 'teal',\r\n emergency: 'red',\r\n specialist: 'violet',\r\n parasite_prevention: 'amber',\r\n boarding_exam: 'lime',\r\n euthanasia: 'gray',\r\n other: 'gray',\r\n}\r\n\r\n// =============================================================================\r\n// Military Record Options\r\n// =============================================================================\r\n\r\nexport const MILITARY_RECORD_TYPES = [\r\n 'dd214',\r\n 'service_record',\r\n 'discharge_papers',\r\n 'enlistment_contract',\r\n 'promotion_orders',\r\n 'awards_decorations',\r\n 'medical_records',\r\n 'training_certificate',\r\n 'other',\r\n] as const\r\nexport type MilitaryRecordType = typeof MILITARY_RECORD_TYPES[number]\r\n\r\nexport const MILITARY_RECORD_TYPE_OPTIONS = [\r\n { label: 'DD-214 (Discharge)', value: 'dd214' },\r\n { label: 'Service Record', value: 'service_record' },\r\n { label: 'Discharge Papers', value: 'discharge_papers' },\r\n { label: 'Enlistment Contract', value: 'enlistment_contract' },\r\n { label: 'Promotion Orders', value: 'promotion_orders' },\r\n { label: 'Awards & Decorations', value: 'awards_decorations' },\r\n { label: 'Medical Records', value: 'medical_records' },\r\n { label: 'Training Certificate', value: 'training_certificate' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const MILITARY_BRANCHES = [\r\n 'army',\r\n 'navy',\r\n 'air_force',\r\n 'marines',\r\n 'coast_guard',\r\n 'space_force',\r\n 'national_guard',\r\n 'reserves',\r\n 'other',\r\n] as const\r\nexport type MilitaryBranch = typeof MILITARY_BRANCHES[number]\r\n\r\nexport const MILITARY_BRANCH_OPTIONS = [\r\n { label: 'Army', value: 'army' },\r\n { label: 'Navy', value: 'navy' },\r\n { label: 'Air Force', value: 'air_force' },\r\n { label: 'Marines', value: 'marines' },\r\n { label: 'Coast Guard', value: 'coast_guard' },\r\n { label: 'Space Force', value: 'space_force' },\r\n { label: 'National Guard', value: 'national_guard' },\r\n { label: 'Reserves', value: 'reserves' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const MILITARY_RECORD_TYPE_ICONS: Record<MilitaryRecordType, string> = {\r\n dd214: 'FileText',\r\n service_record: 'ClipboardList',\r\n discharge_papers: 'FileSignature',\r\n enlistment_contract: 'FileText',\r\n promotion_orders: 'Award',\r\n awards_decorations: 'Medal',\r\n medical_records: 'Stethoscope',\r\n training_certificate: 'GraduationCap',\r\n other: 'FileText',\r\n}\r\n\r\nexport const MILITARY_COUNTRIES = ['USA', 'CAN'] as const\r\nexport type MilitaryCountry = typeof MILITARY_COUNTRIES[number]\r\n\r\nexport const MILITARY_COUNTRY_OPTIONS = [\r\n { label: 'United States', value: 'USA' },\r\n { label: 'Canada', value: 'CAN' },\r\n] as const\r\n\r\n// Canadian military branches\r\nexport const CANADIAN_MILITARY_BRANCHES = [\r\n 'canadian_army',\r\n 'royal_canadian_navy',\r\n 'royal_canadian_air_force',\r\n 'canadian_special_operations',\r\n 'canadian_reserves',\r\n 'other',\r\n] as const\r\nexport type CanadianMilitaryBranch = typeof CANADIAN_MILITARY_BRANCHES[number]\r\n\r\nexport const CANADIAN_MILITARY_BRANCH_OPTIONS = [\r\n { label: 'Canadian Army', value: 'canadian_army' },\r\n { label: 'Royal Canadian Navy', value: 'royal_canadian_navy' },\r\n { label: 'Royal Canadian Air Force', value: 'royal_canadian_air_force' },\r\n { label: 'Canadian Special Operations', value: 'canadian_special_operations' },\r\n { label: 'Canadian Reserves', value: 'canadian_reserves' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Education Record Options\r\n// =============================================================================\r\n\r\nexport const EDUCATION_RECORD_TYPES = [\r\n 'diploma',\r\n 'degree',\r\n 'transcript',\r\n 'certification',\r\n 'license',\r\n 'training_completion',\r\n 'continuing_education',\r\n 'other',\r\n] as const\r\nexport type EducationRecordType = typeof EDUCATION_RECORD_TYPES[number]\r\n\r\nexport const EDUCATION_RECORD_TYPE_OPTIONS = [\r\n { label: 'Diploma', value: 'diploma' },\r\n { label: 'Degree', value: 'degree' },\r\n { label: 'Transcript', value: 'transcript' },\r\n { label: 'Certification', value: 'certification' },\r\n { label: 'Professional License', value: 'license' },\r\n { label: 'Training Completion', value: 'training_completion' },\r\n { label: 'Continuing Education', value: 'continuing_education' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const EDUCATION_LEVELS = [\r\n 'high_school',\r\n 'associate',\r\n 'bachelor',\r\n 'master',\r\n 'doctorate',\r\n 'professional',\r\n 'vocational',\r\n 'certificate',\r\n 'other',\r\n] as const\r\nexport type EducationLevel = typeof EDUCATION_LEVELS[number]\r\n\r\nexport const EDUCATION_LEVEL_OPTIONS = [\r\n { label: 'High School', value: 'high_school' },\r\n { label: 'Associate Degree', value: 'associate' },\r\n { label: 'Bachelor\\'s Degree', value: 'bachelor' },\r\n { label: 'Master\\'s Degree', value: 'master' },\r\n { label: 'Doctorate', value: 'doctorate' },\r\n { label: 'Professional Degree', value: 'professional' },\r\n { label: 'Vocational/Trade', value: 'vocational' },\r\n { label: 'Certificate Program', value: 'certificate' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const EDUCATION_RECORD_TYPE_ICONS: Record<EducationRecordType, string> = {\r\n diploma: 'ScrollText',\r\n degree: 'GraduationCap',\r\n transcript: 'FileText',\r\n certification: 'Award',\r\n license: 'BadgeCheck',\r\n training_completion: 'CheckCircle',\r\n continuing_education: 'BookOpen',\r\n other: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Credential Options (Professional Licenses, Government IDs, Travel Credentials)\r\n// =============================================================================\r\n\r\nexport const CREDENTIAL_TYPES = [\r\n 'professional_license',\r\n 'government_id',\r\n 'travel_credential',\r\n 'permit',\r\n 'security_clearance',\r\n] as const\r\nexport type CredentialType = typeof CREDENTIAL_TYPES[number]\r\n\r\nexport const CREDENTIAL_TYPE_OPTIONS = [\r\n { label: 'Professional License', value: 'professional_license' },\r\n { label: 'Government ID', value: 'government_id' },\r\n { label: 'Travel Credential', value: 'travel_credential' },\r\n { label: 'Permit', value: 'permit' },\r\n { label: 'Security Clearance', value: 'security_clearance' },\r\n] as const\r\n\r\nexport const CREDENTIAL_TYPE_ICONS: Record<CredentialType, string> = {\r\n professional_license: 'BadgeCheck',\r\n government_id: 'CreditCard',\r\n travel_credential: 'Plane',\r\n permit: 'FileCheck',\r\n security_clearance: 'ShieldCheck',\r\n}\r\n\r\n// Subtypes for each credential type\r\nexport const CREDENTIAL_SUBTYPES = {\r\n professional_license: [\r\n 'cpa',\r\n 'bar_admission',\r\n 'medical_license',\r\n 'nursing_license',\r\n 'real_estate_license',\r\n 'insurance_license',\r\n 'engineering_license',\r\n 'teaching_license',\r\n 'financial_advisor',\r\n 'other',\r\n ],\r\n government_id: [\r\n 'passport',\r\n 'drivers_license',\r\n 'state_id',\r\n 'national_id',\r\n 'green_card',\r\n 'visa',\r\n 'military_id',\r\n 'other',\r\n ],\r\n travel_credential: [\r\n 'tsa_precheck',\r\n 'global_entry',\r\n 'nexus',\r\n 'sentri',\r\n 'clear',\r\n 'other',\r\n ],\r\n permit: [\r\n 'concealed_carry',\r\n 'business_license',\r\n 'hunting_license',\r\n 'fishing_license',\r\n 'other',\r\n ],\r\n security_clearance: [\r\n 'confidential',\r\n 'secret',\r\n 'top_secret',\r\n 'top_secret_sci',\r\n 'other',\r\n ],\r\n} as const\r\n\r\nexport type CredentialSubtype<T extends CredentialType> = typeof CREDENTIAL_SUBTYPES[T][number]\r\nexport type AnyCredentialSubtype = CredentialSubtype<CredentialType>\r\n\r\nexport const CREDENTIAL_SUBTYPE_OPTIONS: Record<CredentialType, Array<{ label: string; value: string }>> = {\r\n professional_license: [\r\n { label: 'CPA (Certified Public Accountant)', value: 'cpa' },\r\n { label: 'Bar Admission (Attorney)', value: 'bar_admission' },\r\n { label: 'Medical License (MD/DO)', value: 'medical_license' },\r\n { label: 'Nursing License (RN/LPN)', value: 'nursing_license' },\r\n { label: 'Real Estate License', value: 'real_estate_license' },\r\n { label: 'Insurance License', value: 'insurance_license' },\r\n { label: 'Engineering License (PE)', value: 'engineering_license' },\r\n { label: 'Teaching License', value: 'teaching_license' },\r\n { label: 'Financial Advisor', value: 'financial_advisor' },\r\n { label: 'Other', value: 'other' },\r\n ],\r\n government_id: [\r\n { label: 'Passport', value: 'passport' },\r\n { label: \"Driver's License\", value: 'drivers_license' },\r\n { label: 'State ID', value: 'state_id' },\r\n { label: 'National ID (Cédula, etc.)', value: 'national_id' },\r\n { label: 'Green Card', value: 'green_card' },\r\n { label: 'Visa', value: 'visa' },\r\n { label: 'Military ID', value: 'military_id' },\r\n { label: 'Other', value: 'other' },\r\n ],\r\n travel_credential: [\r\n { label: 'TSA PreCheck', value: 'tsa_precheck' },\r\n { label: 'Global Entry', value: 'global_entry' },\r\n { label: 'NEXUS', value: 'nexus' },\r\n { label: 'SENTRI', value: 'sentri' },\r\n { label: 'CLEAR', value: 'clear' },\r\n { label: 'Other', value: 'other' },\r\n ],\r\n permit: [\r\n { label: 'Concealed Carry Permit', value: 'concealed_carry' },\r\n { label: 'Business License', value: 'business_license' },\r\n { label: 'Hunting License', value: 'hunting_license' },\r\n { label: 'Fishing License', value: 'fishing_license' },\r\n { label: 'Other', value: 'other' },\r\n ],\r\n security_clearance: [\r\n { label: 'Confidential', value: 'confidential' },\r\n { label: 'Secret', value: 'secret' },\r\n { label: 'Top Secret', value: 'top_secret' },\r\n { label: 'Top Secret/SCI', value: 'top_secret_sci' },\r\n { label: 'Other', value: 'other' },\r\n ],\r\n}\r\n\r\nexport const CREDENTIAL_SUBTYPE_ICONS: Record<string, string> = {\r\n // Professional licenses\r\n cpa: 'Calculator',\r\n bar_admission: 'Scale',\r\n medical_license: 'Stethoscope',\r\n nursing_license: 'HeartPulse',\r\n real_estate_license: 'Home',\r\n insurance_license: 'Shield',\r\n engineering_license: 'Wrench',\r\n teaching_license: 'GraduationCap',\r\n financial_advisor: 'TrendingUp',\r\n // Government IDs\r\n passport: 'BookOpen',\r\n drivers_license: 'Car',\r\n state_id: 'CreditCard',\r\n national_id: 'CreditCard',\r\n green_card: 'Wallet',\r\n visa: 'Stamp',\r\n military_id: 'Shield',\r\n // Travel credentials\r\n tsa_precheck: 'Plane',\r\n global_entry: 'Globe',\r\n nexus: 'ArrowLeftRight',\r\n sentri: 'ArrowLeftRight',\r\n clear: 'Scan',\r\n // Permits\r\n concealed_carry: 'Target',\r\n business_license: 'Briefcase',\r\n hunting_license: 'Crosshair',\r\n fishing_license: 'Fish',\r\n // Security clearances\r\n confidential: 'Lock',\r\n secret: 'Lock',\r\n top_secret: 'ShieldAlert',\r\n top_secret_sci: 'ShieldAlert',\r\n // Default\r\n other: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Membership Record Options\r\n// =============================================================================\r\n\r\nexport const MEMBERSHIP_TYPES = [\r\n 'airline',\r\n 'hotel',\r\n 'rental_car',\r\n 'retail',\r\n 'restaurant',\r\n 'entertainment',\r\n 'travel',\r\n 'rewards',\r\n 'other',\r\n] as const\r\nexport type MembershipType = typeof MEMBERSHIP_TYPES[number]\r\n\r\nexport const MEMBERSHIP_TYPE_OPTIONS = [\r\n { label: 'Airline', value: 'airline' },\r\n { label: 'Hotel', value: 'hotel' },\r\n { label: 'Rental Car', value: 'rental_car' },\r\n { label: 'Retail', value: 'retail' },\r\n { label: 'Restaurant', value: 'restaurant' },\r\n { label: 'Entertainment', value: 'entertainment' },\r\n { label: 'Travel', value: 'travel' },\r\n { label: 'Rewards Program', value: 'rewards' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const MEMBERSHIP_TYPE_ICONS: Record<MembershipType, string> = {\r\n airline: 'Plane',\r\n hotel: 'Hotel',\r\n rental_car: 'Car',\r\n retail: 'ShoppingBag',\r\n restaurant: 'UtensilsCrossed',\r\n entertainment: 'Ticket',\r\n travel: 'MapPin',\r\n rewards: 'Gift',\r\n other: 'CreditCard',\r\n}\r\n\r\nexport const MEMBERSHIP_TYPE_COLORS: Record<MembershipType, IconBgColor> = {\r\n airline: 'blue',\r\n hotel: 'purple',\r\n rental_car: 'orange',\r\n retail: 'pink',\r\n restaurant: 'amber',\r\n entertainment: 'indigo',\r\n travel: 'teal',\r\n rewards: 'green',\r\n other: 'gray',\r\n}\r\n\r\n","{\r\n \"limits\": {\r\n \"property\": { \"household\": 1, \"estate\": 50, \"requiredPlan\": \"estate\" },\r\n \"vehicle\": { \"household\": 3, \"estate\": 50, \"requiredPlan\": \"household\" },\r\n \"pet\": { \"household\": 10, \"estate\": 50, \"requiredPlan\": \"household\" },\r\n \"contact\": { \"household\": 200, \"estate\": 1000, \"requiredPlan\": \"household\" },\r\n \"resident\": { \"household\": 6, \"estate\": 50, \"requiredPlan\": \"household\" },\r\n \"maintenance_task\": { \"household\": 100, \"estate\": 1000, \"requiredPlan\": \"household\" },\r\n \"subscription\": { \"household\": 100, \"estate\": 1000, \"requiredPlan\": \"household\" },\r\n \"valuable\": { \"household\": 20, \"estate\": 2000, \"requiredPlan\": \"household\" },\r\n \"legal\": { \"household\": 5, \"estate\": 500, \"requiredPlan\": \"household\" },\r\n \"financial_account\": { \"household\": 100, \"estate\": 1000, \"requiredPlan\": \"household\" },\r\n \"service\": { \"household\": 20, \"estate\": 100, \"requiredPlan\": \"household\" },\r\n \"insurance\": { \"household\": 20, \"estate\": 100, \"requiredPlan\": \"household\" },\r\n \"device\": { \"household\": 50, \"estate\": 1000, \"requiredPlan\": \"household\" },\r\n \"identity\": { \"household\": 10, \"estate\": 50, \"requiredPlan\": \"household\" }\r\n }\r\n}\r\n","/**\r\n * Subscription plan definitions shared between marketing site and app.\r\n * Note: Free tier has been removed. All new households start with a 14-day trial.\r\n */\r\n\r\nexport type PlanCode = 'household_monthly' | 'household_annual' | 'estate_monthly' | 'estate_annual'\r\nexport type PlanTier = 'household' | 'estate'\r\n\r\nexport interface PlanDefinition {\r\n planCode: PlanCode\r\n tier: PlanTier\r\n name: string\r\n description: string\r\n descriptionAnnual?: string\r\n amountCents: number\r\n billingCycle: 'monthly' | 'annual'\r\n features: readonly string[]\r\n tagline?: string\r\n}\r\n\r\nexport const PLAN_FEATURES = {\r\n household: [\r\n '1 property',\r\n 'Up to 3 vehicles',\r\n 'Up to 5 family members',\r\n\r\n 'Store contractors, providers, and emergency contacts',\r\n 'Track cleaners, gardeners, and other service visits',\r\n 'Maintenance task wizard for your property and vehicles',\r\n 'Maintenance reminders',\r\n 'Emergency procedures',\r\n\r\n 'Subscription cost and renewal tracking',\r\n 'Link insurance to assets, expiration reminders',\r\n 'Valuables cataloging',\r\n 'Encrypted legal documents',\r\n\r\n 'Secure sharing'\r\n ],\r\n\r\n estate: [\r\n 'Dozens of properties and vehicles',\r\n 'Dozens of members including family and staff',\r\n\r\n 'Link LLCs and Trusts to your assets',\r\n 'Continuity Protocol (automatic contingency access)',\r\n\r\n 'Everything in Household',\r\n ],\r\n};\r\n\r\nexport const PLAN_DEFINITIONS: Record<PlanCode, PlanDefinition> = {\r\n household_monthly: {\r\n planCode: 'household_monthly',\r\n tier: 'household',\r\n name: 'Household',\r\n description: 'For managing your home and family with confidence',\r\n amountCents: 999,\r\n billingCycle: 'monthly',\r\n features: PLAN_FEATURES.household,\r\n },\r\n household_annual: {\r\n planCode: 'household_annual',\r\n tier: 'household',\r\n name: 'Household',\r\n description: 'For managing your home and family with confidence',\r\n descriptionAnnual: 'For managing your home and family with confidence - Save 17%',\r\n amountCents: 9900,\r\n billingCycle: 'annual',\r\n features: PLAN_FEATURES.household,\r\n },\r\n estate_monthly: {\r\n planCode: 'estate_monthly',\r\n tier: 'estate',\r\n name: 'Estate',\r\n description: 'For estates, multiple properties, and long term planning',\r\n amountCents: 1999,\r\n billingCycle: 'monthly',\r\n features: PLAN_FEATURES.estate,\r\n tagline: 'Designed for families who want their home information, digital assets, and legal documents to stay accessible — even if something happens.',\r\n },\r\n estate_annual: {\r\n planCode: 'estate_annual',\r\n tier: 'estate',\r\n name: 'Estate',\r\n description: 'For estates, multiple properties, and long term planning',\r\n descriptionAnnual: 'For estates, multiple properties, and long term planning - Save 17%',\r\n amountCents: 19900,\r\n billingCycle: 'annual',\r\n features: PLAN_FEATURES.estate,\r\n tagline: 'Designed for families who want their home information, digital assets, and legal documents to stay accessible — even if something happens.',\r\n },\r\n}\r\n\r\n/**\r\n * Get plan definition by plan code\r\n */\r\nexport function getPlanDefinition(planCode: PlanCode): PlanDefinition | null {\r\n return PLAN_DEFINITIONS[planCode] ?? null\r\n}\r\n\r\n/**\r\n * Get features for a plan tier\r\n */\r\nexport function getPlanFeatures(tier: PlanTier): readonly string[] {\r\n return PLAN_FEATURES[tier]\r\n}\r\n\r\n/**\r\n * Format price for display\r\n */\r\nexport function formatPlanPrice(amountCents: number, billingCycle: 'monthly' | 'annual'): string {\r\n const amount = (amountCents / 100).toFixed(amountCents % 100 === 0 ? 0 : 2)\r\n return billingCycle === 'annual' ? `$${amount}/year` : `$${amount}/mo`\r\n}\r\n\r\n/**\r\n * Entity type strings used in the database/API.\r\n * Also used as feature limit keys - single naming convention.\r\n */\r\nexport type EntityType =\r\n | 'property'\r\n | 'vehicle'\r\n | 'pet'\r\n | 'contact'\r\n | 'resident'\r\n | 'maintenance_task'\r\n | 'subscription'\r\n | 'valuable'\r\n | 'legal'\r\n | 'financial_account'\r\n | 'service'\r\n | 'insurance'\r\n | 'device'\r\n | 'identity'\r\n\r\n/**\r\n * Feature limit configuration per tier\r\n * Note: Free tier has been removed. All new households start with a Household trial.\r\n */\r\nexport interface FeatureLimitConfig {\r\n household: number\r\n estate: number\r\n /** The minimum plan required to use this feature */\r\n requiredPlan: PlanTier\r\n}\r\n\r\n// Import from JSON - single source of truth\r\nimport featureLimitsJson from './feature-limits.json'\r\n\r\n/**\r\n * Feature limits by entity type.\r\n * Used by both frontend (to show upgrade prompts) and backend (to enforce limits on writes).\r\n */\r\nexport const FEATURE_LIMITS: Record<EntityType, FeatureLimitConfig> =\r\n featureLimitsJson.limits as Record<EntityType, FeatureLimitConfig>\r\n\r\n/**\r\n * Get the limit for an entity type based on the user's plan tier\r\n */\r\nexport function getFeatureLimit(entityType: EntityType, tier: PlanTier): number {\r\n return FEATURE_LIMITS[entityType][tier]\r\n}\r\n\r\n/**\r\n * Check if a user can add more of an entity type based on current count and plan tier\r\n */\r\nexport function canAddFeature(entityType: EntityType, tier: PlanTier, currentCount: number): boolean {\r\n const limit = getFeatureLimit(entityType, tier)\r\n return currentCount < limit\r\n}\r\n\r\n/**\r\n * Get the required plan to use an entity type (for upgrade prompts)\r\n */\r\nexport function getRequiredPlan(entityType: EntityType): PlanTier {\r\n return FEATURE_LIMITS[entityType].requiredPlan\r\n}\r\n\r\n/**\r\n * Check if an entity type is fully gated (limit is 0) for the given tier\r\n */\r\nexport function isFeatureGated(entityType: EntityType, tier: PlanTier): boolean {\r\n return getFeatureLimit(entityType, tier) === 0\r\n}\r\n\r\n/**\r\n * Check if an entity type string is a valid EntityType with limits\r\n */\r\nexport function isLimitedEntityType(entityType: string): entityType is EntityType {\r\n return entityType in FEATURE_LIMITS\r\n}\r\n\r\n/**\r\n * Get the limit for an entity type based on the user's plan tier\r\n * Returns undefined if the entity type doesn't have limits\r\n */\r\nexport function getEntityLimit(entityType: string, tier: PlanTier): number | undefined {\r\n if (!isLimitedEntityType(entityType)) return undefined\r\n return getFeatureLimit(entityType, tier)\r\n}\r\n","/**\r\n * File type utilities\r\n *\r\n * Shared constants and helpers for file handling across web and mobile.\r\n */\r\n\r\n// =============================================================================\r\n// File Size Limits\r\n// IMPORTANT: Keep in sync with backend-v2/Configuration/AttachmentConfig.cs\r\n// =============================================================================\r\n\r\n/** Maximum file size in MB for regular uploads */\r\nexport const MAX_FILE_SIZE_MB = 200\r\n\r\n/** Maximum file size in bytes for regular uploads */\r\nexport const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024\r\n\r\n/** Maximum file size in MB for heartbeat/continuity videos */\r\nexport const MAX_HEARTBEAT_VIDEO_SIZE_MB = 500\r\n\r\n/** Maximum file size in bytes for heartbeat/continuity videos */\r\nexport const MAX_HEARTBEAT_VIDEO_SIZE_BYTES = MAX_HEARTBEAT_VIDEO_SIZE_MB * 1024 * 1024\r\n\r\n// =============================================================================\r\n// MIME Type Mappings\r\n// =============================================================================\r\n\r\n/**\r\n * Mapping of MIME types to file extensions.\r\n * Used for both download (adding extensions) and display purposes.\r\n */\r\nexport const MIME_TO_EXTENSION: Record<string, string> = {\r\n // Images\r\n 'image/jpeg': '.jpg',\r\n 'image/jpg': '.jpg',\r\n 'image/png': '.png',\r\n 'image/gif': '.gif',\r\n 'image/webp': '.webp',\r\n 'image/svg+xml': '.svg',\r\n 'image/bmp': '.bmp',\r\n 'image/heic': '.heic',\r\n 'image/heif': '.heif',\r\n // Documents\r\n 'application/pdf': '.pdf',\r\n 'application/msword': '.doc',\r\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',\r\n 'text/plain': '.txt',\r\n // Spreadsheets\r\n 'application/vnd.ms-excel': '.xls',\r\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',\r\n 'text/csv': '.csv',\r\n // Archives\r\n 'application/zip': '.zip',\r\n 'application/x-zip-compressed': '.zip',\r\n // Videos\r\n 'video/mp4': '.mp4',\r\n 'video/webm': '.webm',\r\n 'video/ogg': '.ogv',\r\n 'video/quicktime': '.mov',\r\n 'video/x-m4v': '.m4v',\r\n 'video/mpeg': '.mpeg',\r\n 'video/x-mpeg': '.mpeg',\r\n // Audio\r\n 'audio/mpeg': '.mp3',\r\n 'audio/wav': '.wav',\r\n}\r\n\r\n/**\r\n * Get file extension from MIME type (with leading dot).\r\n * Returns empty string if MIME type is unknown.\r\n */\r\nexport function getExtensionFromMimeType(mimeType: string): string {\r\n return MIME_TO_EXTENSION[mimeType.toLowerCase()] || ''\r\n}\r\n\r\n/**\r\n * Get file extension from MIME type (without leading dot).\r\n * Returns 'bin' if MIME type is unknown.\r\n */\r\nexport function getExtensionWithoutDot(mimeType: string): string {\r\n const ext = MIME_TO_EXTENSION[mimeType.toLowerCase()]\r\n return ext ? ext.substring(1) : 'bin'\r\n}\r\n\r\n/**\r\n * Add extension to filename if not already present.\r\n * Does nothing if MIME type is unknown.\r\n */\r\nexport function ensureFileExtension(fileName: string, mimeType: string): string {\r\n const ext = getExtensionFromMimeType(mimeType)\r\n if (!ext) return fileName\r\n // Check if fileName already has this extension (case-insensitive)\r\n if (fileName.toLowerCase().endsWith(ext.toLowerCase())) return fileName\r\n return fileName + ext\r\n}\r\n","/**\r\n * Navigation Icon Colors\r\n *\r\n * Color mapping for sidebar navigation icons.\r\n * Each category has a unique color to help with visual identification.\r\n * Supports holiday themes when colorful icons are enabled.\r\n */\r\n\r\nexport type HolidayTheme = 'default' | 'christmas' | 'halloween' | 'valentines' | 'pride' | 'stpatricks' | 'july4th' | 'canadaday' | 'thanksgiving' | 'earthday' | 'newyear' | 'easter' | 'costarica'\r\n\r\n/**\r\n * Detect current holiday theme based on date.\r\n * Returns 'default' if no holiday is active.\r\n *\r\n * For testing, add ?theme=christmas (or halloween, valentines, pride, stpatricks, july4th, canadaday, thanksgiving, earthday, newyear, easter, costarica) to the URL.\r\n */\r\nexport function getCurrentHolidayTheme(): HolidayTheme {\r\n // Check for URL override (for testing)\r\n if (typeof window !== 'undefined') {\r\n const params = new URLSearchParams(window.location.search)\r\n const themeOverride = params.get('theme') as HolidayTheme | null\r\n if (themeOverride && ['christmas', 'halloween', 'valentines', 'pride', 'stpatricks', 'july4th', 'canadaday', 'thanksgiving', 'earthday', 'newyear', 'easter', 'costarica', 'default'].includes(themeOverride)) {\r\n return themeOverride\r\n }\r\n }\r\n\r\n const now = new Date()\r\n const month = now.getMonth() + 1 // 1-indexed\r\n const day = now.getDate()\r\n\r\n // New Year's: Dec 31 and Jan 1\r\n if ((month === 12 && day === 31) || (month === 1 && day === 1)) return 'newyear'\r\n // Valentine's: Feb 14\r\n if (month === 2 && day === 14) return 'valentines'\r\n // St. Patrick's Day: March 17\r\n if (month === 3 && day === 17) return 'stpatricks'\r\n // Earth Day: April 22\r\n if (month === 4 && day === 22) return 'earthday'\r\n // Easter: Calculate using Anonymous Gregorian algorithm\r\n const year = now.getFullYear()\r\n const a = year % 19\r\n const b = Math.floor(year / 100)\r\n const c = year % 100\r\n const d = Math.floor(b / 4)\r\n const e = b % 4\r\n const f = Math.floor((b + 8) / 25)\r\n const g = Math.floor((b - f + 1) / 3)\r\n const h = (19 * a + b - d - g + 15) % 30\r\n const i = Math.floor(c / 4)\r\n const k = c % 4\r\n const l = (32 + 2 * e + 2 * i - h - k) % 7\r\n const m = Math.floor((a + 11 * h + 22 * l) / 451)\r\n const easterMonth = Math.floor((h + l - 7 * m + 114) / 31)\r\n const easterDay = ((h + l - 7 * m + 114) % 31) + 1\r\n if (month === easterMonth && day === easterDay) return 'easter'\r\n // Pride: June 28 (Stonewall anniversary)\r\n if (month === 6 && day === 28) return 'pride'\r\n // Canada Day: July 1\r\n if (month === 7 && day === 1) return 'canadaday'\r\n // Costa Rica Independence Day: September 15\r\n if (month === 9 && day === 15) return 'costarica'\r\n // Independence Day: July 4\r\n if (month === 7 && day === 4) return 'july4th'\r\n // Halloween: Oct 30-31\r\n if (month === 10 && (day === 30 || day === 31)) return 'halloween'\r\n // Thanksgiving: 4th Thursday of November\r\n if (month === 11) {\r\n const firstDay = new Date(now.getFullYear(), 10, 1).getDay() // Day of week for Nov 1\r\n const fourthThursday = 1 + ((11 - firstDay) % 7) + 21 // Calculate 4th Thursday\r\n if (day === fourthThursday) return 'thanksgiving'\r\n }\r\n // Christmas: Dec 24-25\r\n if (month === 12 && (day === 24 || day === 25)) return 'christmas'\r\n\r\n return 'default'\r\n}\r\n\r\n/** Default nav icon colors */\r\nexport const NAV_ICON_COLORS: Record<string, string> = {\r\n // Primary navigation\r\n '/dashboard': 'text-blue-500',\r\n '/people': 'text-violet-500',\r\n '/share': 'text-cyan-500',\r\n '/access_code': 'text-amber-500',\r\n '/contact': 'text-emerald-500',\r\n '/credentials': 'text-amber-600',\r\n '/device': 'text-sky-500',\r\n '/insurance': 'text-indigo-500',\r\n '/legal': 'text-purple-500',\r\n '/financial': 'text-teal-500',\r\n '/pet': 'text-orange-500',\r\n '/password': 'text-amber-600',\r\n '/property': 'text-blue-500',\r\n '/service': 'text-pink-500',\r\n '/subscription': 'text-fuchsia-500',\r\n '/taxes': 'text-lime-500',\r\n '/valuables': 'text-yellow-500',\r\n '/vehicle': 'text-rose-500',\r\n '/continuity': 'text-red-500',\r\n '/search': 'text-sky-500',\r\n '/more': 'text-purple-500',\r\n '/signout': 'text-slate-500',\r\n '/settings': 'text-zinc-500',\r\n // Secondary tabs (sub-sections with different icons)\r\n 'records': 'text-slate-500',\r\n 'maintenance': 'text-amber-500',\r\n 'improvements': 'text-emerald-500',\r\n}\r\n\r\n/** Nav keys in display order for generating themed colors (optimized for mobile bottom nav + drawer) */\r\nconst NAV_KEYS = [\r\n // Bottom nav (always visible)\r\n '/dashboard', '/people', '/password', '/search', '/more',\r\n // More drawer - Group 1 (Assets)\r\n '/property', '/pet', '/valuables', '/vehicle',\r\n // More drawer - Group 2 (Financial & Services)\r\n '/subscription', '/financial', '/service', '/contact', '/insurance',\r\n // More drawer - Group 3 (Bottom row)\r\n '/signout', '/settings', '/continuity', '/share',\r\n // Other routes (sidebar, less common)\r\n '/access_code', '/credentials', '/device', '/legal', '/taxes',\r\n] as const\r\n\r\nconst SECONDARY_KEYS = ['records', 'maintenance', 'improvements'] as const\r\n\r\n/** Generate nav colors by cycling through a color palette */\r\nfunction cycleColors(colors: string[], secondary?: string[]): Record<string, string> {\r\n const result: Record<string, string> = {}\r\n NAV_KEYS.forEach((key, i) => { result[key] = colors[i % colors.length] })\r\n const sec = secondary || colors\r\n SECONDARY_KEYS.forEach((key, i) => { result[key] = sec[i % sec.length] })\r\n return result\r\n}\r\n\r\n/** Generate nav colors using stripe pattern (for flags) */\r\nfunction stripeColors(stripes: string[][]): Record<string, string> {\r\n const result: Record<string, string> = {}\r\n const itemsPerStripe = Math.ceil(NAV_KEYS.length / stripes.length)\r\n NAV_KEYS.forEach((key, i) => {\r\n const stripeIndex = Math.floor(i / itemsPerStripe)\r\n const stripe = stripes[Math.min(stripeIndex, stripes.length - 1)]\r\n result[key] = stripe[i % stripe.length]\r\n })\r\n SECONDARY_KEYS.forEach((key, i) => {\r\n result[key] = stripes[i % stripes.length][0]\r\n })\r\n return result\r\n}\r\n\r\n/** Christmas: red, green, gold */\r\nconst CHRISTMAS_NAV_COLORS = cycleColors([\r\n 'text-red-600', 'text-green-600', 'text-red-500', 'text-yellow-500',\r\n 'text-green-500', 'text-red-500', 'text-green-600',\r\n])\r\n\r\n/** Halloween: orange, purple, black */\r\nconst HALLOWEEN_NAV_COLORS = cycleColors([\r\n 'text-orange-500', 'text-purple-600', 'text-orange-600', 'text-purple-500',\r\n 'text-orange-500', 'text-purple-600', 'text-zinc-800',\r\n])\r\n\r\n/** Valentine's: pink, red, rose */\r\nconst VALENTINES_NAV_COLORS = cycleColors([\r\n 'text-pink-500', 'text-red-500', 'text-rose-500', 'text-pink-600',\r\n 'text-red-500', 'text-rose-500', 'text-red-600',\r\n])\r\n\r\n/** Pride: rainbow */\r\nconst PRIDE_NAV_COLORS = cycleColors([\r\n 'text-red-500', 'text-orange-500', 'text-yellow-500',\r\n 'text-green-500', 'text-blue-500', 'text-purple-500',\r\n])\r\n\r\n/** St. Patrick's: greens and gold */\r\nconst STPATRICKS_NAV_COLORS = cycleColors([\r\n 'text-green-600', 'text-green-500', 'text-yellow-500', 'text-emerald-500',\r\n])\r\n\r\n/** Canada Day: red and white */\r\nconst CANADADAY_NAV_COLORS = cycleColors(['text-red-600', 'text-slate-400', 'text-red-500'])\r\n\r\n/** July 4th: red, white, blue */\r\nconst JULY4TH_NAV_COLORS = cycleColors(['text-red-500', 'text-blue-600', 'text-slate-400', 'text-red-600', 'text-blue-500'])\r\n\r\n/** Thanksgiving: autumn colors */\r\nconst THANKSGIVING_NAV_COLORS = cycleColors(['text-orange-600', 'text-amber-700', 'text-yellow-600', 'text-orange-500', 'text-amber-600'])\r\n\r\n/** Earth Day: greens and blues */\r\nconst EARTHDAY_NAV_COLORS = cycleColors(['text-green-600', 'text-blue-500', 'text-emerald-500', 'text-sky-500', 'text-green-500', 'text-blue-600'])\r\n\r\n/** New Year's: gold and silver */\r\nconst NEWYEAR_NAV_COLORS = cycleColors(['text-yellow-500', 'text-slate-400', 'text-yellow-600', 'text-slate-500'])\r\n\r\n/** Easter: pastel spring colors */\r\nconst EASTER_NAV_COLORS = cycleColors(['text-pink-500', 'text-purple-500', 'text-yellow-500', 'text-emerald-500', 'text-sky-500'])\r\n\r\n/** Costa Rica: blue, white, red stripes (flag pattern) */\r\nconst COSTARICA_NAV_COLORS = stripeColors([\r\n ['text-blue-600', 'text-blue-500'], // Blue stripe\r\n ['text-slate-400'], // White stripe \r\n ['text-red-600', 'text-red-500'], // Red stripe (center)\r\n ['text-slate-400'], // White stripe\r\n ['text-blue-600', 'text-blue-500'], // Blue stripe\r\n])\r\n\r\n/** Map of holiday themes to their color palettes */\r\nexport const HOLIDAY_NAV_COLORS: Record<HolidayTheme, Record<string, string>> = {\r\n default: NAV_ICON_COLORS,\r\n christmas: CHRISTMAS_NAV_COLORS,\r\n halloween: HALLOWEEN_NAV_COLORS,\r\n valentines: VALENTINES_NAV_COLORS,\r\n pride: PRIDE_NAV_COLORS,\r\n stpatricks: STPATRICKS_NAV_COLORS,\r\n canadaday: CANADADAY_NAV_COLORS,\r\n july4th: JULY4TH_NAV_COLORS,\r\n thanksgiving: THANKSGIVING_NAV_COLORS,\r\n earthday: EARTHDAY_NAV_COLORS,\r\n newyear: NEWYEAR_NAV_COLORS,\r\n easter: EASTER_NAV_COLORS,\r\n costarica: COSTARICA_NAV_COLORS,\r\n}\r\n\r\n/**\r\n * Get nav icon colors for the current holiday theme.\r\n * Falls back to default colors if path not found in holiday theme.\r\n */\r\nexport function getNavIconColor(path: string): string {\r\n const theme = getCurrentHolidayTheme()\r\n const colors = HOLIDAY_NAV_COLORS[theme]\r\n return colors[path] || NAV_ICON_COLORS[path] || ''\r\n}\r\n\r\n/**\r\n * Animal icon names for pet nav rotation easter egg.\r\n * Hovering over the pet nav item will randomly change the icon to one of these animals.\r\n */\r\nexport const ANIMAL_ICON_NAMES = [\r\n 'PawPrint',\r\n 'Bird',\r\n 'Cat',\r\n 'Dog',\r\n 'Fish',\r\n 'Rabbit',\r\n 'Rat',\r\n 'Snail',\r\n 'Squirrel',\r\n 'Turtle',\r\n 'Bug',\r\n] as const\r\n\r\nexport type AnimalIconName = typeof ANIMAL_ICON_NAMES[number]\r\n\r\n/**\r\n * Feature flag for pet icon rotation easter egg.\r\n * Set to false to disable the rotating animal icons on pet nav hover.\r\n */\r\nexport const PET_ICON_ROTATION_ENABLED = true\r\n","/**\r\n * User Key Bundle Utilities\r\n *\r\n * Handles generation and encryption of user root keypairs (asymmetric keys).\r\n * These keys are used to wrap/unwrap household symmetric keys.\r\n *\r\n * Key Hierarchy:\r\n * - Recovery Key + server_wrap_secret → WrapKey → encrypts private key\r\n * - PRF output → PRF_key → encrypts private key (alternative path)\r\n * - Private key → unwraps household keys\r\n *\r\n * @module encryption/keyBundle\r\n */\r\n\r\nimport { base64Encode, base64Decode } from './utils';\r\nimport { DEFAULT_KEY_BUNDLE_ALG } from '@hearthcoo/types';\r\n\r\n/**\r\n * Supported asymmetric key algorithms\r\n */\r\nexport type KeyAlgorithm = 'P-521' | 'P-384' | 'P-256' | 'Ed25519';\r\n\r\n/**\r\n * Generate asymmetric keypair for user root key bundle\r\n *\r\n * Uses P-521 (ECDH) for maximum security and quantum resistance.\r\n *\r\n * @param algorithm - Key algorithm (default: P-521 for maximum security)\r\n * @returns Generated keypair with raw bytes\r\n *\r\n * @example\r\n * ```ts\r\n * const { publicKey, privateKey, publicKeyBytes, privateKeyBytes } =\r\n * await generateKeyPair();\r\n *\r\n * // Store public key in user_key_bundles.public_key\r\n * // Encrypt private key with WrapKey or PRF_key\r\n * ```\r\n */\r\nexport async function generateKeyPair(algorithm: KeyAlgorithm = 'P-521'): Promise<{\r\n publicKey: CryptoKey;\r\n privateKey: CryptoKey;\r\n publicKeyBytes: Uint8Array;\r\n privateKeyBytes: Uint8Array;\r\n algorithm: string;\r\n}> {\r\n let keyPair: CryptoKeyPair;\r\n let alg: string;\r\n\r\n switch (algorithm) {\r\n case 'P-521':\r\n // P-521 (NIST P-521) - Maximum security, quantum-resistant\r\n // ~256-bit security level\r\n keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-521'\r\n },\r\n true, // extractable\r\n ['deriveKey']\r\n ) as CryptoKeyPair;\r\n alg = 'ECDH-P-521';\r\n break;\r\n\r\n case 'P-384':\r\n // P-384 (NIST P-384) - High security\r\n // ~192-bit security level\r\n keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-384'\r\n },\r\n true,\r\n ['deriveKey']\r\n ) as CryptoKeyPair;\r\n alg = 'ECDH-P-384';\r\n break;\r\n\r\n case 'P-256':\r\n // P-256 (NIST P-256) - Standard security\r\n // ~128-bit security level\r\n keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-256'\r\n },\r\n true,\r\n ['deriveKey']\r\n ) as CryptoKeyPair;\r\n alg = 'ECDH-P-256';\r\n break;\r\n\r\n case 'Ed25519':\r\n // Note: Ed25519 is for signing, not encryption\r\n // Would need X25519 for ECDH, which isn't widely supported in WebCrypto yet\r\n throw new Error('Ed25519 not yet supported - use P-521 for maximum security');\r\n\r\n default:\r\n throw new Error(`Unsupported algorithm: ${algorithm}`);\r\n }\r\n\r\n // Export keys to raw bytes\r\n const publicKeyBytes = new Uint8Array(\r\n await crypto.subtle.exportKey('raw', keyPair.publicKey)\r\n );\r\n\r\n const privateKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);\r\n const privateKeyBytes = new TextEncoder().encode(JSON.stringify(privateKeyJwk));\r\n\r\n return {\r\n publicKey: keyPair.publicKey,\r\n privateKey: keyPair.privateKey,\r\n publicKeyBytes,\r\n privateKeyBytes,\r\n algorithm: alg\r\n };\r\n}\r\n\r\n/**\r\n * Encrypt private key with WrapKey (derived from Recovery Key + server_wrap_secret)\r\n *\r\n * @param privateKeyBytes - Private key raw bytes\r\n * @param wrapKey - WrapKey from deriveWrapKey()\r\n * @returns Encrypted private key (base64-encoded packed blob)\r\n *\r\n * @example\r\n * ```ts\r\n * const wrapKey = await deriveWrapKey(recoveryKey, serverWrapSecret);\r\n * const encryptedPrivateKey = await encryptPrivateKeyWithWrapKey(\r\n * privateKeyBytes,\r\n * wrapKey\r\n * );\r\n *\r\n * // Store in user_key_bundles.encrypted_private_key\r\n * ```\r\n */\r\nexport async function encryptPrivateKeyWithWrapKey(\r\n privateKeyBytes: Uint8Array,\r\n wrapKey: CryptoKey\r\n): Promise<string> {\r\n // Generate IV\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n\r\n // Encrypt with AES-GCM\r\n const ciphertext = await crypto.subtle.encrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n },\r\n wrapKey,\r\n privateKeyBytes as BufferSource\r\n );\r\n\r\n // Pack: [version (1 byte)][IV (12 bytes)][ciphertext + auth tag]\r\n const version = new Uint8Array([1]);\r\n const packed = new Uint8Array(1 + 12 + ciphertext.byteLength);\r\n packed.set(version, 0);\r\n packed.set(iv, 1);\r\n packed.set(new Uint8Array(ciphertext), 13);\r\n\r\n return base64Encode(packed);\r\n}\r\n\r\n/**\r\n * Decrypt private key with WrapKey\r\n *\r\n * @param encryptedPrivateKey - Encrypted private key (base64-encoded)\r\n * @param wrapKey - WrapKey from deriveWrapKey()\r\n * @returns Decrypted private key bytes\r\n *\r\n * @example\r\n * ```ts\r\n * const wrapKey = await deriveWrapKey(recoveryKey, serverWrapSecret);\r\n * const privateKeyBytes = await decryptPrivateKeyWithWrapKey(\r\n * encryptedPrivateKey,\r\n * wrapKey\r\n * );\r\n * ```\r\n */\r\nexport async function decryptPrivateKeyWithWrapKey(\r\n encryptedPrivateKey: string,\r\n wrapKey: CryptoKey\r\n): Promise<Uint8Array> {\r\n const packed = base64Decode(encryptedPrivateKey);\r\n\r\n // Unpack\r\n const version = packed[0];\r\n if (version !== 1) {\r\n throw new Error(`Unsupported encryption version: ${version}`);\r\n }\r\n\r\n const iv = packed.slice(1, 13);\r\n const ciphertext = packed.slice(13);\r\n\r\n // Decrypt with AES-GCM\r\n const plaintextBuffer = await crypto.subtle.decrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv\r\n },\r\n wrapKey,\r\n ciphertext\r\n );\r\n\r\n return new Uint8Array(plaintextBuffer);\r\n}\r\n\r\n/**\r\n * Import private key from bytes for use in key derivation\r\n *\r\n * @param privateKeyBytes - Private key bytes (JWK format as JSON string)\r\n * @param algorithm - Key algorithm\r\n * @returns Imported CryptoKey\r\n */\r\nexport async function importPrivateKey(\r\n privateKeyBytes: Uint8Array,\r\n algorithm: string = DEFAULT_KEY_BUNDLE_ALG\r\n): Promise<CryptoKey> {\r\n // Parse JWK from bytes\r\n const jwkString = new TextDecoder().decode(privateKeyBytes);\r\n const jwk = JSON.parse(jwkString);\r\n\r\n // Determine curve from algorithm\r\n let namedCurve: string;\r\n if (algorithm.includes('P-521')) {\r\n namedCurve = 'P-521';\r\n } else if (algorithm.includes('P-384')) {\r\n namedCurve = 'P-384';\r\n } else if (algorithm.includes('P-256')) {\r\n namedCurve = 'P-256';\r\n } else {\r\n throw new Error(`Unsupported algorithm: ${algorithm}`);\r\n }\r\n\r\n // Import private key\r\n return await crypto.subtle.importKey(\r\n 'jwk',\r\n jwk,\r\n {\r\n name: 'ECDH',\r\n namedCurve\r\n },\r\n true,\r\n ['deriveKey']\r\n );\r\n}\r\n\r\n/**\r\n * Import public key from bytes\r\n *\r\n * @param publicKeyBytes - Public key raw bytes\r\n * @param algorithm - Key algorithm\r\n * @returns Imported CryptoKey\r\n */\r\nexport async function importPublicKey(\r\n publicKeyBytes: Uint8Array,\r\n algorithm: string = DEFAULT_KEY_BUNDLE_ALG\r\n): Promise<CryptoKey> {\r\n // Determine curve from algorithm\r\n let namedCurve: string;\r\n if (algorithm.includes('P-521')) {\r\n namedCurve = 'P-521';\r\n } else if (algorithm.includes('P-384')) {\r\n namedCurve = 'P-384';\r\n } else if (algorithm.includes('P-256')) {\r\n namedCurve = 'P-256';\r\n } else {\r\n throw new Error(`Unsupported algorithm: ${algorithm}`);\r\n }\r\n\r\n return await crypto.subtle.importKey(\r\n 'raw',\r\n publicKeyBytes as BufferSource,\r\n {\r\n name: 'ECDH',\r\n namedCurve\r\n },\r\n true,\r\n []\r\n );\r\n}\r\n","/**\r\n * Asymmetric Key Wrapping with ECDH\r\n *\r\n * Implements ECIES (Elliptic Curve Integrated Encryption Scheme) to wrap/unwrap\r\n * symmetric household keys using user's P-521 public/private keys.\r\n *\r\n * Algorithm:\r\n * 1. Generate ephemeral P-521 keypair\r\n * 2. Perform ECDH with (ephemeral_private + user_public) → shared_secret\r\n * 3. Derive AES-256-GCM key from shared_secret using HKDF\r\n * 4. Encrypt household_key with AES-256-GCM\r\n * 5. Output: [ephemeral_public][IV][ciphertext][auth_tag]\r\n *\r\n * @module encryption/asymmetricWrap\r\n */\r\n\r\nimport { base64Encode, base64Decode } from './utils';\r\nimport { DEFAULT_KEY_BUNDLE_ALG } from '@hearthcoo/types';\r\n\r\n/**\r\n * Wrap (encrypt) a household key using a user's public key (ECIES)\r\n *\r\n * @param householdKey - Symmetric household key to wrap (32 bytes AES-256)\r\n * @param publicKey - User's P-521 public key\r\n * @param algorithm - Algorithm string (e.g., 'ECDH-P-521')\r\n * @returns Base64-encoded wrapped key blob\r\n *\r\n * @example\r\n * ```ts\r\n * const publicKey = await importPublicKey(publicKeyBytes, 'ECDH-P-521');\r\n * const wrappedKey = await wrapHouseholdKey(householdKeyBytes, publicKey, 'ECDH-P-521');\r\n * // Store wrappedKey in member_key_access.wrapped_key\r\n * ```\r\n */\r\nexport async function wrapHouseholdKey(\r\n householdKey: Uint8Array,\r\n publicKey: CryptoKey,\r\n algorithm: string = DEFAULT_KEY_BUNDLE_ALG\r\n): Promise<string> {\r\n // Determine curve from algorithm\r\n const namedCurve = algorithm.includes('P-521') ? 'P-521'\r\n : algorithm.includes('P-384') ? 'P-384'\r\n : algorithm.includes('P-256') ? 'P-256'\r\n : (() => { throw new Error(`Unsupported algorithm: ${algorithm}`); })();\r\n\r\n // 1. Generate ephemeral keypair\r\n const ephemeralKeyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve\r\n },\r\n true, // extractable\r\n ['deriveKey']\r\n ) as CryptoKeyPair;\r\n\r\n // 2. Perform ECDH: derive shared secret\r\n const sharedSecret = await crypto.subtle.deriveKey(\r\n {\r\n name: 'ECDH',\r\n public: publicKey\r\n },\r\n ephemeralKeyPair.privateKey,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false, // not extractable (security)\r\n ['encrypt']\r\n );\r\n\r\n // 3. Generate IV for AES-GCM\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n\r\n // 4. Encrypt household key with derived AES key\r\n const ciphertext = await crypto.subtle.encrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n },\r\n sharedSecret,\r\n householdKey as BufferSource\r\n );\r\n\r\n // 5. Export ephemeral public key\r\n const ephemeralPublicKeyBytes = new Uint8Array(\r\n await crypto.subtle.exportKey('raw', ephemeralKeyPair.publicKey)\r\n );\r\n\r\n // Pack: [version (1 byte)][ephemeral_public_key][IV (12 bytes)][ciphertext + auth_tag]\r\n const version = new Uint8Array([1]);\r\n const packed = new Uint8Array(\r\n 1 + ephemeralPublicKeyBytes.length + 12 + ciphertext.byteLength\r\n );\r\n\r\n let offset = 0;\r\n packed.set(version, offset);\r\n offset += 1;\r\n packed.set(ephemeralPublicKeyBytes, offset);\r\n offset += ephemeralPublicKeyBytes.length;\r\n packed.set(iv, offset);\r\n offset += 12;\r\n packed.set(new Uint8Array(ciphertext), offset);\r\n\r\n return base64Encode(packed);\r\n}\r\n\r\n/**\r\n * Unwrap (decrypt) a household key using a user's private key (ECIES)\r\n *\r\n * @param wrappedKey - Base64-encoded wrapped key blob\r\n * @param privateKey - User's P-521 private key\r\n * @param algorithm - Algorithm string (e.g., 'ECDH-P-521')\r\n * @returns Decrypted household key bytes\r\n *\r\n * @example\r\n * ```ts\r\n * const privateKey = await importPrivateKey(privateKeyBytes, 'ECDH-P-521');\r\n * const householdKey = await unwrapHouseholdKey(wrappedKeyBase64, privateKey, 'ECDH-P-521');\r\n * ```\r\n */\r\nexport async function unwrapHouseholdKey(\r\n wrappedKey: string,\r\n privateKey: CryptoKey,\r\n algorithm: string = DEFAULT_KEY_BUNDLE_ALG\r\n): Promise<Uint8Array> {\r\n const packed = base64Decode(wrappedKey);\r\n\r\n // Unpack version\r\n const version = packed[0];\r\n if (version !== 1) {\r\n throw new Error(`Unsupported wrap version: ${version}`);\r\n }\r\n\r\n // Determine curve and ephemeral public key size\r\n const namedCurve = algorithm.includes('P-521') ? 'P-521'\r\n : algorithm.includes('P-384') ? 'P-384'\r\n : algorithm.includes('P-256') ? 'P-256'\r\n : (() => { throw new Error(`Unsupported algorithm: ${algorithm}`); })();\r\n\r\n // Public key sizes (uncompressed point format: 0x04 + x + y)\r\n const publicKeySize = namedCurve === 'P-521' ? 133 // 1 + 66 + 66\r\n : namedCurve === 'P-384' ? 97 // 1 + 48 + 48\r\n : 65; // P-256: 1 + 32 + 32\r\n\r\n // Unpack components\r\n let offset = 1;\r\n const ephemeralPublicKeyBytes = packed.slice(offset, offset + publicKeySize);\r\n offset += publicKeySize;\r\n\r\n const iv = packed.slice(offset, offset + 12);\r\n offset += 12;\r\n\r\n const ciphertext = packed.slice(offset);\r\n\r\n // Import ephemeral public key\r\n const ephemeralPublicKey = await crypto.subtle.importKey(\r\n 'raw',\r\n ephemeralPublicKeyBytes as BufferSource,\r\n {\r\n name: 'ECDH',\r\n namedCurve\r\n },\r\n false,\r\n []\r\n );\r\n\r\n // Perform ECDH: derive shared secret\r\n const sharedSecret = await crypto.subtle.deriveKey(\r\n {\r\n name: 'ECDH',\r\n public: ephemeralPublicKey\r\n },\r\n privateKey,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false,\r\n ['decrypt']\r\n );\r\n\r\n // Decrypt household key\r\n const plaintextBuffer = await crypto.subtle.decrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv\r\n },\r\n sharedSecret,\r\n ciphertext\r\n );\r\n\r\n return new Uint8Array(plaintextBuffer);\r\n}\r\n","/**\r\n * WebAuthn Device-Bound Storage (Windows Hello Alternative to PRF)\r\n *\r\n * Provides biometric unlock on platforms without PRF support (Windows).\r\n * Uses WebAuthn for authentication + device fingerprinting for key derivation.\r\n *\r\n * ## Security Model\r\n *\r\n * Without PRF, we can't get deterministic secrets from WebAuthn directly.\r\n * Instead, we use a \"device binding\" approach:\r\n *\r\n * 1. Generate random \"device unlock key\" (DUK) - 32 bytes\r\n * 2. Derive \"device binding key\" (DBK) from:\r\n * - credentialId (deterministic, not secret)\r\n * - device fingerprint (browser/OS info, not secret)\r\n * - random salt (stored, not secret)\r\n * 3. Encrypt DUK with DBK → store in localStorage\r\n * 4. Encrypt user private key with DUK → store on server\r\n *\r\n * On unlock:\r\n * - User authenticates with Windows Hello (proves identity)\r\n * - Derive DBK from credentialId (obtained after auth) + fingerprint + salt\r\n * - Decrypt DUK\r\n * - Download encrypted private key from server\r\n * - Decrypt private key with DUK\r\n *\r\n * ## Security Properties\r\n *\r\n * - ✅ Biometric authentication required (Windows Hello)\r\n * - ✅ Server-side access control (encrypted key requires auth to download)\r\n * - ✅ Device-bound (can't easily export)\r\n * - ⚠️ If attacker compromises account AND clones device, could decrypt\r\n * - ⚠️ Recovery key still needed for device loss/reset\r\n *\r\n * ## Platform Support\r\n *\r\n * - ✅ Windows (all browsers with WebAuthn)\r\n * - ✅ Linux\r\n * - ✅ Android (fingerprint/face unlock)\r\n * - ⚠️ Don't use on Apple devices - use PRF instead (more secure)\r\n *\r\n * @module encryption/webauthnDeviceBound\r\n */\r\n\r\nimport { base64Encode, base64Decode } from './utils';\r\n\r\n/**\r\n * Relying Party ID for WebAuthn\r\n */\r\nexport const RP_ID = typeof window !== 'undefined'\r\n ? window.location.hostname\r\n : 'estatehelm.com';\r\n\r\n/**\r\n * Relying Party Name\r\n */\r\nexport const RP_NAME = 'EstateHelm';\r\n\r\n/**\r\n * Storage key prefix for device-bound credentials\r\n * Used by webauthn device-bound and trusted-device modules\r\n */\r\nexport const DEVICE_BOUND_STORAGE_PREFIX = 'hearthcoo_device_bound_';\r\n\r\n// Note: Device fingerprint functions removed in v2\r\n// They were too unstable (changed with browser updates, screen resolution, etc.)\r\n// Windows Hello authentication via TPM is sufficient for device binding\r\n\r\n/**\r\n * Device-bound credential data stored in localStorage\r\n */\r\ninterface DeviceBoundCredential {\r\n /** Credential ID (base64) */\r\n credentialId: string;\r\n\r\n /** Random salt for key derivation (base64) */\r\n salt: string;\r\n\r\n /** Encrypted device unlock key (base64) */\r\n encryptedDeviceKey: string;\r\n\r\n /** Public key (base64, for server verification) */\r\n publicKey: string;\r\n\r\n /** Relying Party ID */\r\n rpId: string;\r\n}\r\n\r\n/**\r\n * Registration result\r\n */\r\nexport interface DeviceBoundRegistrationResult {\r\n /** Credential ID */\r\n credentialId: Uint8Array;\r\n\r\n /** Public key */\r\n publicKey: Uint8Array;\r\n\r\n /** Device unlock key (use this to encrypt user's private key) */\r\n deviceUnlockKey: Uint8Array;\r\n\r\n /** Relying Party ID */\r\n rpId: string;\r\n\r\n /** Authenticator Attestation GUID - identifies the authenticator type */\r\n aaguid: string | null;\r\n\r\n /** Salt used for key derivation (needed to reconstruct binding key) */\r\n salt: Uint8Array;\r\n\r\n /** Encrypted device key (deviceUnlockKey encrypted with binding key) */\r\n encryptedDeviceKey: string;\r\n}\r\n\r\n/**\r\n * Register a new Windows Hello credential with device binding\r\n *\r\n * @param userId - User's Kratos identity ID\r\n * @param userEmail - User's email\r\n * @param excludeCredentials - Optional list of credential IDs to exclude (prevent duplicates)\r\n * @returns Registration result with device unlock key\r\n *\r\n * @example\r\n * ```ts\r\n * // During household setup on Windows\r\n * const result = await registerDeviceBoundCredential(\r\n * user.id,\r\n * user.traits.email\r\n * );\r\n *\r\n * // Use device unlock key to encrypt private key\r\n * const { key: deviceKey, salt } = await deriveDeviceKey(result.deviceUnlockKey);\r\n * const encrypted = await encryptWithDeviceKey(deviceKey, salt, privateKeyBytes);\r\n *\r\n * // Store on server\r\n * await api.post('/webauthn/device-bound-envelopes', {\r\n * credentialId: base64Encode(result.credentialId),\r\n * publicKey: base64Encode(result.publicKey),\r\n * encryptedPrivateKey: encrypted\r\n * });\r\n * ```\r\n */\r\nexport async function registerDeviceBoundCredential(\r\n userId: string,\r\n userEmail: string,\r\n excludeCredentials?: Uint8Array[]\r\n): Promise<DeviceBoundRegistrationResult> {\r\n // Generate device unlock key (this will encrypt the user's private key)\r\n const deviceUnlockKey = crypto.getRandomValues(new Uint8Array(32));\r\n\r\n // Generate salt for key derivation\r\n const salt = crypto.getRandomValues(new Uint8Array(32));\r\n\r\n // Build excludeCredentials list to prevent duplicates\r\n const excludeCredentialsList = excludeCredentials?.map(id => ({\r\n id: id as BufferSource,\r\n type: 'public-key' as const,\r\n transports: ['internal' as AuthenticatorTransport]\r\n })) || [];\r\n\r\n // Create WebAuthn credential\r\n const userIdBytes = new TextEncoder().encode(userId);\r\n\r\n const publicKeyOptions: PublicKeyCredentialCreationOptions = {\r\n challenge: crypto.getRandomValues(new Uint8Array(32)),\r\n\r\n rp: {\r\n id: RP_ID,\r\n name: RP_NAME\r\n },\r\n\r\n user: {\r\n id: userIdBytes,\r\n name: userEmail,\r\n displayName: userEmail\r\n },\r\n\r\n pubKeyCredParams: [\r\n { alg: -7, type: 'public-key' }, // ES256\r\n { alg: -257, type: 'public-key' } // RS256\r\n ],\r\n\r\n authenticatorSelection: {\r\n authenticatorAttachment: 'platform',\r\n // Use 'discouraged' to force local Windows Hello (non-syncing)\r\n // 'required' creates discoverable credentials which Windows treats as passkeys\r\n requireResidentKey: false,\r\n residentKey: 'discouraged',\r\n userVerification: 'required'\r\n },\r\n\r\n excludeCredentials: excludeCredentialsList,\r\n\r\n timeout: 60000\r\n };\r\n\r\n console.log('[DeviceBound] Creating credential...', {\r\n rpId: RP_ID,\r\n userId,\r\n userEmail,\r\n options: publicKeyOptions\r\n });\r\n\r\n let credential: Credential | null;\r\n try {\r\n credential = await navigator.credentials.create({\r\n publicKey: publicKeyOptions\r\n });\r\n } catch (error: any) {\r\n console.error('[DeviceBound] Credential creation failed:', error);\r\n\r\n // Provide more helpful error messages\r\n if (error.name === 'NotAllowedError') {\r\n throw new Error('Windows Hello prompt was cancelled or timed out. Please try again and approve the Windows Hello prompt when it appears.');\r\n } else if (error.name === 'SecurityError') {\r\n throw new Error('Security error - WebAuthn may not be available on this domain. Try using localhost or HTTPS.');\r\n } else if (error.name === 'InvalidStateError') {\r\n throw new Error('This credential already exists. Try clearing existing credentials first.');\r\n } else {\r\n throw new Error(`Failed to create device-bound credential: ${error.message || 'Unknown error'}`);\r\n }\r\n }\r\n\r\n if (!credential) {\r\n throw new Error('Credential creation returned null');\r\n }\r\n\r\n const publicKeyCredential = credential as PublicKeyCredential;\r\n const response = publicKeyCredential.response as AuthenticatorAttestationResponse;\r\n\r\n const credentialId = new Uint8Array(publicKeyCredential.rawId);\r\n const publicKey = new Uint8Array(response.getPublicKey() || []);\r\n\r\n if (publicKey.length === 0) {\r\n throw new Error('Failed to extract public key from credential');\r\n }\r\n\r\n // Extract aaguid from authenticatorData\r\n // Structure: rpIdHash (32) + flags (1) + signCount (4) + aaguid (16) + ...\r\n let aaguid: string | null = null;\r\n try {\r\n const authData = new Uint8Array(response.getAuthenticatorData());\r\n if (authData.length >= 53) {\r\n const aaguidBytes = authData.slice(37, 53);\r\n // Format as UUID string: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\r\n const hex = Array.from(aaguidBytes).map(b => b.toString(16).padStart(2, '0')).join('');\r\n aaguid = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\r\n console.log('[DeviceBound] Extracted aaguid:', aaguid);\r\n }\r\n } catch (err) {\r\n console.warn('[DeviceBound] Failed to extract aaguid:', err);\r\n }\r\n\r\n // Derive device binding key from credentialId + device fingerprint + salt\r\n const deviceBindingKey = await deriveDeviceBindingKey(credentialId, salt);\r\n\r\n // Encrypt device unlock key with device binding key\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n const encryptedDeviceKey = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n deviceBindingKey,\r\n deviceUnlockKey as BufferSource\r\n );\r\n\r\n // Pack encrypted key: [iv (12 bytes)][ciphertext + auth tag]\r\n const packed = new Uint8Array(12 + encryptedDeviceKey.byteLength);\r\n packed.set(iv, 0);\r\n packed.set(new Uint8Array(encryptedDeviceKey), 12);\r\n\r\n // Store in localStorage\r\n const storageData: DeviceBoundCredential = {\r\n credentialId: base64Encode(credentialId),\r\n salt: base64Encode(salt),\r\n encryptedDeviceKey: base64Encode(packed),\r\n publicKey: base64Encode(publicKey),\r\n rpId: RP_ID\r\n };\r\n\r\n localStorage.setItem(\r\n `${DEVICE_BOUND_STORAGE_PREFIX}${userId}`,\r\n JSON.stringify(storageData)\r\n );\r\n\r\n console.log('[DeviceBound] ✓ Credential registered and stored locally');\r\n\r\n return {\r\n credentialId,\r\n publicKey,\r\n deviceUnlockKey,\r\n rpId: RP_ID,\r\n aaguid,\r\n salt,\r\n encryptedDeviceKey: base64Encode(packed)\r\n };\r\n}\r\n\r\n/**\r\n * Register a trusted device (low-security fallback, no WebAuthn)\r\n * Stores device unlock key in localStorage - anyone with device access can unlock\r\n *\r\n * @param userId - User's Kratos identity ID\r\n * @param userEmail - User's email\r\n * @returns Registration result with device unlock key and fake credential info\r\n */\r\nexport async function registerTrustedDeviceCredential(\r\n userId: string,\r\n _userEmail: string\r\n): Promise<{\r\n credentialId: Uint8Array;\r\n publicKey: Uint8Array;\r\n deviceUnlockKey: Uint8Array;\r\n deviceId: string;\r\n rpId: string;\r\n}> {\r\n console.log('[TrustedDevice] Creating trusted device credential (low-security)...');\r\n\r\n // Generate device unlock key\r\n const deviceUnlockKey = crypto.getRandomValues(new Uint8Array(32));\r\n\r\n // Generate device ID (UUID-like)\r\n const deviceId = `trusted-${crypto.randomUUID()}`;\r\n\r\n // Generate fake credential ID (for API consistency)\r\n const credentialId = crypto.getRandomValues(new Uint8Array(16));\r\n\r\n // Generate fake public key (for API consistency)\r\n const publicKey = crypto.getRandomValues(new Uint8Array(32));\r\n\r\n // Store in localStorage (plaintext - this is low security!)\r\n const storageData = {\r\n deviceId,\r\n deviceUnlockKey: base64Encode(deviceUnlockKey),\r\n credentialId: base64Encode(credentialId),\r\n publicKey: base64Encode(publicKey),\r\n rpId: RP_ID,\r\n type: 'trusted-device'\r\n };\r\n\r\n localStorage.setItem(\r\n `${DEVICE_BOUND_STORAGE_PREFIX}${userId}`,\r\n JSON.stringify(storageData)\r\n );\r\n\r\n console.log('[TrustedDevice] ✓ Trusted device credential registered (stored in localStorage)');\r\n\r\n return {\r\n credentialId,\r\n publicKey,\r\n deviceUnlockKey,\r\n deviceId,\r\n rpId: RP_ID\r\n };\r\n}\r\n\r\n/**\r\n * Derive device binding key from credentialId + salt\r\n *\r\n * Note: Previously included device fingerprint, but that was too unstable\r\n * (changed with browser updates, screen resolution, etc.). Windows Hello\r\n * authentication itself already proves device identity via TPM.\r\n */\r\nasync function deriveDeviceBindingKey(\r\n credentialId: Uint8Array,\r\n salt: Uint8Array\r\n): Promise<CryptoKey> {\r\n // Import credentialId as key material\r\n // The credentialId is unique per device and tied to the TPM\r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n credentialId as BufferSource,\r\n 'HKDF',\r\n false,\r\n ['deriveKey']\r\n );\r\n\r\n // Derive AES-GCM key using HKDF\r\n // Using v2 info string since we removed fingerprint from derivation\r\n const bindingKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: salt as BufferSource,\r\n info: new TextEncoder().encode('hearthcoo-device-binding-v2')\r\n },\r\n keyMaterial,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false,\r\n ['encrypt', 'decrypt']\r\n );\r\n\r\n return bindingKey;\r\n}\r\n\r\n/**\r\n * Authentication result\r\n */\r\nexport interface DeviceBoundAuthenticationResult {\r\n /** Device unlock key (use this to decrypt user's private key) */\r\n deviceUnlockKey: Uint8Array;\r\n\r\n /** Signature (for server verification if needed, not present for trusted-device) */\r\n signature?: Uint8Array;\r\n\r\n /** Authenticator data (not present for trusted-device) */\r\n authenticatorData?: Uint8Array;\r\n\r\n /** Client data JSON (not present for trusted-device) */\r\n clientDataJSON?: Uint8Array;\r\n}\r\n\r\n/**\r\n * Authenticate with Windows Hello and get device unlock key\r\n *\r\n * @param userId - User's Kratos identity ID\r\n * @returns Authentication result with device unlock key\r\n *\r\n * @example\r\n * ```ts\r\n * try {\r\n * // User taps Windows Hello\r\n * const result = await authenticateDeviceBound(user.id);\r\n *\r\n * // Use device unlock key to decrypt private key (from server)\r\n * const privateKey = await decryptWithDeviceKey(\r\n * result.deviceUnlockKey,\r\n * encryptedPrivateKey\r\n * );\r\n * } catch (err) {\r\n * console.error('Windows Hello auth failed:', err);\r\n * }\r\n * ```\r\n */\r\nexport async function authenticateDeviceBound(\r\n userId: string\r\n): Promise<DeviceBoundAuthenticationResult> {\r\n // Load stored credential data\r\n const storageData = localStorage.getItem(`${DEVICE_BOUND_STORAGE_PREFIX}${userId}`);\r\n if (!storageData) {\r\n throw new Error('No device-bound credential found for this user. Please set up biometric unlock first.');\r\n }\r\n\r\n const credentialData = JSON.parse(storageData);\r\n\r\n // Check if this is a trusted-device (no WebAuthn, just localStorage)\r\n if (credentialData.type === 'trusted-device') {\r\n console.log('[TrustedDevice] Using trusted device unlock (no biometric prompt)');\r\n\r\n const deviceUnlockKey = base64Decode(credentialData.deviceUnlockKey);\r\n\r\n return {\r\n deviceUnlockKey\r\n };\r\n }\r\n\r\n // For real device-bound credentials, proceed with WebAuthn\r\n const credential: DeviceBoundCredential = credentialData;\r\n const credentialId = base64Decode(credential.credentialId);\r\n const salt = base64Decode(credential.salt);\r\n const encryptedDeviceKeyPacked = base64Decode(credential.encryptedDeviceKey);\r\n\r\n // Authenticate with WebAuthn\r\n const publicKeyOptions: PublicKeyCredentialRequestOptions = {\r\n challenge: crypto.getRandomValues(new Uint8Array(32)),\r\n rpId: RP_ID,\r\n allowCredentials: [{\r\n id: credentialId as BufferSource,\r\n type: 'public-key',\r\n transports: ['internal']\r\n }],\r\n timeout: 60000,\r\n userVerification: 'required'\r\n };\r\n\r\n console.log('[DeviceBound] Authenticating...', {\r\n credentialIdLength: credentialId.length\r\n });\r\n\r\n let authCredential: Credential | null;\r\n try {\r\n authCredential = await navigator.credentials.get({\r\n publicKey: publicKeyOptions\r\n });\r\n } catch (error) {\r\n console.error('[DeviceBound] Authentication failed:', error);\r\n throw new Error(`Failed to authenticate with device-bound credential: ${error instanceof Error ? error.message : 'Unknown error'}`);\r\n }\r\n\r\n if (!authCredential) {\r\n throw new Error('Authentication returned null');\r\n }\r\n\r\n const publicKeyCredential = authCredential as PublicKeyCredential;\r\n const response = publicKeyCredential.response as AuthenticatorAssertionResponse;\r\n\r\n // Derive device binding key (same as registration)\r\n const deviceBindingKey = await deriveDeviceBindingKey(credentialId, salt);\r\n\r\n // Decrypt device unlock key\r\n const iv = encryptedDeviceKeyPacked.slice(0, 12);\r\n const encryptedDeviceKey = encryptedDeviceKeyPacked.slice(12);\r\n\r\n let deviceUnlockKeyBuffer: ArrayBuffer;\r\n try {\r\n deviceUnlockKeyBuffer = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n deviceBindingKey,\r\n encryptedDeviceKey as BufferSource\r\n );\r\n } catch (error) {\r\n console.error('[DeviceBound] Failed to decrypt device unlock key:', error);\r\n throw new Error('Failed to decrypt device unlock key. Device fingerprint may have changed.');\r\n }\r\n\r\n const deviceUnlockKey = new Uint8Array(deviceUnlockKeyBuffer);\r\n\r\n console.log('[DeviceBound] ✓ Authentication successful, device unlock key retrieved');\r\n\r\n return {\r\n deviceUnlockKey,\r\n signature: new Uint8Array(response.signature),\r\n authenticatorData: new Uint8Array(response.authenticatorData),\r\n clientDataJSON: new Uint8Array(response.clientDataJSON)\r\n };\r\n}\r\n\r\n/**\r\n * Derive encryption key from device unlock key using HKDF\r\n *\r\n * Similar to derivePRFKey but for device-bound keys.\r\n *\r\n * @param deviceUnlockKey - Device unlock key from registration/authentication\r\n * @param salt - Optional salt for HKDF (default: random 32 bytes)\r\n * @param info - Context info string\r\n * @returns Object with derived key and the salt used\r\n */\r\nexport async function deriveDeviceKey(\r\n deviceUnlockKey: Uint8Array,\r\n salt?: Uint8Array,\r\n info: string = 'hearthcoo-device-key-v1'\r\n): Promise<{ key: CryptoKey; salt: Uint8Array }> {\r\n if (deviceUnlockKey.length !== 32) {\r\n throw new Error('Device unlock key must be 32 bytes');\r\n }\r\n\r\n // Use random salt for maximum security\r\n const hkdfSalt = salt || crypto.getRandomValues(new Uint8Array(32));\r\n\r\n // Import device unlock key as key material\r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n deviceUnlockKey as BufferSource,\r\n 'HKDF',\r\n false,\r\n ['deriveKey']\r\n );\r\n\r\n // Derive AES-GCM key using HKDF\r\n const deviceKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: hkdfSalt as BufferSource,\r\n info: new TextEncoder().encode(info)\r\n },\r\n keyMaterial,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false,\r\n ['encrypt', 'decrypt']\r\n );\r\n\r\n return { key: deviceKey, salt: hkdfSalt };\r\n}\r\n\r\n/**\r\n * Encrypt data with device-derived key\r\n *\r\n * @param deviceKey - Key derived from device unlock key\r\n * @param hkdfSalt - HKDF salt used to derive the key\r\n * @param data - Data to encrypt\r\n * @returns Encrypted blob with format: [version][salt][IV][ciphertext][auth_tag]\r\n */\r\nexport async function encryptWithDeviceKey<T>(\r\n deviceKey: CryptoKey,\r\n hkdfSalt: Uint8Array,\r\n data: T\r\n): Promise<string> {\r\n // Convert data to bytes\r\n let plaintextBytes: Uint8Array;\r\n\r\n if (data instanceof Uint8Array) {\r\n plaintextBytes = data;\r\n } else if (typeof data === 'string') {\r\n plaintextBytes = new TextEncoder().encode(data);\r\n } else {\r\n const plaintext = JSON.stringify(data);\r\n plaintextBytes = new TextEncoder().encode(plaintext);\r\n }\r\n\r\n // Generate IV\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n\r\n // Encrypt with AES-GCM\r\n const ciphertext = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n deviceKey,\r\n plaintextBytes as BufferSource\r\n );\r\n\r\n // Pack into blob: [version (1 byte)][salt (32 bytes)][IV (12 bytes)][ciphertext + auth tag]\r\n const version = new Uint8Array([1]); // Version 1\r\n const packed = new Uint8Array(1 + 32 + 12 + ciphertext.byteLength);\r\n packed.set(version, 0);\r\n packed.set(hkdfSalt, 1);\r\n packed.set(iv, 33);\r\n packed.set(new Uint8Array(ciphertext), 45);\r\n\r\n return base64Encode(packed);\r\n}\r\n\r\n/**\r\n * Decrypt data with device unlock key\r\n *\r\n * @param deviceUnlockKey - Device unlock key from authentication\r\n * @param encryptedBlob - Encrypted blob from encryptWithDeviceKey\r\n * @param returnRawBytes - If true, returns raw bytes without JSON parsing\r\n * @returns Decrypted data\r\n */\r\nexport async function decryptWithDeviceKey<T = any>(\r\n deviceUnlockKey: Uint8Array,\r\n encryptedBlob: string,\r\n returnRawBytes: boolean = false\r\n): Promise<T> {\r\n const packed = base64Decode(encryptedBlob);\r\n\r\n // Unpack blob\r\n const version = packed[0];\r\n\r\n if (version !== 1) {\r\n throw new Error(`Unsupported encryption version: ${version}`);\r\n }\r\n\r\n // Version 1 format: [version (1 byte)][salt (32 bytes)][IV (12 bytes)][ciphertext + auth tag]\r\n const hkdfSalt = packed.slice(1, 33);\r\n const iv = packed.slice(33, 45);\r\n const ciphertext = packed.slice(45);\r\n\r\n // Derive key using the same salt\r\n const { key: deviceKey } = await deriveDeviceKey(deviceUnlockKey, hkdfSalt);\r\n\r\n // Decrypt with AES-GCM\r\n const plaintextBytes = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n deviceKey,\r\n ciphertext as BufferSource\r\n );\r\n\r\n // Return raw bytes if requested\r\n if (returnRawBytes) {\r\n return new Uint8Array(plaintextBytes) as T;\r\n }\r\n\r\n // Otherwise decode and parse JSON\r\n const plaintext = new TextDecoder().decode(plaintextBytes);\r\n\r\n try {\r\n return JSON.parse(plaintext) as T;\r\n } catch {\r\n return plaintext as unknown as T;\r\n }\r\n}\r\n\r\n/**\r\n * Check if user has a device-bound credential registered\r\n */\r\nexport function hasDeviceBoundCredential(userId: string): boolean {\r\n return localStorage.getItem(`${DEVICE_BOUND_STORAGE_PREFIX}${userId}`) !== null;\r\n}\r\n\r\n/**\r\n * Remove device-bound credential\r\n */\r\nexport function removeDeviceBoundCredential(userId: string): void {\r\n localStorage.removeItem(`${DEVICE_BOUND_STORAGE_PREFIX}${userId}`);\r\n console.log('[DeviceBound] Credential removed');\r\n}\r\n\r\n/**\r\n * Detect if running in Remote Desktop session\r\n * RDP sessions can't access Windows Hello biometrics\r\n */\r\nfunction isRemoteDesktopSession(): boolean {\r\n // Check for Remote Desktop indicators\r\n // Note: Not 100% reliable, but catches most cases\r\n\r\n // Check for RDP-specific screen properties\r\n if (screen.width === 1024 && screen.height === 768) {\r\n // Common RDP default resolution (not definitive)\r\n console.log('[DeviceBound] Possible RDP session detected (screen resolution)');\r\n }\r\n\r\n // Check for Virtual Channel indicators in user agent\r\n const ua = navigator.userAgent.toLowerCase();\r\n if (ua.includes('rdp') || ua.includes('remote desktop')) {\r\n console.log('[DeviceBound] RDP detected in user agent');\r\n return true;\r\n }\r\n\r\n // Check for console/headless indicators\r\n if (!navigator.webdriver && screen.colorDepth === 24 && screen.width < 1280) {\r\n console.log('[DeviceBound] Possible remote session detected (screen properties)');\r\n }\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * Check if platform supports WebAuthn (basic check)\r\n * Also detects and warns about Remote Desktop sessions\r\n */\r\nexport async function detectDeviceBoundSupport(): Promise<boolean> {\r\n try {\r\n if (!window.PublicKeyCredential) {\r\n console.log('[DeviceBound] PublicKeyCredential not available');\r\n return false;\r\n }\r\n\r\n const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();\r\n if (!available) {\r\n console.log('[DeviceBound] Platform authenticator not available');\r\n return false;\r\n }\r\n\r\n console.log('[DeviceBound] Platform authenticator available');\r\n\r\n // Warn if in RDP session\r\n if (isRemoteDesktopSession()) {\r\n console.warn('[DeviceBound] ⚠️ Remote Desktop session detected - Windows Hello biometrics will not work over RDP');\r\n }\r\n\r\n return true;\r\n } catch (error) {\r\n console.error('[DeviceBound] Error detecting support:', error);\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Check if running in a Remote Desktop session\r\n * Exported so UI can show warnings\r\n */\r\nexport function isInRemoteDesktopSession(): boolean {\r\n return isRemoteDesktopSession();\r\n}\r\n","/**\r\n * Configuration and Constants\r\n *\r\n * @module estatehelm/config\r\n */\r\n\r\nimport envPaths from 'env-paths'\r\nimport * as path from 'path'\r\nimport * as fs from 'fs'\r\n\r\n// Platform-specific paths\r\nconst paths = envPaths('estatehelm', { suffix: '' })\r\n\r\n/**\r\n * Application data directory\r\n * - macOS: ~/Library/Application Support/estatehelm/\r\n * - Windows: C:\\Users\\<name>\\AppData\\Roaming\\estatehelm\\\r\n * - Linux: ~/.local/share/estatehelm/\r\n */\r\nexport const DATA_DIR = paths.data\r\n\r\n/**\r\n * SQLite cache database path\r\n */\r\nexport const CACHE_DB_PATH = path.join(DATA_DIR, 'cache.db')\r\n\r\n/**\r\n * Configuration file path\r\n */\r\nexport const CONFIG_PATH = path.join(DATA_DIR, 'config.json')\r\n\r\n/**\r\n * Device ID file path\r\n */\r\nexport const DEVICE_ID_PATH = path.join(DATA_DIR, '.device-id')\r\n\r\n/**\r\n * Keytar service name for credential storage\r\n */\r\nexport const KEYTAR_SERVICE = 'estatehelm'\r\n\r\n/**\r\n * Keytar account names\r\n */\r\nexport const KEYTAR_ACCOUNTS = {\r\n BEARER_TOKEN: 'bearer-token',\r\n REFRESH_TOKEN: 'refresh-token',\r\n DEVICE_CREDENTIALS: 'device-credentials',\r\n} as const\r\n\r\n/**\r\n * API base URL\r\n */\r\nexport const API_BASE_URL = process.env.ESTATEHELM_API_URL || 'https://api.estatehelm.com'\r\n\r\n/**\r\n * OAuth configuration\r\n */\r\nexport const OAUTH_CONFIG = {\r\n // OAuth authorization URL (opens in browser)\r\n authUrl: `${API_BASE_URL}/.ory/self-service/login/browser`,\r\n // OAuth token exchange endpoint\r\n tokenUrl: `${API_BASE_URL}/api/v2/auth/device-token`,\r\n // Device code flow endpoint\r\n deviceCodeUrl: `${API_BASE_URL}/api/v2/auth/device-code`,\r\n // Client ID for CLI app\r\n clientId: 'estatehelm-cli',\r\n}\r\n\r\n/**\r\n * Privacy mode\r\n */\r\nexport type PrivacyMode = 'full' | 'safe'\r\n\r\n/**\r\n * User configuration\r\n */\r\nexport interface UserConfig {\r\n /** Default privacy mode */\r\n defaultMode: PrivacyMode\r\n /** Last used household ID */\r\n lastHouseholdId?: string\r\n}\r\n\r\n/**\r\n * Default user configuration\r\n */\r\nconst DEFAULT_CONFIG: UserConfig = {\r\n defaultMode: 'full',\r\n}\r\n\r\n/**\r\n * Ensure data directory exists\r\n */\r\nexport function ensureDataDir(): void {\r\n if (!fs.existsSync(DATA_DIR)) {\r\n fs.mkdirSync(DATA_DIR, { recursive: true })\r\n }\r\n}\r\n\r\n/**\r\n * Load user configuration\r\n */\r\nexport function loadConfig(): UserConfig {\r\n ensureDataDir()\r\n try {\r\n if (fs.existsSync(CONFIG_PATH)) {\r\n const data = fs.readFileSync(CONFIG_PATH, 'utf-8')\r\n return { ...DEFAULT_CONFIG, ...JSON.parse(data) }\r\n }\r\n } catch (err) {\r\n console.warn('[Config] Failed to load config:', err)\r\n }\r\n return DEFAULT_CONFIG\r\n}\r\n\r\n/**\r\n * Save user configuration\r\n */\r\nexport function saveConfig(config: UserConfig): void {\r\n ensureDataDir()\r\n fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2))\r\n}\r\n\r\n/**\r\n * Get or generate device ID\r\n */\r\nexport function getDeviceId(): string {\r\n ensureDataDir()\r\n try {\r\n if (fs.existsSync(DEVICE_ID_PATH)) {\r\n return fs.readFileSync(DEVICE_ID_PATH, 'utf-8').trim()\r\n }\r\n } catch {\r\n // Generate new ID\r\n }\r\n\r\n // Generate a random device ID\r\n const id = `mcp-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`\r\n fs.writeFileSync(DEVICE_ID_PATH, id)\r\n return id\r\n}\r\n\r\n/**\r\n * Get device platform string\r\n */\r\nexport function getDevicePlatform(): string {\r\n const platform = process.platform\r\n switch (platform) {\r\n case 'darwin':\r\n return 'macOS'\r\n case 'win32':\r\n return 'Windows'\r\n case 'linux':\r\n return 'Linux'\r\n default:\r\n return platform\r\n }\r\n}\r\n\r\n/**\r\n * Get device user agent string\r\n */\r\nexport function getDeviceUserAgent(): string {\r\n return `estatehelm-mcp/1.0 (${getDevicePlatform()})`\r\n}\r\n\r\n/**\r\n * Sanitize token for logging (show first/last 4 chars)\r\n */\r\nexport function sanitizeToken(token: string): string {\r\n if (token.length <= 8) return '***'\r\n return `${token.slice(0, 4)}...${token.slice(-4)}`\r\n}\r\n\r\n/**\r\n * Log a request (safe for production)\r\n */\r\nexport function logRequest(endpoint: string, status: number): void {\r\n console.log(`[${new Date().toISOString()}] ${endpoint} → ${status}`)\r\n}\r\n","/**\r\n * OS Keychain Wrapper\r\n *\r\n * Uses keytar for secure credential storage in the OS keychain.\r\n * - macOS: Keychain Access\r\n * - Windows: Credential Manager\r\n * - Linux: Secret Service (GNOME Keyring / KWallet)\r\n *\r\n * @module estatehelm/keyStore\r\n */\r\n\r\nimport keytar from 'keytar'\r\nimport { KEYTAR_SERVICE, KEYTAR_ACCOUNTS, sanitizeToken } from './config.js'\r\n\r\n/**\r\n * Stored credentials\r\n */\r\nexport interface StoredCredentials {\r\n /** Bearer token for API calls */\r\n bearerToken: string\r\n /** Refresh token for token renewal */\r\n refreshToken: string\r\n /** Device credential data (JSON) */\r\n deviceCredentials: {\r\n credentialId: string\r\n encryptedPayload: string\r\n privateKeyBytes: string // Base64 encoded\r\n }\r\n}\r\n\r\n/**\r\n * Save bearer token to keychain\r\n */\r\nexport async function saveBearerToken(token: string): Promise<void> {\r\n await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.BEARER_TOKEN, token)\r\n console.log(`[KeyStore] Saved bearer token: ${sanitizeToken(token)}`)\r\n}\r\n\r\n/**\r\n * Get bearer token from keychain\r\n */\r\nexport async function getBearerToken(): Promise<string | null> {\r\n return keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.BEARER_TOKEN)\r\n}\r\n\r\n/**\r\n * Save refresh token to keychain\r\n */\r\nexport async function saveRefreshToken(token: string): Promise<void> {\r\n await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.REFRESH_TOKEN, token)\r\n console.log(`[KeyStore] Saved refresh token: ${sanitizeToken(token)}`)\r\n}\r\n\r\n/**\r\n * Get refresh token from keychain\r\n */\r\nexport async function getRefreshToken(): Promise<string | null> {\r\n return keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.REFRESH_TOKEN)\r\n}\r\n\r\n/**\r\n * Save device credentials to keychain\r\n */\r\nexport async function saveDeviceCredentials(credentials: StoredCredentials['deviceCredentials']): Promise<void> {\r\n const json = JSON.stringify(credentials)\r\n await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.DEVICE_CREDENTIALS, json)\r\n console.log(`[KeyStore] Saved device credentials`)\r\n}\r\n\r\n/**\r\n * Get device credentials from keychain\r\n */\r\nexport async function getDeviceCredentials(): Promise<StoredCredentials['deviceCredentials'] | null> {\r\n const json = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.DEVICE_CREDENTIALS)\r\n if (!json) return null\r\n try {\r\n return JSON.parse(json)\r\n } catch {\r\n console.warn('[KeyStore] Failed to parse device credentials')\r\n return null\r\n }\r\n}\r\n\r\n/**\r\n * Check if credentials are stored\r\n */\r\nexport async function hasCredentials(): Promise<boolean> {\r\n const [bearer, device] = await Promise.all([\r\n getBearerToken(),\r\n getDeviceCredentials(),\r\n ])\r\n return !!(bearer && device)\r\n}\r\n\r\n/**\r\n * Get all stored credentials\r\n */\r\nexport async function getCredentials(): Promise<StoredCredentials | null> {\r\n const [bearerToken, refreshToken, deviceCredentials] = await Promise.all([\r\n getBearerToken(),\r\n getRefreshToken(),\r\n getDeviceCredentials(),\r\n ])\r\n\r\n if (!bearerToken || !deviceCredentials) {\r\n return null\r\n }\r\n\r\n return {\r\n bearerToken,\r\n refreshToken: refreshToken || '',\r\n deviceCredentials,\r\n }\r\n}\r\n\r\n/**\r\n * Clear all credentials from keychain\r\n */\r\nexport async function clearCredentials(): Promise<void> {\r\n await Promise.all([\r\n keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.BEARER_TOKEN),\r\n keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.REFRESH_TOKEN),\r\n keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.DEVICE_CREDENTIALS),\r\n ])\r\n console.log('[KeyStore] Cleared all credentials')\r\n}\r\n","/**\r\n * MCP Server\r\n *\r\n * Implements the Model Context Protocol server using @modelcontextprotocol/sdk.\r\n * Exposes EstateHelm data as resources and tools for AI assistants.\r\n *\r\n * @module estatehelm/server\r\n */\r\n\r\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js'\r\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\r\nimport {\r\n CallToolRequestSchema,\r\n ListResourcesRequestSchema,\r\n ListToolsRequestSchema,\r\n ReadResourceRequestSchema,\r\n ListPromptsRequestSchema,\r\n GetPromptRequestSchema,\r\n} from '@modelcontextprotocol/sdk/types.js'\r\nimport type { PrivacyMode } from './config.js'\r\nimport { loadConfig } from './config.js'\r\nimport { getAuthenticatedClient, getPrivateKey } from './login.js'\r\nimport { redactEntity } from './filter.js'\r\nimport { initCache, syncIfNeeded, getDecryptedEntities, getHouseholds } from './cache.js'\r\n\r\n/**\r\n * Start the MCP server\r\n */\r\nexport async function startServer(mode?: PrivacyMode): Promise<void> {\r\n // Load config\r\n const config = loadConfig()\r\n const privacyMode = mode || config.defaultMode\r\n\r\n console.error(`[MCP] Starting server in ${privacyMode} mode`)\r\n\r\n // Verify login\r\n const client = await getAuthenticatedClient()\r\n if (!client) {\r\n console.error('[MCP] Not logged in. Run: estatehelm login')\r\n process.exit(1)\r\n }\r\n\r\n const privateKey = await getPrivateKey()\r\n if (!privateKey) {\r\n console.error('[MCP] Failed to load encryption keys. Run: estatehelm login')\r\n process.exit(1)\r\n }\r\n\r\n // Initialize cache\r\n await initCache()\r\n\r\n // Sync on startup\r\n console.error('[MCP] Checking for updates...')\r\n const synced = await syncIfNeeded(client, privateKey)\r\n if (synced) {\r\n console.error('[MCP] Cache updated')\r\n } else {\r\n console.error('[MCP] Cache is up to date')\r\n }\r\n\r\n // Create MCP server\r\n const server = new Server(\r\n {\r\n name: 'estatehelm',\r\n version: '1.0.0',\r\n },\r\n {\r\n capabilities: {\r\n resources: {},\r\n tools: {},\r\n prompts: {},\r\n },\r\n }\r\n )\r\n\r\n // List available resources\r\n server.setRequestHandler(ListResourcesRequestSchema, async () => {\r\n const households = await getHouseholds()\r\n\r\n const resources = [\r\n {\r\n uri: 'estatehelm://households',\r\n name: 'All Households',\r\n description: 'List of all households you have access to',\r\n mimeType: 'application/json',\r\n },\r\n ]\r\n\r\n // Add household-specific resources\r\n for (const household of households) {\r\n resources.push({\r\n uri: `estatehelm://households/${household.id}`,\r\n name: household.name,\r\n description: `Household: ${household.name}`,\r\n mimeType: 'application/json',\r\n })\r\n\r\n // Add entity type resources for each household\r\n const entityTypes = [\r\n 'pet', 'property', 'vehicle', 'contact', 'insurance', 'bank_account',\r\n 'investment', 'subscription', 'maintenance_task', 'password', 'access_code',\r\n 'document', 'medical', 'prescription', 'credential', 'utility',\r\n ]\r\n\r\n for (const type of entityTypes) {\r\n resources.push({\r\n uri: `estatehelm://households/${household.id}/${type}`,\r\n name: `${household.name} - ${formatEntityType(type)}`,\r\n description: `${formatEntityType(type)} in ${household.name}`,\r\n mimeType: 'application/json',\r\n })\r\n }\r\n }\r\n\r\n return { resources }\r\n })\r\n\r\n // Read resource content\r\n server.setRequestHandler(ReadResourceRequestSchema, async (request) => {\r\n const uri = request.params.uri\r\n const parsed = parseResourceUri(uri)\r\n\r\n if (!parsed) {\r\n throw new Error(`Invalid resource URI: ${uri}`)\r\n }\r\n\r\n let content: any\r\n\r\n if (parsed.type === 'households' && !parsed.householdId) {\r\n // List all households\r\n content = await getHouseholds()\r\n } else if (parsed.type === 'households' && parsed.householdId && !parsed.entityType) {\r\n // Get specific household\r\n const households = await getHouseholds()\r\n content = households.find((h) => h.id === parsed.householdId)\r\n if (!content) {\r\n throw new Error(`Household not found: ${parsed.householdId}`)\r\n }\r\n } else if (parsed.householdId && parsed.entityType) {\r\n // Get entities of a type for a household\r\n const entities = await getDecryptedEntities(parsed.householdId, parsed.entityType, privateKey)\r\n content = entities.map((e) => redactEntity(e, parsed.entityType!, privacyMode))\r\n } else {\r\n throw new Error(`Unsupported resource: ${uri}`)\r\n }\r\n\r\n return {\r\n contents: [\r\n {\r\n uri,\r\n mimeType: 'application/json',\r\n text: JSON.stringify(content, null, 2),\r\n },\r\n ],\r\n }\r\n })\r\n\r\n // List available tools\r\n server.setRequestHandler(ListToolsRequestSchema, async () => {\r\n return {\r\n tools: [\r\n {\r\n name: 'search_entities',\r\n description: 'Search across all entities in EstateHelm',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n query: {\r\n type: 'string',\r\n description: 'Search query',\r\n },\r\n householdId: {\r\n type: 'string',\r\n description: 'Optional: Limit search to a specific household',\r\n },\r\n entityType: {\r\n type: 'string',\r\n description: 'Optional: Limit search to a specific entity type',\r\n },\r\n },\r\n required: ['query'],\r\n },\r\n },\r\n {\r\n name: 'get_household_summary',\r\n description: 'Get a summary of a household including counts and key dates',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n householdId: {\r\n type: 'string',\r\n description: 'The household ID',\r\n },\r\n },\r\n required: ['householdId'],\r\n },\r\n },\r\n {\r\n name: 'get_expiring_items',\r\n description: 'Get items expiring within a given number of days',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n days: {\r\n type: 'number',\r\n description: 'Number of days to look ahead (default: 30)',\r\n },\r\n householdId: {\r\n type: 'string',\r\n description: 'Optional: Limit to a specific household',\r\n },\r\n },\r\n },\r\n },\r\n {\r\n name: 'refresh',\r\n description: 'Force refresh of cached data from the server',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {},\r\n },\r\n },\r\n ],\r\n }\r\n })\r\n\r\n // Handle tool calls\r\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\r\n const { name, arguments: args } = request.params\r\n\r\n switch (name) {\r\n case 'search_entities': {\r\n const { query, householdId, entityType } = args as {\r\n query: string\r\n householdId?: string\r\n entityType?: string\r\n }\r\n\r\n // Get all households to search\r\n const households = await getHouseholds()\r\n const searchHouseholds = householdId\r\n ? households.filter((h) => h.id === householdId)\r\n : households\r\n\r\n const results: any[] = []\r\n\r\n for (const household of searchHouseholds) {\r\n const entityTypes = entityType\r\n ? [entityType]\r\n : ['pet', 'property', 'vehicle', 'contact', 'insurance', 'bank_account',\r\n 'investment', 'subscription', 'maintenance_task', 'password', 'access_code']\r\n\r\n for (const type of entityTypes) {\r\n const entities = await getDecryptedEntities(household.id, type, privateKey)\r\n const matches = entities.filter((e) => searchEntity(e, query))\r\n\r\n for (const match of matches) {\r\n results.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n entityType: type,\r\n entity: redactEntity(match, type, privacyMode),\r\n })\r\n }\r\n }\r\n }\r\n\r\n return {\r\n content: [\r\n {\r\n type: 'text',\r\n text: JSON.stringify(results, null, 2),\r\n },\r\n ],\r\n }\r\n }\r\n\r\n case 'get_household_summary': {\r\n const { householdId } = args as { householdId: string }\r\n\r\n const households = await getHouseholds()\r\n const household = households.find((h) => h.id === householdId)\r\n if (!household) {\r\n throw new Error(`Household not found: ${householdId}`)\r\n }\r\n\r\n const entityTypes = ['pet', 'property', 'vehicle', 'contact', 'insurance', 'bank_account',\r\n 'investment', 'subscription', 'maintenance_task', 'password', 'access_code']\r\n\r\n const counts: Record<string, number> = {}\r\n for (const type of entityTypes) {\r\n const entities = await getDecryptedEntities(householdId, type, privateKey)\r\n counts[type] = entities.length\r\n }\r\n\r\n const summary = {\r\n household: {\r\n id: household.id,\r\n name: household.name,\r\n },\r\n counts,\r\n totalEntities: Object.values(counts).reduce((a, b) => a + b, 0),\r\n }\r\n\r\n return {\r\n content: [\r\n {\r\n type: 'text',\r\n text: JSON.stringify(summary, null, 2),\r\n },\r\n ],\r\n }\r\n }\r\n\r\n case 'get_expiring_items': {\r\n const { days = 30, householdId } = args as { days?: number; householdId?: string }\r\n\r\n const households = await getHouseholds()\r\n const searchHouseholds = householdId\r\n ? households.filter((h) => h.id === householdId)\r\n : households\r\n\r\n const now = new Date()\r\n const cutoff = new Date(now.getTime() + days * 24 * 60 * 60 * 1000)\r\n\r\n const expiring: any[] = []\r\n\r\n for (const household of searchHouseholds) {\r\n // Check insurance expirations\r\n const insurance = await getDecryptedEntities(household.id, 'insurance', privateKey)\r\n for (const policy of insurance) {\r\n if (policy.expirationDate) {\r\n const expires = new Date(policy.expirationDate)\r\n if (expires <= cutoff) {\r\n expiring.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n type: 'insurance',\r\n name: policy.name || policy.policyNumber,\r\n expiresAt: policy.expirationDate,\r\n daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)),\r\n })\r\n }\r\n }\r\n }\r\n\r\n // Check vehicle registrations\r\n const vehicles = await getDecryptedEntities(household.id, 'vehicle', privateKey)\r\n for (const vehicle of vehicles) {\r\n if (vehicle.registrationExpiration) {\r\n const expires = new Date(vehicle.registrationExpiration)\r\n if (expires <= cutoff) {\r\n expiring.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n type: 'vehicle_registration',\r\n name: `${vehicle.year || ''} ${vehicle.make || ''} ${vehicle.model || ''}`.trim(),\r\n expiresAt: vehicle.registrationExpiration,\r\n daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)),\r\n })\r\n }\r\n }\r\n }\r\n\r\n // Check subscriptions\r\n const subscriptions = await getDecryptedEntities(household.id, 'subscription', privateKey)\r\n for (const sub of subscriptions) {\r\n if (sub.renewalDate) {\r\n const renews = new Date(sub.renewalDate)\r\n if (renews <= cutoff) {\r\n expiring.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n type: 'subscription',\r\n name: sub.name || sub.serviceName,\r\n expiresAt: sub.renewalDate,\r\n daysUntil: Math.ceil((renews.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)),\r\n })\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Sort by days until expiration\r\n expiring.sort((a, b) => a.daysUntil - b.daysUntil)\r\n\r\n return {\r\n content: [\r\n {\r\n type: 'text',\r\n text: JSON.stringify(expiring, null, 2),\r\n },\r\n ],\r\n }\r\n }\r\n\r\n case 'refresh': {\r\n const synced = await syncIfNeeded(client!, privateKey!, true)\r\n return {\r\n content: [\r\n {\r\n type: 'text',\r\n text: synced ? 'Cache refreshed with latest data' : 'Cache was already up to date',\r\n },\r\n ],\r\n }\r\n }\r\n\r\n default:\r\n throw new Error(`Unknown tool: ${name}`)\r\n }\r\n })\r\n\r\n // List available prompts\r\n server.setRequestHandler(ListPromptsRequestSchema, async () => {\r\n return {\r\n prompts: [\r\n {\r\n name: 'household_summary',\r\n description: 'Get an overview of a household',\r\n arguments: [\r\n {\r\n name: 'householdId',\r\n description: 'Optional household ID (uses first household if not specified)',\r\n required: false,\r\n },\r\n ],\r\n },\r\n {\r\n name: 'expiring_soon',\r\n description: 'Show items expiring soon',\r\n arguments: [\r\n {\r\n name: 'days',\r\n description: 'Number of days to look ahead (default: 30)',\r\n required: false,\r\n },\r\n ],\r\n },\r\n {\r\n name: 'emergency_contacts',\r\n description: 'Show emergency contacts',\r\n arguments: [],\r\n },\r\n ],\r\n }\r\n })\r\n\r\n // Handle prompt requests\r\n server.setRequestHandler(GetPromptRequestSchema, async (request) => {\r\n const { name, arguments: args } = request.params\r\n\r\n switch (name) {\r\n case 'household_summary': {\r\n const householdId = args?.householdId\r\n const households = await getHouseholds()\r\n const household = householdId\r\n ? households.find((h) => h.id === householdId)\r\n : households[0]\r\n\r\n if (!household) {\r\n throw new Error('No household found')\r\n }\r\n\r\n return {\r\n messages: [\r\n {\r\n role: 'user',\r\n content: {\r\n type: 'text',\r\n text: `Please give me an overview of my household \"${household.name}\". Include counts of different items, any upcoming expirations, and notable information.`,\r\n },\r\n },\r\n ],\r\n }\r\n }\r\n\r\n case 'expiring_soon': {\r\n const days = args?.days || 30\r\n return {\r\n messages: [\r\n {\r\n role: 'user',\r\n content: {\r\n type: 'text',\r\n text: `What items are expiring in the next ${days} days? Include insurance policies, vehicle registrations, subscriptions, and any other items with expiration dates.`,\r\n },\r\n },\r\n ],\r\n }\r\n }\r\n\r\n case 'emergency_contacts': {\r\n return {\r\n messages: [\r\n {\r\n role: 'user',\r\n content: {\r\n type: 'text',\r\n text: 'Show me all emergency contacts across my households. Include their names, phone numbers, and relationship to the household.',\r\n },\r\n },\r\n ],\r\n }\r\n }\r\n\r\n default:\r\n throw new Error(`Unknown prompt: ${name}`)\r\n }\r\n })\r\n\r\n // Start server with stdio transport\r\n const transport = new StdioServerTransport()\r\n await server.connect(transport)\r\n console.error('[MCP] Server started')\r\n}\r\n\r\n/**\r\n * Parse a resource URI\r\n */\r\nfunction parseResourceUri(uri: string): {\r\n type: string\r\n householdId?: string\r\n entityType?: string\r\n entityId?: string\r\n} | null {\r\n const match = uri.match(/^estatehelm:\\/\\/([^/]+)(?:\\/([^/]+))?(?:\\/([^/]+))?(?:\\/([^/]+))?$/)\r\n if (!match) return null\r\n\r\n const [, type, householdId, entityType, entityId] = match\r\n return { type, householdId, entityType, entityId }\r\n}\r\n\r\n/**\r\n * Format entity type for display\r\n */\r\nfunction formatEntityType(type: string): string {\r\n return type\r\n .split('_')\r\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\r\n .join(' ')\r\n}\r\n\r\n/**\r\n * Search an entity for a query string\r\n */\r\nfunction searchEntity(entity: Record<string, any>, query: string): boolean {\r\n const lowerQuery = query.toLowerCase()\r\n\r\n const searchFields = ['name', 'title', 'description', 'notes', 'make', 'model',\r\n 'policyNumber', 'serviceName', 'username', 'email']\r\n\r\n for (const field of searchFields) {\r\n if (entity[field] && String(entity[field]).toLowerCase().includes(lowerQuery)) {\r\n return true\r\n }\r\n }\r\n\r\n return false\r\n}\r\n","/**\r\n * Safe Mode Field Redaction\r\n *\r\n * Implements field-level redaction for privacy when sharing screen\r\n * or when uncertain about who might see the data.\r\n *\r\n * @module estatehelm/filter\r\n */\r\n\r\nimport type { PrivacyMode } from './config.js'\r\n\r\n/**\r\n * Fields to redact by entity type in safe mode\r\n */\r\nconst REDACTION_RULES: Record<string, string[]> = {\r\n // Password entries\r\n password: ['password', 'notes'],\r\n\r\n // Identity documents\r\n identity: ['password', 'recoveryKey', 'securityAnswers'],\r\n\r\n // Financial accounts\r\n bank_account: ['accountNumber', 'routingNumber'],\r\n investment: ['accountNumber'],\r\n\r\n // Access codes\r\n access_code: ['code', 'pin'],\r\n\r\n // Credentials (show last 4 of document number)\r\n credential: ['documentNumber'],\r\n}\r\n\r\n/**\r\n * Fields that should show partial value (last 4 characters)\r\n */\r\nconst PARTIAL_REDACTION_FIELDS = ['documentNumber', 'accountNumber']\r\n\r\n/**\r\n * Redacted value placeholder\r\n */\r\nconst REDACTED = '[REDACTED]'\r\n\r\n/**\r\n * Redact sensitive fields from an entity\r\n *\r\n * @param entity - The entity to redact\r\n * @param entityType - The type of entity\r\n * @param mode - Privacy mode ('full' or 'safe')\r\n * @returns The entity with sensitive fields redacted\r\n */\r\nexport function redactEntity<T extends Record<string, any>>(\r\n entity: T,\r\n entityType: string,\r\n mode: PrivacyMode\r\n): T {\r\n // Full mode returns entity as-is\r\n if (mode === 'full') {\r\n return entity\r\n }\r\n\r\n // Get fields to redact for this entity type\r\n const fieldsToRedact = REDACTION_RULES[entityType] || []\r\n if (fieldsToRedact.length === 0) {\r\n return entity\r\n }\r\n\r\n // Create a copy to avoid mutating original\r\n const redacted: Record<string, any> = { ...entity }\r\n\r\n for (const field of fieldsToRedact) {\r\n if (field in redacted && redacted[field] != null) {\r\n const value = redacted[field]\r\n\r\n // Partial redaction for certain fields (show last 4)\r\n if (PARTIAL_REDACTION_FIELDS.includes(field) && typeof value === 'string' && value.length > 4) {\r\n redacted[field] = `****${value.slice(-4)}`\r\n } else {\r\n redacted[field] = REDACTED\r\n }\r\n }\r\n }\r\n\r\n return redacted as T\r\n}\r\n\r\n/**\r\n * Redact an array of entities\r\n *\r\n * @param entities - The entities to redact\r\n * @param entityType - The type of entities\r\n * @param mode - Privacy mode\r\n * @returns The entities with sensitive fields redacted\r\n */\r\nexport function redactEntities<T extends Record<string, any>>(\r\n entities: T[],\r\n entityType: string,\r\n mode: PrivacyMode\r\n): T[] {\r\n return entities.map((entity) => redactEntity(entity, entityType, mode))\r\n}\r\n\r\n/**\r\n * Check if an entity type has any redaction rules\r\n */\r\nexport function hasRedactionRules(entityType: string): boolean {\r\n return entityType in REDACTION_RULES\r\n}\r\n\r\n/**\r\n * Get the list of sensitive entity types\r\n */\r\nexport function getSensitiveEntityTypes(): string[] {\r\n return Object.keys(REDACTION_RULES)\r\n}\r\n\r\n/**\r\n * Describe what fields are redacted for an entity type\r\n */\r\nexport function describeRedactions(entityType: string): string[] {\r\n return REDACTION_RULES[entityType] || []\r\n}\r\n","/**\r\n * SQLite Cache Store Implementation\r\n *\r\n * Implements CacheStore interface using better-sqlite3 for Node.js environments.\r\n * This is used by the MCP server for local caching.\r\n *\r\n * @module @hearthcoo/cache-sqlite\r\n */\r\n\r\nimport Database from 'better-sqlite3'\r\nimport type {\r\n CacheStore,\r\n CacheMetadata,\r\n CachedEntity,\r\n EntityCacheEntry,\r\n CachedAttachment,\r\n EncryptedKeyCache,\r\n OfflineCredential,\r\n CachedHouseholdMembers,\r\n CacheStats,\r\n} from '@hearthcoo/cache'\r\nimport { DB_VERSION, makeCacheKey, makeMembersCacheKey } from '@hearthcoo/cache'\r\nimport * as fs from 'fs'\r\nimport * as path from 'path'\r\n\r\n/**\r\n * SQLite implementation of CacheStore\r\n */\r\nexport class SqliteCacheStore implements CacheStore {\r\n private db: Database.Database\r\n\r\n constructor(dbPath: string) {\r\n // Ensure directory exists\r\n const dir = path.dirname(dbPath)\r\n if (!fs.existsSync(dir)) {\r\n fs.mkdirSync(dir, { recursive: true })\r\n }\r\n\r\n this.db = new Database(dbPath)\r\n this.db.pragma('journal_mode = WAL')\r\n this.initializeSchema()\r\n }\r\n\r\n private initializeSchema(): void {\r\n // Check current schema version\r\n const versionRow = this.db.prepare(\r\n \"SELECT value FROM metadata WHERE key = 'schema_version'\"\r\n ).get() as { value: string } | undefined\r\n\r\n const currentVersion = versionRow ? parseInt(versionRow.value, 10) : 0\r\n\r\n if (currentVersion < DB_VERSION) {\r\n this.migrate(currentVersion)\r\n }\r\n }\r\n\r\n private migrate(fromVersion: number): void {\r\n console.log(`[SqliteCache] Migrating from version ${fromVersion} to ${DB_VERSION}`)\r\n\r\n // Create tables if they don't exist\r\n this.db.exec(`\r\n -- Metadata table (key-value store)\r\n CREATE TABLE IF NOT EXISTS metadata (\r\n key TEXT PRIMARY KEY,\r\n value TEXT NOT NULL\r\n );\r\n\r\n -- Cache metadata (user/household info)\r\n CREATE TABLE IF NOT EXISTS cache_metadata (\r\n id TEXT PRIMARY KEY DEFAULT 'metadata',\r\n household_id TEXT,\r\n user_id TEXT,\r\n user_identity TEXT, -- JSON\r\n last_full_sync TEXT,\r\n last_changelog_id INTEGER DEFAULT 0,\r\n offline_enabled INTEGER DEFAULT 0,\r\n created_at TEXT\r\n );\r\n\r\n -- Credentials for offline unlock\r\n CREATE TABLE IF NOT EXISTS credentials (\r\n user_id TEXT PRIMARY KEY,\r\n credential_id TEXT NOT NULL,\r\n prf_input TEXT NOT NULL,\r\n cached_at TEXT NOT NULL\r\n );\r\n\r\n -- Encrypted key cache\r\n CREATE TABLE IF NOT EXISTS keys (\r\n user_id TEXT PRIMARY KEY,\r\n household_id TEXT NOT NULL,\r\n iv TEXT NOT NULL,\r\n encrypted_data TEXT NOT NULL,\r\n cached_at TEXT NOT NULL\r\n );\r\n\r\n -- Entity cache\r\n CREATE TABLE IF NOT EXISTS entities (\r\n cache_key TEXT PRIMARY KEY,\r\n household_id TEXT NOT NULL,\r\n entity_type TEXT NOT NULL,\r\n items TEXT NOT NULL, -- JSON array\r\n last_sync TEXT NOT NULL,\r\n expected_count INTEGER,\r\n changelog_id INTEGER\r\n );\r\n\r\n -- Attachment cache\r\n CREATE TABLE IF NOT EXISTS attachments (\r\n file_id TEXT PRIMARY KEY,\r\n encrypted_data BLOB NOT NULL,\r\n entity_id TEXT NOT NULL,\r\n entity_type TEXT NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n key_type TEXT NOT NULL,\r\n version INTEGER NOT NULL,\r\n cached_at TEXT NOT NULL,\r\n crypto_version INTEGER,\r\n key_derivation_id TEXT\r\n );\r\n\r\n CREATE INDEX IF NOT EXISTS idx_attachments_entity_id ON attachments(entity_id);\r\n `)\r\n\r\n // Update schema version\r\n this.db.prepare(\r\n \"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)\"\r\n ).run(DB_VERSION.toString())\r\n\r\n console.log('[SqliteCache] Migration complete')\r\n }\r\n\r\n // ============================================================================\r\n // Metadata Operations\r\n // ============================================================================\r\n\r\n async getMetadata(): Promise<CacheMetadata | null> {\r\n const row = this.db.prepare(\r\n 'SELECT * FROM cache_metadata WHERE id = ?'\r\n ).get('metadata') as any\r\n\r\n if (!row) return null\r\n\r\n return {\r\n householdId: row.household_id,\r\n userId: row.user_id,\r\n userIdentity: row.user_identity ? JSON.parse(row.user_identity) : undefined,\r\n lastFullSync: row.last_full_sync,\r\n lastChangelogId: row.last_changelog_id,\r\n offlineEnabled: !!row.offline_enabled,\r\n createdAt: row.created_at,\r\n }\r\n }\r\n\r\n async saveMetadata(metadata: CacheMetadata): Promise<void> {\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO cache_metadata\r\n (id, household_id, user_id, user_identity, last_full_sync, last_changelog_id, offline_enabled, created_at)\r\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\r\n `).run(\r\n 'metadata',\r\n metadata.householdId,\r\n metadata.userId,\r\n metadata.userIdentity ? JSON.stringify(metadata.userIdentity) : null,\r\n metadata.lastFullSync,\r\n metadata.lastChangelogId,\r\n metadata.offlineEnabled ? 1 : 0,\r\n metadata.createdAt\r\n )\r\n }\r\n\r\n async getLastChangelogId(): Promise<number> {\r\n const metadata = await this.getMetadata()\r\n return metadata?.lastChangelogId ?? 0\r\n }\r\n\r\n async updateLastChangelogId(\r\n changelogId: number,\r\n householdId?: string,\r\n userId?: string\r\n ): Promise<void> {\r\n const existing = await this.getMetadata()\r\n\r\n if (existing) {\r\n this.db.prepare(\r\n 'UPDATE cache_metadata SET last_changelog_id = ? WHERE id = ?'\r\n ).run(changelogId, 'metadata')\r\n } else if (householdId && userId) {\r\n await this.saveMetadata({\r\n householdId,\r\n userId,\r\n lastFullSync: null,\r\n lastChangelogId: changelogId,\r\n offlineEnabled: false,\r\n createdAt: new Date().toISOString(),\r\n })\r\n }\r\n }\r\n\r\n // ============================================================================\r\n // Credential Operations\r\n // ============================================================================\r\n\r\n async hasOfflineCredential(userId: string): Promise<boolean> {\r\n const row = this.db.prepare(\r\n 'SELECT 1 FROM credentials WHERE user_id = ?'\r\n ).get(userId)\r\n return !!row\r\n }\r\n\r\n async getOfflineCredential(userId: string): Promise<OfflineCredential | null> {\r\n const row = this.db.prepare(\r\n 'SELECT * FROM credentials WHERE user_id = ?'\r\n ).get(userId) as any\r\n\r\n if (!row) return null\r\n\r\n return {\r\n userId: row.user_id,\r\n credentialId: row.credential_id,\r\n prfInput: row.prf_input,\r\n cachedAt: row.cached_at,\r\n }\r\n }\r\n\r\n async saveOfflineCredential(credential: OfflineCredential): Promise<void> {\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO credentials (user_id, credential_id, prf_input, cached_at)\r\n VALUES (?, ?, ?, ?)\r\n `).run(\r\n credential.userId,\r\n credential.credentialId,\r\n credential.prfInput,\r\n credential.cachedAt\r\n )\r\n console.log('[SqliteCache] Saved offline credential for user:', credential.userId)\r\n }\r\n\r\n async removeOfflineCredential(userId: string): Promise<void> {\r\n this.db.prepare('DELETE FROM credentials WHERE user_id = ?').run(userId)\r\n }\r\n\r\n // ============================================================================\r\n // Key Cache Operations\r\n // ============================================================================\r\n\r\n async getKeyCache(userId: string): Promise<EncryptedKeyCache | null> {\r\n const row = this.db.prepare(\r\n 'SELECT * FROM keys WHERE user_id = ?'\r\n ).get(userId) as any\r\n\r\n if (!row) return null\r\n\r\n return {\r\n userId: row.user_id,\r\n householdId: row.household_id,\r\n iv: row.iv,\r\n encryptedData: row.encrypted_data,\r\n cachedAt: row.cached_at,\r\n }\r\n }\r\n\r\n async saveKeyCache(keyCache: EncryptedKeyCache): Promise<void> {\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO keys (user_id, household_id, iv, encrypted_data, cached_at)\r\n VALUES (?, ?, ?, ?, ?)\r\n `).run(\r\n keyCache.userId,\r\n keyCache.householdId,\r\n keyCache.iv,\r\n keyCache.encryptedData,\r\n keyCache.cachedAt\r\n )\r\n console.log('[SqliteCache] Saved encrypted keys for user:', keyCache.userId)\r\n }\r\n\r\n async removeKeyCache(userId: string): Promise<void> {\r\n this.db.prepare('DELETE FROM keys WHERE user_id = ?').run(userId)\r\n }\r\n\r\n // ============================================================================\r\n // Entity Cache Operations\r\n // ============================================================================\r\n\r\n async getEntityCache(\r\n householdId: string,\r\n entityType: string\r\n ): Promise<EntityCacheEntry | null> {\r\n const cacheKey = makeCacheKey(householdId, entityType)\r\n\r\n const row = this.db.prepare(\r\n 'SELECT * FROM entities WHERE cache_key = ?'\r\n ).get(cacheKey) as any\r\n\r\n if (!row) return null\r\n\r\n const entry: EntityCacheEntry = {\r\n cacheKey: row.cache_key,\r\n householdId: row.household_id,\r\n entityType: row.entity_type,\r\n items: JSON.parse(row.items),\r\n lastSync: row.last_sync,\r\n expectedCount: row.expected_count,\r\n changelogId: row.changelog_id,\r\n }\r\n\r\n // Validate cache integrity\r\n if (entry.expectedCount === undefined || entry.items.length !== entry.expectedCount) {\r\n return null\r\n }\r\n\r\n if (entry.changelogId === undefined) {\r\n return null\r\n }\r\n\r\n return entry\r\n }\r\n\r\n async getAllEntityCaches(householdId?: string): Promise<EntityCacheEntry[]> {\r\n const query = householdId\r\n ? 'SELECT * FROM entities WHERE household_id = ?'\r\n : 'SELECT * FROM entities'\r\n\r\n const rows = householdId\r\n ? this.db.prepare(query).all(householdId) as any[]\r\n : this.db.prepare(query).all() as any[]\r\n\r\n return rows.map(row => ({\r\n cacheKey: row.cache_key,\r\n householdId: row.household_id,\r\n entityType: row.entity_type,\r\n items: JSON.parse(row.items),\r\n lastSync: row.last_sync,\r\n expectedCount: row.expected_count,\r\n changelogId: row.changelog_id,\r\n }))\r\n }\r\n\r\n async saveEntityCache(\r\n householdId: string,\r\n entityType: string,\r\n items: CachedEntity[],\r\n changelogId: number\r\n ): Promise<void> {\r\n const cacheKey = makeCacheKey(householdId, entityType)\r\n\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO entities\r\n (cache_key, household_id, entity_type, items, last_sync, expected_count, changelog_id)\r\n VALUES (?, ?, ?, ?, ?, ?, ?)\r\n `).run(\r\n cacheKey,\r\n householdId,\r\n entityType,\r\n JSON.stringify(items),\r\n new Date().toISOString(),\r\n items.length,\r\n changelogId\r\n )\r\n }\r\n\r\n async updateEntityInCache(\r\n householdId: string,\r\n entity: CachedEntity\r\n ): Promise<boolean> {\r\n const { entityType } = entity\r\n const cacheKey = makeCacheKey(householdId, entityType)\r\n\r\n const existing = await this.getEntityCache(householdId, entityType)\r\n\r\n const items = existing?.items || []\r\n const existingIndex = items.findIndex(e => e.id === entity.id)\r\n const isUpdate = existingIndex >= 0\r\n\r\n if (isUpdate) {\r\n items[existingIndex] = entity\r\n } else {\r\n items.push(entity)\r\n }\r\n\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO entities\r\n (cache_key, household_id, entity_type, items, last_sync, expected_count, changelog_id)\r\n VALUES (?, ?, ?, ?, ?, ?, ?)\r\n `).run(\r\n cacheKey,\r\n householdId,\r\n entityType,\r\n JSON.stringify(items),\r\n new Date().toISOString(),\r\n items.length,\r\n existing?.changelogId ?? null\r\n )\r\n\r\n return isUpdate\r\n }\r\n\r\n async removeEntityFromCache(\r\n householdId: string,\r\n entityType: string,\r\n entityId: string\r\n ): Promise<void> {\r\n const cacheKey = makeCacheKey(householdId, entityType)\r\n\r\n const existing = await this.getEntityCache(householdId, entityType)\r\n if (!existing) return\r\n\r\n const items = existing.items.filter(e => e.id !== entityId)\r\n\r\n this.db.prepare(`\r\n UPDATE entities SET items = ?, expected_count = ?, last_sync = ? WHERE cache_key = ?\r\n `).run(\r\n JSON.stringify(items),\r\n items.length,\r\n new Date().toISOString(),\r\n cacheKey\r\n )\r\n }\r\n\r\n async getEntityVersions(\r\n householdId: string,\r\n entityType: string\r\n ): Promise<Map<string, number>> {\r\n const cache = await this.getEntityCache(householdId, entityType)\r\n const versions = new Map<string, number>()\r\n\r\n if (cache) {\r\n for (const entity of cache.items) {\r\n versions.set(entity.id, entity.version)\r\n }\r\n }\r\n\r\n return versions\r\n }\r\n\r\n // ============================================================================\r\n // Household Members Cache\r\n // ============================================================================\r\n\r\n async getHouseholdMembersCache(\r\n householdId: string\r\n ): Promise<CachedHouseholdMembers | null> {\r\n const cacheKey = makeMembersCacheKey(householdId)\r\n\r\n const row = this.db.prepare(\r\n 'SELECT * FROM entities WHERE cache_key = ?'\r\n ).get(cacheKey) as any\r\n\r\n if (!row) return null\r\n\r\n const items = JSON.parse(row.items)\r\n if (!items.members) return null\r\n\r\n return {\r\n householdId: row.household_id,\r\n members: items.members,\r\n cachedAt: items.cachedAt || row.last_sync,\r\n }\r\n }\r\n\r\n async saveHouseholdMembersCache(\r\n householdId: string,\r\n members: any[]\r\n ): Promise<void> {\r\n const cacheKey = makeMembersCacheKey(householdId)\r\n const cachedAt = new Date().toISOString()\r\n\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO entities\r\n (cache_key, household_id, entity_type, items, last_sync, expected_count)\r\n VALUES (?, ?, ?, ?, ?, ?)\r\n `).run(\r\n cacheKey,\r\n householdId,\r\n '_members',\r\n JSON.stringify({ members, cachedAt }),\r\n cachedAt,\r\n members.length\r\n )\r\n }\r\n\r\n // ============================================================================\r\n // Attachment Cache Operations\r\n // ============================================================================\r\n\r\n async getAttachmentCache(fileId: string): Promise<CachedAttachment | null> {\r\n const row = this.db.prepare(\r\n 'SELECT * FROM attachments WHERE file_id = ?'\r\n ).get(fileId) as any\r\n\r\n if (!row) return null\r\n\r\n return {\r\n fileId: row.file_id,\r\n encryptedData: row.encrypted_data,\r\n entityId: row.entity_id,\r\n entityType: row.entity_type,\r\n mimeType: row.mime_type,\r\n keyType: row.key_type,\r\n version: row.version,\r\n cachedAt: row.cached_at,\r\n cryptoVersion: row.crypto_version,\r\n keyDerivationId: row.key_derivation_id,\r\n }\r\n }\r\n\r\n async saveAttachmentCache(attachment: CachedAttachment): Promise<void> {\r\n // Convert Blob to Buffer if needed\r\n let data: Buffer\r\n if (attachment.encryptedData instanceof Buffer) {\r\n data = attachment.encryptedData\r\n } else if (attachment.encryptedData instanceof Blob) {\r\n const arrayBuffer = await attachment.encryptedData.arrayBuffer()\r\n data = Buffer.from(arrayBuffer)\r\n } else {\r\n data = Buffer.from(attachment.encryptedData as any)\r\n }\r\n\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO attachments\r\n (file_id, encrypted_data, entity_id, entity_type, mime_type, key_type, version, cached_at, crypto_version, key_derivation_id)\r\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\r\n `).run(\r\n attachment.fileId,\r\n data,\r\n attachment.entityId,\r\n attachment.entityType,\r\n attachment.mimeType,\r\n attachment.keyType,\r\n attachment.version,\r\n attachment.cachedAt,\r\n attachment.cryptoVersion ?? null,\r\n attachment.keyDerivationId ?? null\r\n )\r\n console.log('[SqliteCache] Cached attachment:', attachment.fileId)\r\n }\r\n\r\n async removeAttachmentCache(fileId: string): Promise<void> {\r\n this.db.prepare('DELETE FROM attachments WHERE file_id = ?').run(fileId)\r\n }\r\n\r\n async getAttachmentsForEntity(entityId: string): Promise<CachedAttachment[]> {\r\n const rows = this.db.prepare(\r\n 'SELECT * FROM attachments WHERE entity_id = ?'\r\n ).all(entityId) as any[]\r\n\r\n return rows.map(row => ({\r\n fileId: row.file_id,\r\n encryptedData: row.encrypted_data,\r\n entityId: row.entity_id,\r\n entityType: row.entity_type,\r\n mimeType: row.mime_type,\r\n keyType: row.key_type,\r\n version: row.version,\r\n cachedAt: row.cached_at,\r\n cryptoVersion: row.crypto_version,\r\n keyDerivationId: row.key_derivation_id,\r\n }))\r\n }\r\n\r\n // ============================================================================\r\n // Cache Management\r\n // ============================================================================\r\n\r\n async clearAllCache(): Promise<void> {\r\n this.db.exec(`\r\n DELETE FROM cache_metadata;\r\n DELETE FROM credentials;\r\n DELETE FROM keys;\r\n DELETE FROM entities;\r\n DELETE FROM attachments;\r\n `)\r\n console.log('[SqliteCache] All cache data cleared')\r\n }\r\n\r\n async clearUserCache(userId: string): Promise<void> {\r\n this.db.exec('DELETE FROM cache_metadata')\r\n this.db.prepare('DELETE FROM credentials WHERE user_id = ?').run(userId)\r\n this.db.prepare('DELETE FROM keys WHERE user_id = ?').run(userId)\r\n this.db.exec('DELETE FROM entities')\r\n this.db.exec('DELETE FROM attachments')\r\n console.log('[SqliteCache] User cache cleared:', userId)\r\n }\r\n\r\n async isOfflineCacheAvailable(userId: string): Promise<boolean> {\r\n const [metadata, credential, keys] = await Promise.all([\r\n this.getMetadata(),\r\n this.getOfflineCredential(userId),\r\n this.getKeyCache(userId),\r\n ])\r\n\r\n return !!(\r\n metadata &&\r\n metadata.offlineEnabled &&\r\n metadata.userId === userId &&\r\n credential &&\r\n keys\r\n )\r\n }\r\n\r\n async getCacheStats(): Promise<CacheStats> {\r\n const metadata = await this.getMetadata()\r\n const entityCaches = await this.getAllEntityCaches()\r\n\r\n const attachmentCount = (this.db.prepare(\r\n 'SELECT COUNT(*) as count FROM attachments'\r\n ).get() as any).count\r\n\r\n return {\r\n entityTypes: entityCaches.length,\r\n totalEntities: entityCaches.reduce((sum, cache) => sum + cache.items.length, 0),\r\n attachments: attachmentCount,\r\n lastSync: metadata?.lastFullSync || null,\r\n }\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.db.close()\r\n console.log('[SqliteCache] Database closed')\r\n }\r\n}\r\n","/**\r\n * Cache Schema Constants\r\n *\r\n * Shared schema definitions for offline cache storage.\r\n *\r\n * @module @hearthcoo/cache\r\n */\r\n\r\n/**\r\n * Database/schema version\r\n * Increment when making breaking changes to the cache schema\r\n */\r\nexport const DB_VERSION = 3\r\n\r\n/**\r\n * Database name (for IndexedDB)\r\n */\r\nexport const DB_NAME = 'estatehelm-offline'\r\n\r\n/**\r\n * Object store / table names\r\n */\r\nexport const STORES = {\r\n METADATA: 'metadata',\r\n CREDENTIALS: 'credentials',\r\n KEYS: 'keys',\r\n ENTITIES: 'entities',\r\n ATTACHMENTS: 'attachments',\r\n} as const\r\n\r\n/**\r\n * Generate cache key for entity store\r\n * Format: householdId:entityType\r\n */\r\nexport function makeCacheKey(householdId: string, entityType: string): string {\r\n return `${householdId}:${entityType}`\r\n}\r\n\r\n/**\r\n * Parse cache key back to components\r\n */\r\nexport function parseCacheKey(cacheKey: string): {\r\n householdId: string\r\n entityType: string\r\n} {\r\n const [householdId, entityType] = cacheKey.split(':')\r\n return { householdId, entityType }\r\n}\r\n\r\n/**\r\n * Generate special cache key for household members\r\n * Format: _members_{householdId}\r\n */\r\nexport function makeMembersCacheKey(householdId: string): string {\r\n return `_members_${householdId}`\r\n}\r\n","/**\r\n * Cache Management\r\n *\r\n * Handles SQLite caching and synchronization with the EstateHelm API.\r\n *\r\n * @module estatehelm/cache\r\n */\r\n\r\nimport { SqliteCacheStore } from '@hearthcoo/cache-sqlite'\r\nimport type { CachedEntity } from '@hearthcoo/cache'\r\nimport type { ApiClient } from '@hearthcoo/api-client'\r\n// Use mobile export to avoid frontend-specific dependencies\r\nimport {\r\n unwrapHouseholdKey,\r\n decryptEntity,\r\n unpackEncryptedBlob,\r\n getKeyTypeForEntity,\r\n base64Encode,\r\n} from '@hearthcoo/encryption/mobile'\r\nimport type { EncryptedEntity } from '@hearthcoo/encryption/mobile'\r\nimport { CACHE_DB_PATH } from './config.js'\r\n\r\n// Singleton cache store\r\nlet cacheStore: SqliteCacheStore | null = null\r\n\r\n// In-memory cache for decrypted entities (session only)\r\nconst decryptedCache = new Map<string, any>()\r\n\r\n// In-memory cache for household keys (raw bytes)\r\nconst householdKeysCache = new Map<string, Uint8Array>()\r\n\r\n// Households from API\r\nlet householdsCache: Array<{ id: string; name: string }> = []\r\n\r\n/**\r\n * Initialize the cache store\r\n */\r\nexport async function initCache(): Promise<void> {\r\n if (!cacheStore) {\r\n cacheStore = new SqliteCacheStore(CACHE_DB_PATH)\r\n console.error(`[Cache] Initialized at ${CACHE_DB_PATH}`)\r\n }\r\n}\r\n\r\n/**\r\n * Get the cache store\r\n */\r\nexport function getCache(): SqliteCacheStore {\r\n if (!cacheStore) {\r\n throw new Error('Cache not initialized. Call initCache() first.')\r\n }\r\n return cacheStore\r\n}\r\n\r\n/**\r\n * Close the cache\r\n */\r\nexport async function closeCache(): Promise<void> {\r\n if (cacheStore) {\r\n await cacheStore.close()\r\n cacheStore = null\r\n }\r\n decryptedCache.clear()\r\n householdKeysCache.clear()\r\n}\r\n\r\n/**\r\n * Clear the cache\r\n */\r\nexport async function clearCache(): Promise<void> {\r\n const cache = getCache()\r\n await cache.clearAllCache()\r\n decryptedCache.clear()\r\n householdKeysCache.clear()\r\n householdsCache = []\r\n console.error('[Cache] Cleared')\r\n}\r\n\r\n/**\r\n * Sync cache if server has changes\r\n *\r\n * @param client - API client\r\n * @param privateKey - User's private key for decrypting household keys\r\n * @param force - Force sync even if no changes detected\r\n * @returns true if sync was performed\r\n */\r\nexport async function syncIfNeeded(\r\n client: ApiClient,\r\n privateKey: CryptoKey,\r\n force = false\r\n): Promise<boolean> {\r\n const cache = getCache()\r\n\r\n // Fetch households\r\n const households = await client.getHouseholds()\r\n householdsCache = households.map((h) => ({ id: h.id, name: h.name }))\r\n\r\n let synced = false\r\n\r\n for (const household of households) {\r\n // Load household keys\r\n await loadHouseholdKeys(client, household.id, privateKey)\r\n\r\n // Check changelog\r\n const localChangelogId = await cache.getLastChangelogId()\r\n\r\n if (!force && localChangelogId > 0) {\r\n // Check if server has changes\r\n try {\r\n const response = await client.get<{ latestChangelogId: number }>(\r\n `/households/${household.id}/sync/changes?latestOnly=true`\r\n )\r\n\r\n if (response.latestChangelogId <= localChangelogId) {\r\n console.error(`[Cache] Household ${household.id} up to date (changelog ${localChangelogId})`)\r\n continue\r\n }\r\n\r\n console.error(`[Cache] Household ${household.id} has changes (${localChangelogId} -> ${response.latestChangelogId})`)\r\n } catch (err) {\r\n console.error(`[Cache] Failed to check changelog for ${household.id}:`, err)\r\n continue\r\n }\r\n }\r\n\r\n // Sync all entity types\r\n await syncHousehold(client, household.id, cache)\r\n synced = true\r\n }\r\n\r\n return synced\r\n}\r\n\r\n/**\r\n * Sync all entities for a household\r\n */\r\nasync function syncHousehold(\r\n client: ApiClient,\r\n householdId: string,\r\n cache: SqliteCacheStore\r\n): Promise<void> {\r\n const entityTypes = [\r\n 'pet', 'property', 'vehicle', 'contact', 'insurance', 'bank_account',\r\n 'investment', 'subscription', 'maintenance_task', 'password', 'access_code',\r\n 'document', 'medical', 'prescription', 'credential', 'utility',\r\n ]\r\n\r\n let latestChangelogId = 0\r\n\r\n for (const entityType of entityTypes) {\r\n try {\r\n const response = await client.getEntities(householdId, { entityType, batched: false })\r\n const items = response.items || []\r\n\r\n // Transform to CachedEntity format\r\n const cachedItems: CachedEntity[] = items.map((item: any) => ({\r\n id: item.id,\r\n entityType: item.entityType,\r\n encryptedData: item.encryptedData,\r\n keyType: item.keyType,\r\n householdId: item.householdId,\r\n ownerUserId: item.ownerUserId,\r\n version: item.version,\r\n createdAt: item.createdAt,\r\n updatedAt: item.updatedAt,\r\n cachedAt: new Date().toISOString(),\r\n }))\r\n\r\n // Get current changelog ID\r\n const changelogResponse = await client.get<{ latestChangelogId: number }>(\r\n `/households/${householdId}/sync/changes?latestOnly=true`\r\n )\r\n latestChangelogId = Math.max(latestChangelogId, changelogResponse.latestChangelogId)\r\n\r\n await cache.saveEntityCache(householdId, entityType, cachedItems, latestChangelogId)\r\n console.error(`[Cache] Synced ${items.length} ${entityType}(s) for household ${householdId}`)\r\n } catch (err: any) {\r\n // 404 means no entities of this type - that's fine\r\n if (err.status !== 404) {\r\n console.error(`[Cache] Failed to sync ${entityType} for ${householdId}:`, err.message)\r\n }\r\n }\r\n }\r\n\r\n // Update changelog ID\r\n await cache.updateLastChangelogId(latestChangelogId, householdId)\r\n}\r\n\r\n/**\r\n * Load and cache household keys\r\n */\r\nasync function loadHouseholdKeys(\r\n client: ApiClient,\r\n householdId: string,\r\n privateKey: CryptoKey\r\n): Promise<void> {\r\n // Check if already loaded\r\n if (householdKeysCache.has(`${householdId}:general`)) {\r\n return\r\n }\r\n\r\n try {\r\n const keys = await client.getHouseholdKeys(householdId)\r\n\r\n for (const key of keys) {\r\n const cacheKey = `${householdId}:${key.keyType}`\r\n\r\n // Skip if already loaded\r\n if (householdKeysCache.has(cacheKey)) {\r\n continue\r\n }\r\n\r\n // Unwrap the household key (returns raw bytes)\r\n const householdKeyBytes = await unwrapHouseholdKey(\r\n key.encryptedKey,\r\n privateKey\r\n )\r\n\r\n householdKeysCache.set(cacheKey, householdKeyBytes)\r\n }\r\n\r\n console.error(`[Cache] Loaded ${keys.length} keys for household ${householdId}`)\r\n } catch (err) {\r\n console.error(`[Cache] Failed to load keys for household ${householdId}:`, err)\r\n throw err\r\n }\r\n}\r\n\r\n/**\r\n * Get household key for a specific key type\r\n */\r\nfunction getHouseholdKey(householdId: string, keyType: string): Uint8Array | undefined {\r\n return householdKeysCache.get(`${householdId}:${keyType}`)\r\n}\r\n\r\n/**\r\n * Get decrypted entities from cache\r\n */\r\nexport async function getDecryptedEntities(\r\n householdId: string,\r\n entityType: string,\r\n privateKey: CryptoKey\r\n): Promise<any[]> {\r\n const cache = getCache()\r\n const cacheEntry = await cache.getEntityCache(householdId, entityType)\r\n\r\n if (!cacheEntry || cacheEntry.items.length === 0) {\r\n return []\r\n }\r\n\r\n const results: any[] = []\r\n\r\n for (const item of cacheEntry.items) {\r\n const decryptCacheKey = `${householdId}:${entityType}:${item.id}`\r\n\r\n // Check in-memory cache first\r\n if (decryptedCache.has(decryptCacheKey)) {\r\n results.push(decryptedCache.get(decryptCacheKey))\r\n continue\r\n }\r\n\r\n try {\r\n // Get the appropriate household key\r\n const keyType = getKeyTypeForEntity(entityType)\r\n const householdKeyBytes = getHouseholdKey(householdId, keyType)\r\n\r\n if (!householdKeyBytes) {\r\n console.error(`[Cache] No key for ${householdId}:${keyType}`)\r\n continue\r\n }\r\n\r\n // Unpack the encrypted blob to get IV and ciphertext\r\n const { iv, ciphertext } = unpackEncryptedBlob(item.encryptedData)\r\n\r\n // Construct EncryptedEntity\r\n const encryptedEntity: EncryptedEntity = {\r\n entityId: item.id,\r\n entityType: item.entityType,\r\n keyType: item.keyType,\r\n ciphertext: base64Encode(ciphertext),\r\n iv: base64Encode(iv),\r\n encryptedAt: new Date(item.createdAt),\r\n derivedEntityKey: new Uint8Array(),\r\n }\r\n\r\n // Decrypt entity\r\n const decrypted = await decryptEntity<Record<string, any>>(\r\n householdKeyBytes,\r\n encryptedEntity\r\n )\r\n\r\n // Add metadata\r\n const entity = {\r\n ...decrypted,\r\n id: item.id,\r\n entityType: item.entityType,\r\n householdId: item.householdId,\r\n version: item.version,\r\n createdAt: item.createdAt,\r\n updatedAt: item.updatedAt,\r\n }\r\n\r\n // Cache in memory\r\n decryptedCache.set(decryptCacheKey, entity)\r\n results.push(entity)\r\n } catch (err) {\r\n console.error(`[Cache] Failed to decrypt ${entityType}:${item.id}:`, err)\r\n }\r\n }\r\n\r\n return results\r\n}\r\n\r\n/**\r\n * Get cached households\r\n */\r\nexport async function getHouseholds(): Promise<Array<{ id: string; name: string }>> {\r\n return householdsCache\r\n}\r\n\r\n/**\r\n * Get cache statistics\r\n */\r\nexport async function getCacheStats(): Promise<{\r\n entityTypes: number\r\n totalEntities: number\r\n attachments: number\r\n lastSync: string | null\r\n}> {\r\n const cache = getCache()\r\n return cache.getCacheStats()\r\n}\r\n"],"mappings":"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","names":["path","envPaths","keytar","keytar","open","import_types","fs","path","Database","synced"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/login.ts","../../api-client/src/auth.ts","../../api-client/src/types.ts","../../api-client/src/client.ts","../../encryption/src/utils.ts","../../encryption/src/householdKeys.ts","../../encryption/src/entityKeys.ts","../../encryption/src/entityEncryption.ts","../../encryption/src/entityKeyMapping.ts","../../encryption/src/recoveryKey.ts","../../types/src/keys.ts","../../types/src/options.ts","../../types/src/feature-limits.json","../../types/src/plans.ts","../../types/src/files.ts","../../types/src/navColors.ts","../../encryption/src/keyBundle.ts","../../encryption/src/asymmetricWrap.ts","../../encryption/src/webauthnDeviceBound.ts","../src/config.ts","../src/keyStore.ts","../src/server.ts","../src/filter.ts","../../cache-sqlite/src/sqliteStore.ts","../../cache/src/schema.ts","../src/cache.ts"],"sourcesContent":["#!/usr/bin/env node\r\n/**\r\n * EstateHelm CLI\r\n *\r\n * Command-line interface for EstateHelm MCP server and utilities.\r\n *\r\n * Commands:\r\n * login - Authenticate with EstateHelm\r\n * logout - Remove credentials and revoke device\r\n * mcp - Start the MCP server\r\n * status - Show current login status\r\n * sync - Force sync cache from server\r\n * cache - Cache management commands\r\n *\r\n * @module estatehelm\r\n */\r\n\r\nimport { Command } from 'commander'\r\nimport { login, logout, checkLogin } from './login.js'\r\nimport { startServer } from './server.js'\r\nimport {\r\n initCache,\r\n clearCache,\r\n getCacheStats,\r\n syncIfNeeded,\r\n closeCache,\r\n} from './cache.js'\r\nimport { loadConfig, saveConfig, PrivacyMode } from './config.js'\r\nimport { getAuthenticatedClient, getPrivateKey } from './login.js'\r\n\r\nconst program = new Command()\r\n\r\nprogram\r\n .name('estatehelm')\r\n .description('EstateHelm CLI - MCP server for AI assistants')\r\n .version('1.0.0')\r\n\r\n// Login command\r\nprogram\r\n .command('login')\r\n .description('Authenticate with EstateHelm')\r\n .action(async () => {\r\n try {\r\n await login()\r\n } catch (err: any) {\r\n console.error('Login failed:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Logout command\r\nprogram\r\n .command('logout')\r\n .description('Remove credentials and revoke device')\r\n .action(async () => {\r\n try {\r\n await initCache()\r\n await clearCache()\r\n await closeCache()\r\n await logout()\r\n } catch (err: any) {\r\n console.error('Logout failed:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// MCP server command\r\nprogram\r\n .command('mcp')\r\n .description('Start the MCP server')\r\n .option('-m, --mode <mode>', 'Privacy mode: full or safe', 'full')\r\n .option('--poll <interval>', 'Background sync interval (e.g., 15m)')\r\n .action(async (options) => {\r\n try {\r\n const mode = options.mode as PrivacyMode\r\n if (mode !== 'full' && mode !== 'safe') {\r\n console.error('Invalid mode. Use \"full\" or \"safe\".')\r\n process.exit(1)\r\n }\r\n\r\n await startServer(mode)\r\n } catch (err: any) {\r\n console.error('MCP server failed:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Status command\r\nprogram\r\n .command('status')\r\n .description('Show current login status and cache info')\r\n .action(async () => {\r\n try {\r\n const status = await checkLogin()\r\n\r\n if (!status.loggedIn) {\r\n console.log('Not logged in.')\r\n console.log('Run: estatehelm login')\r\n return\r\n }\r\n\r\n console.log('✓ Logged in')\r\n\r\n if (status.households && status.households.length > 0) {\r\n console.log(`✓ Households: ${status.households.map((h) => h.name).join(', ')}`)\r\n }\r\n\r\n // Show cache stats\r\n try {\r\n await initCache()\r\n const stats = await getCacheStats()\r\n console.log(`✓ Cache: ${stats.totalEntities} entities, ${stats.entityTypes} types`)\r\n if (stats.lastSync) {\r\n console.log(`✓ Last sync: ${stats.lastSync}`)\r\n }\r\n await closeCache()\r\n } catch {\r\n console.log('✓ Cache: Not initialized')\r\n }\r\n\r\n // Show config\r\n const config = loadConfig()\r\n console.log(`✓ Default mode: ${config.defaultMode}`)\r\n } catch (err: any) {\r\n console.error('Status check failed:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Sync command\r\nprogram\r\n .command('sync')\r\n .description('Force sync cache from server')\r\n .action(async () => {\r\n try {\r\n console.log('Syncing...')\r\n\r\n const client = await getAuthenticatedClient()\r\n if (!client) {\r\n console.error('Not logged in. Run: estatehelm login')\r\n process.exit(1)\r\n }\r\n\r\n const privateKey = await getPrivateKey()\r\n if (!privateKey) {\r\n console.error('Failed to load encryption keys. Run: estatehelm login')\r\n process.exit(1)\r\n }\r\n\r\n await initCache()\r\n await syncIfNeeded(client, privateKey, true)\r\n\r\n const stats = await getCacheStats()\r\n console.log(`✓ Synced: ${stats.totalEntities} entities, ${stats.entityTypes} types`)\r\n await closeCache()\r\n } catch (err: any) {\r\n console.error('Sync failed:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Cache command\r\nconst cacheCommand = program\r\n .command('cache')\r\n .description('Cache management commands')\r\n\r\ncacheCommand\r\n .command('clear')\r\n .description('Clear local cache')\r\n .action(async () => {\r\n try {\r\n await initCache()\r\n await clearCache()\r\n await closeCache()\r\n console.log('✓ Cache cleared')\r\n } catch (err: any) {\r\n console.error('Failed to clear cache:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\ncacheCommand\r\n .command('stats')\r\n .description('Show cache statistics')\r\n .action(async () => {\r\n try {\r\n await initCache()\r\n const stats = await getCacheStats()\r\n console.log('Cache Statistics:')\r\n console.log(` Entity types: ${stats.entityTypes}`)\r\n console.log(` Total entities: ${stats.totalEntities}`)\r\n console.log(` Attachments: ${stats.attachments}`)\r\n console.log(` Last sync: ${stats.lastSync || 'Never'}`)\r\n await closeCache()\r\n } catch (err: any) {\r\n console.error('Failed to get cache stats:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Config command\r\nconst configCommand = program\r\n .command('config')\r\n .description('Configuration management')\r\n\r\nconfigCommand\r\n .command('set <key> <value>')\r\n .description('Set a configuration value')\r\n .action((key: string, value: string) => {\r\n try {\r\n const config = loadConfig()\r\n\r\n if (key === 'defaultMode') {\r\n if (value !== 'full' && value !== 'safe') {\r\n console.error('Invalid mode. Use \"full\" or \"safe\".')\r\n process.exit(1)\r\n }\r\n config.defaultMode = value as PrivacyMode\r\n } else {\r\n console.error(`Unknown config key: ${key}`)\r\n process.exit(1)\r\n }\r\n\r\n saveConfig(config)\r\n console.log(`✓ Set ${key} = ${value}`)\r\n } catch (err: any) {\r\n console.error('Failed to set config:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\nconfigCommand\r\n .command('get [key]')\r\n .description('Get configuration value(s)')\r\n .action((key?: string) => {\r\n try {\r\n const config = loadConfig()\r\n\r\n if (key) {\r\n const value = (config as any)[key]\r\n if (value === undefined) {\r\n console.error(`Unknown config key: ${key}`)\r\n process.exit(1)\r\n }\r\n console.log(value)\r\n } else {\r\n console.log('Configuration:')\r\n for (const [k, v] of Object.entries(config)) {\r\n console.log(` ${k}: ${v}`)\r\n }\r\n }\r\n } catch (err: any) {\r\n console.error('Failed to get config:', err.message)\r\n process.exit(1)\r\n }\r\n })\r\n\r\n// Parse arguments\r\nprogram.parse()\r\n","/**\r\n * Login Flow\r\n *\r\n * Implements browser-based OAuth with local callback server.\r\n * Uses existing @hearthcoo/encryption functions for all crypto operations.\r\n *\r\n * @module estatehelm/login\r\n */\r\n\r\nimport * as http from 'http'\r\nimport * as readline from 'readline'\r\nimport open from 'open'\r\nimport { ApiClient, TokenAuthAdapter } from '@hearthcoo/api-client'\r\n// Use mobile export to avoid frontend-specific dependencies\r\nimport {\r\n parseRecoveryKey,\r\n deriveWrapKey,\r\n decryptPrivateKeyWithWrapKey,\r\n importPrivateKey,\r\n base64Decode,\r\n base64Encode,\r\n} from '@hearthcoo/encryption/mobile'\r\nimport {\r\n API_BASE_URL,\r\n APP_URL,\r\n getDeviceId,\r\n getDevicePlatform,\r\n getDeviceUserAgent,\r\n sanitizeToken,\r\n} from './config.js'\r\nimport {\r\n saveBearerToken,\r\n saveRefreshToken,\r\n saveDeviceCredentials,\r\n getCredentials,\r\n clearCredentials,\r\n getBearerToken,\r\n} from './keyStore.js'\r\n\r\n/**\r\n * Prompt user for input\r\n */\r\nfunction prompt(question: string): Promise<string> {\r\n const rl = readline.createInterface({\r\n input: process.stdin,\r\n output: process.stdout,\r\n })\r\n\r\n return new Promise((resolve) => {\r\n rl.question(question, (answer) => {\r\n rl.close()\r\n resolve(answer)\r\n })\r\n })\r\n}\r\n\r\n/**\r\n * Find an available port for the callback server\r\n */\r\nasync function findAvailablePort(startPort: number = 19847): Promise<number> {\r\n return new Promise((resolve, reject) => {\r\n const server = http.createServer()\r\n server.listen(startPort, '127.0.0.1', () => {\r\n const address = server.address()\r\n const port = typeof address === 'object' && address ? address.port : startPort\r\n server.close(() => resolve(port))\r\n })\r\n server.on('error', (err: NodeJS.ErrnoException) => {\r\n if (err.code === 'EADDRINUSE') {\r\n resolve(findAvailablePort(startPort + 1))\r\n } else {\r\n reject(err)\r\n }\r\n })\r\n })\r\n}\r\n\r\n/**\r\n * Start local callback server and wait for session token\r\n */\r\nfunction waitForCallback(port: number): Promise<string> {\r\n return new Promise((resolve, reject) => {\r\n const server = http.createServer((req, res) => {\r\n const url = new URL(req.url || '/', `http://127.0.0.1:${port}`)\r\n const sessionToken = url.searchParams.get('session_token')\r\n\r\n if (sessionToken) {\r\n // Success - send a nice response\r\n res.writeHead(200, { 'Content-Type': 'text/html' })\r\n res.end(`\r\n <!DOCTYPE html>\r\n <html>\r\n <head>\r\n <title>CLI Authentication Successful</title>\r\n <style>\r\n body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: linear-gradient(115deg, #fff1be 28%, #ee87cb 70%, #b060ff 100%); }\r\n .card { background: white; padding: 2rem; border-radius: 1rem; box-shadow: 0 10px 25px rgba(0,0,0,0.1); text-align: center; }\r\n h1 { color: #059669; margin: 0 0 0.5rem; }\r\n p { color: #6b7280; margin: 0; }\r\n </style>\r\n </head>\r\n <body>\r\n <div class=\"card\">\r\n <h1>✓ Authentication Successful</h1>\r\n <p>You can close this window and return to your terminal.</p>\r\n </div>\r\n </body>\r\n </html>\r\n `)\r\n server.close()\r\n resolve(sessionToken)\r\n } else {\r\n // Error - no token\r\n res.writeHead(400, { 'Content-Type': 'text/html' })\r\n res.end(`\r\n <!DOCTYPE html>\r\n <html>\r\n <head><title>Authentication Failed</title></head>\r\n <body>\r\n <h1>Authentication Failed</h1>\r\n <p>No session token received. Please try again.</p>\r\n </body>\r\n </html>\r\n `)\r\n }\r\n })\r\n\r\n server.listen(port, '127.0.0.1', () => {\r\n console.log(`Callback server listening on http://127.0.0.1:${port}`)\r\n })\r\n\r\n server.on('error', reject)\r\n\r\n // Timeout after 5 minutes\r\n setTimeout(() => {\r\n server.close()\r\n reject(new Error('Authentication timed out. Please try again.'))\r\n }, 5 * 60 * 1000)\r\n })\r\n}\r\n\r\n/**\r\n * WebAuthn initialize response\r\n */\r\ninterface InitializeResponse {\r\n serverWrapSecret: string\r\n}\r\n\r\n/**\r\n * Key bundle from server\r\n */\r\ninterface KeyBundle {\r\n id: string\r\n publicKey: string\r\n encryptedPrivateKey: string\r\n alg: string\r\n}\r\n\r\n/**\r\n * Create an API client with the given token\r\n */\r\nfunction createApiClient(token: string): ApiClient {\r\n return new ApiClient({\r\n baseUrl: API_BASE_URL,\r\n apiVersion: 'v2',\r\n auth: new TokenAuthAdapter(async () => token),\r\n })\r\n}\r\n\r\n/**\r\n * Get server wrap secret for key derivation\r\n */\r\nasync function getServerWrapSecret(client: ApiClient): Promise<Uint8Array> {\r\n const response = await client.post<InitializeResponse>('/webauthn/initialize', {})\r\n return base64Decode(response.serverWrapSecret)\r\n}\r\n\r\n/**\r\n * Get current key bundle\r\n */\r\nasync function getCurrentKeyBundle(client: ApiClient): Promise<KeyBundle> {\r\n return client.get<KeyBundle>('/webauthn/key-bundles/current')\r\n}\r\n\r\n/**\r\n * Register as trusted device\r\n */\r\nasync function registerTrustedDevice(\r\n client: ApiClient,\r\n keyBundleId: string,\r\n privateKeyBytes: Uint8Array\r\n): Promise<{ credentialId: string; encryptedPayload: string }> {\r\n // Generate a new device-specific key to re-encrypt the private key\r\n const deviceKey = crypto.getRandomValues(new Uint8Array(32))\r\n const deviceKeyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n deviceKey,\r\n 'AES-GCM',\r\n false,\r\n ['encrypt', 'decrypt']\r\n )\r\n\r\n // Encrypt private key with device key\r\n const iv = crypto.getRandomValues(new Uint8Array(12))\r\n const ciphertext = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n deviceKeyMaterial,\r\n privateKeyBytes as BufferSource\r\n )\r\n\r\n // Pack: device key + iv + ciphertext\r\n const encryptedPayload = base64Encode(\r\n new Uint8Array([\r\n ...deviceKey,\r\n ...iv,\r\n ...new Uint8Array(ciphertext),\r\n ])\r\n )\r\n\r\n const deviceId = getDeviceId()\r\n\r\n // Register with server\r\n const response = await client.post<{ id: string }>('/webauthn/credentials', {\r\n userKeyBundleId: keyBundleId,\r\n credentialType: 'trusted-device',\r\n credentialId: base64Encode(new TextEncoder().encode(deviceId)),\r\n encryptedPayload,\r\n devicePlatform: getDevicePlatform(),\r\n deviceUserAgent: getDeviceUserAgent(),\r\n })\r\n\r\n return {\r\n credentialId: response.id,\r\n encryptedPayload,\r\n }\r\n}\r\n\r\n/**\r\n * Decrypt device credentials to get private key\r\n */\r\nexport async function decryptDeviceCredentials(\r\n encryptedPayload: string\r\n): Promise<Uint8Array> {\r\n const packed = base64Decode(encryptedPayload)\r\n\r\n // Unpack: device key (32 bytes) + iv (12 bytes) + ciphertext\r\n const deviceKey = packed.slice(0, 32)\r\n const iv = packed.slice(32, 44)\r\n const ciphertext = packed.slice(44)\r\n\r\n // Import device key\r\n const deviceKeyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n deviceKey,\r\n 'AES-GCM',\r\n false,\r\n ['decrypt']\r\n )\r\n\r\n // Decrypt\r\n const plaintext = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv },\r\n deviceKeyMaterial,\r\n ciphertext\r\n )\r\n\r\n return new Uint8Array(plaintext)\r\n}\r\n\r\n/**\r\n * Login with browser-based OAuth and recovery key\r\n */\r\nexport async function login(): Promise<void> {\r\n console.log('\\nEstateHelm Login')\r\n console.log('================\\n')\r\n\r\n // Step 1: Start local callback server\r\n console.log('Starting authentication server...')\r\n const port = await findAvailablePort()\r\n const callbackUrl = `http://127.0.0.1:${port}/callback`\r\n\r\n // Build the login URL\r\n const loginUrl = `${APP_URL}/signin?redirect=/cli-auth?callback=${encodeURIComponent(callbackUrl)}`\r\n\r\n console.log(`\\nOpening browser for authentication...`)\r\n console.log(`If the browser doesn't open, visit: ${loginUrl}\\n`)\r\n\r\n // Start waiting for callback before opening browser\r\n const callbackPromise = waitForCallback(port)\r\n\r\n // Open browser\r\n await open(loginUrl)\r\n\r\n // Wait for the callback with session token\r\n console.log('Waiting for authentication...')\r\n const sessionToken = await callbackPromise\r\n\r\n console.log('Authentication successful!')\r\n console.log(`Token: ${sanitizeToken(sessionToken)}`)\r\n\r\n // Save token (Kratos session token is used as bearer token)\r\n await saveBearerToken(sessionToken)\r\n // Note: No refresh token with Kratos session tokens\r\n await saveRefreshToken('')\r\n\r\n // Step 2: Get server wrap secret\r\n const client = createApiClient(sessionToken)\r\n console.log('\\nFetching encryption keys...')\r\n const serverWrapSecret = await getServerWrapSecret(client)\r\n\r\n // Step 3: Get current key bundle\r\n const keyBundle = await getCurrentKeyBundle(client)\r\n console.log(`Key bundle: ${keyBundle.id} (${keyBundle.alg})`)\r\n\r\n // Step 4: Prompt for recovery key\r\n console.log('\\nYour Recovery Key is required to decrypt your data.')\r\n console.log('Format: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX\\n')\r\n const recoveryKeyInput = await prompt('Enter your Recovery Key: ')\r\n\r\n // Parse and validate recovery key\r\n const recoveryKey = parseRecoveryKey(recoveryKeyInput.trim())\r\n console.log('Recovery key validated!')\r\n\r\n // Step 5: Derive wrap key and decrypt private key\r\n const wrapKey = await deriveWrapKey(recoveryKey.bytes, serverWrapSecret)\r\n const privateKeyBytes = await decryptPrivateKeyWithWrapKey(\r\n keyBundle.encryptedPrivateKey,\r\n wrapKey\r\n )\r\n console.log('Private key decrypted!')\r\n\r\n // Step 6: Register as trusted device\r\n console.log('Registering device...')\r\n const credentials = await registerTrustedDevice(client, keyBundle.id, privateKeyBytes)\r\n\r\n // Save device credentials\r\n await saveDeviceCredentials({\r\n credentialId: credentials.credentialId,\r\n encryptedPayload: credentials.encryptedPayload,\r\n privateKeyBytes: base64Encode(privateKeyBytes),\r\n })\r\n\r\n console.log('\\n✓ Login complete!')\r\n console.log('You can now use: estatehelm mcp')\r\n}\r\n\r\n/**\r\n * Check if user is logged in and credentials are valid\r\n */\r\nexport async function checkLogin(): Promise<{\r\n loggedIn: boolean\r\n email?: string\r\n households?: Array<{ id: string; name: string }>\r\n}> {\r\n const credentials = await getCredentials()\r\n if (!credentials) {\r\n return { loggedIn: false }\r\n }\r\n\r\n try {\r\n const client = createApiClient(credentials.bearerToken)\r\n\r\n // Verify token is still valid by fetching user info\r\n const households = await client.getHouseholds()\r\n\r\n return {\r\n loggedIn: true,\r\n households: households.map((h) => ({ id: h.id, name: h.name })),\r\n }\r\n } catch (error) {\r\n // Token might be expired or revoked\r\n return { loggedIn: false }\r\n }\r\n}\r\n\r\n/**\r\n * Logout and clear credentials\r\n */\r\nexport async function logout(): Promise<void> {\r\n console.log('Logging out...')\r\n\r\n const credentials = await getCredentials()\r\n if (credentials) {\r\n try {\r\n // Try to revoke device from server\r\n const client = createApiClient(credentials.bearerToken)\r\n await client.delete(`/webauthn/credentials/${credentials.deviceCredentials.credentialId}`)\r\n console.log('Device revoked from server')\r\n } catch {\r\n // Ignore errors - device might already be revoked\r\n }\r\n }\r\n\r\n await clearCredentials()\r\n console.log('✓ Logged out')\r\n}\r\n\r\n/**\r\n * Get a ready-to-use API client\r\n */\r\nexport async function getAuthenticatedClient(): Promise<ApiClient | null> {\r\n const token = await getBearerToken()\r\n if (!token) return null\r\n return createApiClient(token)\r\n}\r\n\r\n/**\r\n * Get decrypted private key from stored credentials\r\n */\r\nexport async function getPrivateKey(): Promise<CryptoKey | null> {\r\n const credentials = await getCredentials()\r\n if (!credentials) return null\r\n\r\n try {\r\n const privateKeyBytes = await decryptDeviceCredentials(\r\n credentials.deviceCredentials.encryptedPayload\r\n )\r\n return importPrivateKey(privateKeyBytes)\r\n } catch {\r\n return null\r\n }\r\n}\r\n","/**\r\n * Auth Adapter Interface\r\n *\r\n * Platform-specific auth handling is injected via this interface.\r\n * - Web: Uses cookies (credentials: 'include')\r\n * - Mobile: Uses Bearer token from SecureStore\r\n */\r\n\r\nexport interface AuthAdapter {\r\n /**\r\n * Get auth headers to add to requests.\r\n * Returns empty object if no auth available.\r\n */\r\n getAuthHeaders(): Promise<Record<string, string>>\r\n\r\n /**\r\n * Get RequestInit credentials mode.\r\n * - 'include' for cookie-based auth (web)\r\n * - 'same-origin' or 'omit' for token-based (mobile)\r\n */\r\n getCredentials(): RequestCredentials\r\n}\r\n\r\n/**\r\n * Cookie-based auth adapter for web.\r\n * Uses credentials: 'include' to send cookies with requests.\r\n */\r\nexport class CookieAuthAdapter implements AuthAdapter {\r\n async getAuthHeaders(): Promise<Record<string, string>> {\r\n return {}\r\n }\r\n\r\n getCredentials(): RequestCredentials {\r\n return 'include'\r\n }\r\n}\r\n\r\n/**\r\n * Token-based auth adapter for mobile.\r\n * Uses Authorization header with Bearer token.\r\n */\r\nexport class TokenAuthAdapter implements AuthAdapter {\r\n private getToken: () => Promise<string | null>\r\n\r\n constructor(getToken: () => Promise<string | null>) {\r\n this.getToken = getToken\r\n }\r\n\r\n async getAuthHeaders(): Promise<Record<string, string>> {\r\n const token = await this.getToken()\r\n if (token) {\r\n return { Authorization: `Bearer ${token}` }\r\n }\r\n return {}\r\n }\r\n\r\n getCredentials(): RequestCredentials {\r\n return 'same-origin'\r\n }\r\n}\r\n","/**\r\n * @hearthcoo/api-client\r\n *\r\n * Shared TypeScript types for API client.\r\n */\r\n\r\n// ===== REQUEST/RESPONSE TYPES =====\r\n\r\nexport interface RequestOptions {\r\n headers?: Record<string, string>\r\n body?: unknown\r\n}\r\n\r\nexport interface ApiError extends Error {\r\n status?: number\r\n code?: 'FORBIDDEN' | 'VERSION_CONFLICT' | 'NOT_FOUND' | string\r\n error?: unknown\r\n // Version conflict details (when code === 'VERSION_CONFLICT')\r\n currentVersion?: string\r\n expectedVersion?: string\r\n // Set to true when error has been shown to user (prevents duplicate toasts)\r\n handled?: boolean\r\n}\r\n\r\n// ===== HOUSEHOLD TYPES =====\r\n\r\nexport const HOUSEHOLD_MEMBER_ROLES = {\r\n OWNER: 'owner',\r\n MEMBER: 'member',\r\n EXECUTOR: 'executor'\r\n} as const\r\n\r\nexport type HouseholdMemberRole = typeof HOUSEHOLD_MEMBER_ROLES[keyof typeof HOUSEHOLD_MEMBER_ROLES]\r\n\r\nexport function isValidHouseholdRole(role: unknown): role is HouseholdMemberRole {\r\n return Object.values(HOUSEHOLD_MEMBER_ROLES).includes(role as HouseholdMemberRole)\r\n}\r\n\r\nexport interface Household {\r\n id: string\r\n name: string\r\n myRole: string\r\n createdAt: string\r\n subscriptionStatus?: string\r\n planCode?: string\r\n trialEndsAt?: string\r\n entityCount?: number\r\n}\r\n\r\nexport interface ExtendTrialResponse {\r\n trialEndsAt: string\r\n trialDays: number\r\n}\r\n\r\nexport interface HouseholdMember {\r\n id: string\r\n userId: string\r\n email: string\r\n displayName?: string\r\n role: string\r\n joinedAt: string\r\n invitedBy?: string\r\n keyBundleId?: string\r\n keyBundlePublicKey?: string\r\n keyBundleAlg?: string\r\n}\r\n\r\n// ===== USER KEY BUNDLE TYPES =====\r\n\r\nexport interface UserKeyBundle {\r\n id: string\r\n version: number\r\n publicKey: string\r\n encryptedPrivateKey: string\r\n alg: string\r\n hasPRFEnvelope?: boolean\r\n}\r\n\r\n// ===== HOUSEHOLD KEY TYPES =====\r\n\r\nexport interface HouseholdKey {\r\n id?: string\r\n householdId?: string | null\r\n ownerUserId?: string | null\r\n keyType: string\r\n entityId?: string | null\r\n encryptedKey: string\r\n userKeyBundleId: string\r\n keyBundleVersion: number\r\n keyBundlePublicKey: string\r\n keyBundleEncryptedPrivateKey: string\r\n keyBundleEnc: string\r\n keyBundleAlg: string\r\n grantedAt: string\r\n grantedBy: string\r\n}\r\n\r\nexport interface MemberKeyAccessSummary {\r\n userId: string\r\n email: string\r\n displayName?: string\r\n role: string\r\n keyTypes: string[]\r\n}\r\n\r\nexport interface KeyTypeInfo {\r\n keyType: string\r\n isDefault: boolean\r\n memberCount: number\r\n}\r\n\r\n// ===== ENCRYPTED ENTITY TYPES =====\r\n\r\nexport interface EncryptedEntityResponse {\r\n id: string\r\n householdId: string | null\r\n ownerUserId?: string | null\r\n entityType: string\r\n keyType: string\r\n encryptedData: string\r\n cryptoVersion: number\r\n version: number // Optimistic concurrency version (use as If-Match header)\r\n thumbnailData?: string\r\n createdBy: string\r\n createdAt: string\r\n updatedAt: string\r\n deletedAt?: string\r\n}\r\n\r\nexport interface CreateEntityRequest {\r\n id: string\r\n entityType: string\r\n keyType: string\r\n encryptedData: string\r\n cryptoVersion?: number\r\n thumbnailData?: string\r\n}\r\n\r\nexport interface UpdateEntityRequest {\r\n keyType?: string\r\n encryptedData: string\r\n cryptoVersion?: number\r\n thumbnailData?: string\r\n deleteFileIds?: string[] // File IDs to delete atomically with the update\r\n}\r\n\r\nexport interface EntitiesResponse {\r\n total: number\r\n items: EncryptedEntityResponse[]\r\n}\r\n\r\n// ===== CONTACT TYPES =====\r\n\r\nexport interface Contact {\r\n id: string\r\n householdId: string\r\n type: string\r\n companyName?: string\r\n firstName?: string\r\n lastName?: string\r\n email?: string\r\n phonePrimary?: string\r\n phoneSecondary?: string\r\n website?: string\r\n streetAddress?: string\r\n city?: string\r\n state?: string\r\n postalCode?: string\r\n specialty?: string\r\n licenseNumber?: string\r\n rating?: number\r\n notes?: string\r\n isFavorite?: boolean\r\n createdAt: string\r\n updatedAt: string\r\n}\r\n\r\nexport interface CreateContactRequest {\r\n type: string\r\n company_name?: string\r\n first_name?: string\r\n last_name?: string\r\n email?: string\r\n phone_primary?: string\r\n phone_secondary?: string\r\n website?: string\r\n street_address?: string\r\n city?: string\r\n state?: string\r\n postal_code?: string\r\n specialty?: string\r\n license_number?: string\r\n rating?: number\r\n notes?: string\r\n is_favorite?: boolean\r\n}\r\n\r\n// ===== INSURANCE TYPES =====\r\n\r\nexport interface InsurancePolicy {\r\n id: string\r\n householdId: string\r\n provider: string\r\n policyNumber: string\r\n effectiveDate: string\r\n expirationDate: string\r\n type: 'home' | 'auto' | 'umbrella' | 'life' | 'health' | 'pet' | 'collection' | 'other'\r\n propertyId?: string\r\n vehicleId?: string\r\n petId?: string\r\n createdAt: string\r\n updatedAt: string\r\n}\r\n\r\n// ===== KEY BATCH TYPES =====\r\n\r\nexport interface BatchAddKeysRequest {\r\n keyBundleId: string\r\n keys: Array<{\r\n keyType: string\r\n wrappedKey: string\r\n householdId?: string\r\n }>\r\n}\r\n\r\nexport interface BatchAddKeysResponse {\r\n message: string\r\n keys: Array<{\r\n keyType: string\r\n householdId?: string\r\n id: string\r\n created: boolean\r\n }>\r\n}\r\n\r\n// ===== FILE TYPES =====\r\n\r\nexport interface FileMetadata {\r\n id: string\r\n householdId: string\r\n entityId?: string\r\n entityType?: string\r\n fileName: string\r\n fileType?: string\r\n fileSizeBytes: number\r\n storageProvider: string\r\n storagePath: string\r\n keyType: string\r\n cryptoVersion: number\r\n createdBy: string\r\n createdAt: string\r\n deletedAt?: string\r\n thumbnailStoragePath?: string\r\n thumbnailDownloadUrl?: string\r\n downloadUrl?: string\r\n}\r\n\r\nexport interface GenerateUploadUrlRequest {\r\n contentType: string\r\n keyType: string\r\n fileSizeBytes?: number\r\n includeThumbnailUrl?: boolean\r\n}\r\n\r\nexport interface GenerateUploadUrlResponse {\r\n url: string\r\n fileId: string\r\n storagePath: string\r\n expiresAt: string\r\n thumbnailUrl?: string\r\n thumbnailStoragePath?: string\r\n}\r\n\r\nexport interface CreateFileMetadataRequest {\r\n fileId: string\r\n storagePath: string\r\n entityId?: string\r\n entityType?: string\r\n fileName: string\r\n fileType?: string\r\n fileSizeBytes: number\r\n keyType: string\r\n cryptoVersion: number\r\n thumbnailStoragePath?: string\r\n}\r\n\r\n// ===== BILLING TYPES =====\r\n\r\nexport interface CreateCheckoutSessionRequest {\r\n priceId: string\r\n}\r\n\r\nexport interface CheckoutSessionResponse {\r\n sessionId: string\r\n checkoutUrl: string\r\n}\r\n\r\nexport interface CustomerPortalSessionResponse {\r\n portalUrl: string\r\n}\r\n\r\nexport interface AvailablePlan {\r\n priceId: string\r\n planCode: string\r\n billingCycle: 'monthly' | 'annual'\r\n}\r\n\r\nexport interface StartTrialResponse {\r\n planCode: string\r\n trialEndsAt: string\r\n trialDays: number\r\n}\r\n\r\nexport interface UpgradeSubscriptionResponse {\r\n success: boolean\r\n newPriceId: string\r\n subscriptionId: string\r\n}\r\n\r\nexport interface BillingDetailsResponse {\r\n billingCycle: 'monthly' | 'annual' | null\r\n amountCents: number\r\n paymentProvider: 'stripe' | 'apple' | 'google' | null\r\n nextBillingDate?: string\r\n isLifetime: boolean\r\n}\r\n\r\nexport interface PlanLimitExceededError {\r\n error: 'plan_limit_exceeded'\r\n message: string\r\n upgradeRequired: boolean\r\n requiredPlan?: string\r\n}\r\n","/**\r\n * API Client - Platform-Agnostic HTTP Client\r\n *\r\n * Shared between web and mobile apps.\r\n * All API requests go through this client.\r\n */\r\n\r\nimport type { AuthAdapter } from './auth'\r\nimport type {\r\n RequestOptions,\r\n ApiError,\r\n Household,\r\n HouseholdMember,\r\n HouseholdMemberRole,\r\n UserKeyBundle,\r\n HouseholdKey,\r\n MemberKeyAccessSummary,\r\n KeyTypeInfo,\r\n EncryptedEntityResponse,\r\n CreateEntityRequest,\r\n UpdateEntityRequest,\r\n EntitiesResponse,\r\n Contact,\r\n CreateContactRequest,\r\n InsurancePolicy,\r\n BatchAddKeysRequest,\r\n BatchAddKeysResponse,\r\n CheckoutSessionResponse,\r\n CustomerPortalSessionResponse,\r\n AvailablePlan,\r\n StartTrialResponse,\r\n UpgradeSubscriptionResponse,\r\n BillingDetailsResponse,\r\n ExtendTrialResponse,\r\n} from './types'\r\nimport { HOUSEHOLD_MEMBER_ROLES } from './types'\r\n\r\nexport interface ApiClientConfig {\r\n baseUrl: string\r\n apiVersion: string\r\n auth: AuthAdapter\r\n /** Called when a 401 Unauthorized error is received. Use this to trigger re-authentication. */\r\n onAuthError?: () => void\r\n}\r\n\r\n/**\r\n * Batches multiple getEntities calls into a single request.\r\n * Collects requests over one event loop tick, then combines them.\r\n */\r\nclass EntityBatcher {\r\n private pending = new Map<string, { resolve: (items: any[]) => void; reject: (err: any) => void }>()\r\n private householdId: string | null | undefined = null\r\n private includeDeleted = false\r\n private timer: ReturnType<typeof setTimeout> | null = null\r\n private fetchFn: (householdId: string | null | undefined, entityTypes: string[], includeDeleted: boolean) => Promise<any>\r\n\r\n constructor(fetchFn: (householdId: string | null | undefined, entityTypes: string[], includeDeleted: boolean) => Promise<any>) {\r\n this.fetchFn = fetchFn\r\n }\r\n\r\n request(householdId: string | null | undefined, entityType: string, includeDeleted: boolean = false): Promise<any[]> {\r\n // If householdId changes, flush existing batch first\r\n if (this.pending.size > 0 && this.householdId !== householdId) {\r\n this.flush()\r\n }\r\n\r\n this.householdId = householdId\r\n this.includeDeleted = includeDeleted\r\n\r\n return new Promise((resolve, reject) => {\r\n this.pending.set(entityType, { resolve, reject })\r\n\r\n // Schedule flush for next tick if not already scheduled\r\n if (!this.timer) {\r\n this.timer = setTimeout(() => this.flush(), 0)\r\n }\r\n })\r\n }\r\n\r\n private async flush() {\r\n const entityTypes = Array.from(this.pending.keys())\r\n const callbacks = new Map(this.pending)\r\n const householdId = this.householdId\r\n const includeDeleted = this.includeDeleted\r\n\r\n // Reset state\r\n this.pending.clear()\r\n this.timer = null\r\n this.householdId = null\r\n this.includeDeleted = false\r\n\r\n if (entityTypes.length === 0) return\r\n\r\n try {\r\n const response = await this.fetchFn(householdId, entityTypes, includeDeleted)\r\n const items = response.items || []\r\n\r\n // Group items by entityType and distribute to callers\r\n const groupedByType = new Map<string, any[]>()\r\n for (const type of entityTypes) {\r\n groupedByType.set(type, [])\r\n }\r\n\r\n for (const item of items) {\r\n const type = item.entityType\r\n if (groupedByType.has(type)) {\r\n groupedByType.get(type)!.push(item)\r\n }\r\n }\r\n\r\n // Resolve each caller with their specific items\r\n for (const [type, { resolve }] of callbacks) {\r\n resolve(groupedByType.get(type) || [])\r\n }\r\n } catch (err) {\r\n // Reject all pending requests\r\n for (const { reject } of callbacks.values()) {\r\n reject(err)\r\n }\r\n }\r\n }\r\n}\r\n\r\nexport class ApiClient {\r\n private config: ApiClientConfig\r\n // Cache for in-flight GET requests to deduplicate concurrent calls\r\n private inFlightRequests = new Map<string, Promise<any>>()\r\n // Batcher for entity requests\r\n private entityBatcher: EntityBatcher\r\n\r\n constructor(config: ApiClientConfig) {\r\n this.config = config\r\n // Initialize entity batcher with fetch function\r\n this.entityBatcher = new EntityBatcher((householdId, entityTypes, includeDeleted) =>\r\n this.getEntitiesMultiple(householdId, entityTypes, includeDeleted)\r\n )\r\n }\r\n\r\n private getApiUrl(path: string): string {\r\n const cleanPath = path.startsWith('/') ? path : `/${path}`\r\n return `${this.config.baseUrl}/api/${this.config.apiVersion}${cleanPath}`\r\n }\r\n\r\n private async request<T>(\r\n method: string,\r\n path: string,\r\n options?: RequestOptions\r\n ): Promise<T> {\r\n const url = this.getApiUrl(path)\r\n const authHeaders = await this.config.auth.getAuthHeaders()\r\n const headers: Record<string, string> = {\r\n 'Content-Type': 'application/json',\r\n ...authHeaders,\r\n ...options?.headers,\r\n }\r\n\r\n const config: RequestInit = {\r\n method,\r\n headers,\r\n credentials: this.config.auth.getCredentials(),\r\n }\r\n\r\n if (options?.body) {\r\n config.body = JSON.stringify(options.body)\r\n }\r\n\r\n let response: Response\r\n try {\r\n response = await fetch(url, config)\r\n } catch (networkError) {\r\n // Network error (server down, no internet, CORS, etc.)\r\n console.error('Network error:', { url, method, error: networkError })\r\n const error = Object.assign(\r\n new Error('Unable to connect to server. Please check your connection and try again.'),\r\n { status: 0, code: 'NETWORK_ERROR', originalError: networkError }\r\n )\r\n throw error\r\n }\r\n\r\n if (!response.ok) {\r\n const error = await response.json().catch(() => ({\r\n message: 'An error occurred',\r\n }))\r\n\r\n // Use lower log level for client errors (400, 404) as they're often expected\r\n const logLevel = response.status === 400 || response.status === 404 ? console.debug : console.error\r\n logLevel('API Error:', {\r\n url,\r\n method,\r\n status: response.status,\r\n error,\r\n })\r\n\r\n // Handle authentication errors (401 Unauthorized)\r\n if (response.status === 401) {\r\n console.warn('Authentication required')\r\n // Trigger re-authentication callback if configured\r\n this.config.onAuthError?.()\r\n throw Object.assign(new Error('Authentication required'), { status: 401 })\r\n }\r\n\r\n // Handle payment required errors (402 Payment Required - plan limits)\r\n if (response.status === 402) {\r\n throw Object.assign(\r\n new Error(error.message || 'Upgrade required to access this feature'),\r\n { \r\n status: 402, \r\n code: error.error || 'limit_exceeded',\r\n details: error.details,\r\n requiredPlan: error.details?.requiredPlan\r\n }\r\n )\r\n }\r\n\r\n // Handle authorization errors (403 Forbidden)\r\n if (response.status === 403) {\r\n throw Object.assign(\r\n new Error(error.message || 'You do not have permission to perform this action'),\r\n { status: 403, code: 'FORBIDDEN' }\r\n )\r\n }\r\n\r\n // Handle version conflict errors (412 Precondition Failed)\r\n // Note: Don't include user-facing message here - callers should localize based on code\r\n if (response.status === 412) {\r\n throw Object.assign(\r\n new Error('VERSION_CONFLICT'),\r\n {\r\n status: 412,\r\n code: 'VERSION_CONFLICT',\r\n currentVersion: error.details?.currentVersion,\r\n expectedVersion: error.details?.expectedVersion,\r\n }\r\n )\r\n }\r\n\r\n // Extract validation errors if present\r\n let errorMessage = error.error || error.message || error.title || `HTTP ${response.status}`\r\n\r\n // Handle .NET validation errors format\r\n if (error.errors && typeof error.errors === 'object') {\r\n const validationErrors = Object.entries(error.errors)\r\n .map(([field, messages]) => {\r\n const msgs = Array.isArray(messages) ? messages : [messages]\r\n return `${field}: ${msgs.join(', ')}`\r\n })\r\n .join('; ')\r\n errorMessage = validationErrors || errorMessage\r\n }\r\n\r\n const apiError: ApiError = Object.assign(new Error(errorMessage), {\r\n status: response.status,\r\n error,\r\n })\r\n throw apiError\r\n }\r\n\r\n // Handle 204 No Content\r\n if (response.status === 204) {\r\n return {} as T\r\n }\r\n\r\n return response.json()\r\n }\r\n\r\n async get<T>(path: string, options?: RequestOptions): Promise<T> {\r\n // Deduplicate certain GET requests that may be called multiple times during bootstrap\r\n const deduplicatePaths = ['/webauthn/credentials', '/webauthn/key-bundles/current']\r\n const shouldDeduplicate = deduplicatePaths.some(p => path.includes(p))\r\n \r\n if (shouldDeduplicate) {\r\n const cacheKey = `get:${path}`\r\n const inFlight = this.inFlightRequests.get(cacheKey)\r\n if (inFlight) {\r\n return inFlight\r\n }\r\n \r\n const request = this.request<T>('GET', path, options)\r\n .finally(() => this.inFlightRequests.delete(cacheKey))\r\n \r\n this.inFlightRequests.set(cacheKey, request)\r\n return request\r\n }\r\n \r\n return this.request<T>('GET', path, options)\r\n }\r\n\r\n async post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {\r\n return this.request<T>('POST', path, { ...options, body })\r\n }\r\n\r\n async put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {\r\n return this.request<T>('PUT', path, { ...options, body })\r\n }\r\n\r\n async patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T> {\r\n return this.request<T>('PATCH', path, { ...options, body })\r\n }\r\n\r\n async delete<T>(path: string, options?: RequestOptions): Promise<T> {\r\n return this.request<T>('DELETE', path, options)\r\n }\r\n\r\n /**\r\n * Fetch binary data as a Blob (for downloading files like PDFs).\r\n */\r\n async getBlob(path: string, options?: RequestOptions): Promise<Blob> {\r\n const url = this.getApiUrl(path)\r\n const authHeaders = await this.config.auth.getAuthHeaders()\r\n const headers: Record<string, string> = {\r\n ...authHeaders,\r\n ...options?.headers,\r\n }\r\n\r\n const config: RequestInit = {\r\n method: 'GET',\r\n headers,\r\n credentials: this.config.auth.getCredentials(),\r\n }\r\n\r\n let response: Response\r\n try {\r\n response = await fetch(url, config)\r\n } catch (networkError) {\r\n console.error('Network error:', { url, error: networkError })\r\n const error = Object.assign(\r\n new Error('Unable to connect to server. Please check your connection and try again.'),\r\n { status: 0, code: 'NETWORK_ERROR', originalError: networkError }\r\n )\r\n throw error\r\n }\r\n\r\n if (!response.ok) {\r\n const error = await response.json().catch(() => ({\r\n message: 'An error occurred',\r\n }))\r\n const apiError: ApiError = Object.assign(new Error(error.message || `HTTP ${response.status}`), {\r\n status: response.status,\r\n error,\r\n })\r\n throw apiError\r\n }\r\n\r\n return response.blob()\r\n }\r\n\r\n // ===== HOUSEHOLD METHODS =====\r\n\r\n async getHouseholds(): Promise<Household[]> {\r\n return this.get<Household[]>('/households')\r\n }\r\n\r\n async createHousehold(data: { name: string; planCode?: string }): Promise<Household> {\r\n return this.post<Household>('/households', data)\r\n }\r\n\r\n async updateHousehold(id: string, data: { name: string }): Promise<void> {\r\n await this.put(`/households/${id}`, data)\r\n }\r\n\r\n async deleteHousehold(id: string): Promise<void> {\r\n await this.delete(`/households/${id}`)\r\n }\r\n\r\n /**\r\n * Extend trial for households with less than 5 entities.\r\n * Allows users who haven't fully explored to continue without subscribing.\r\n */\r\n async extendTrial(householdId: string): Promise<ExtendTrialResponse> {\r\n return this.post<ExtendTrialResponse>(`/households/${householdId}/extend-trial`, {})\r\n }\r\n\r\n /**\r\n * Permanently delete the current user's account and all personal data.\r\n * This includes: key bundles, WebAuthn credentials, personal vault entities,\r\n * member key access records, legal consents, user profile, and authentication identity.\r\n *\r\n * WARNING: This action is irreversible. The user must re-register to use the service again.\r\n */\r\n async deleteCurrentUser(): Promise<void> {\r\n await this.delete('/users/me')\r\n }\r\n\r\n async checkUserHasHousehold(): Promise<{ hasHousehold: boolean; households?: Household[] }> {\r\n const households = await this.getHouseholds()\r\n return {\r\n hasHousehold: households.length > 0,\r\n households: households.length > 0 ? households : undefined\r\n }\r\n }\r\n\r\n // ===== HOUSEHOLD KEYS METHODS =====\r\n\r\n async getHouseholdKeys(householdId: string): Promise<HouseholdKey[]> {\r\n const cacheKey = `getHouseholdKeys:${householdId}`\r\n \r\n // Return in-flight request if one exists (deduplication)\r\n const inFlight = this.inFlightRequests.get(cacheKey)\r\n if (inFlight) {\r\n return inFlight\r\n }\r\n \r\n // Create new request and cache it\r\n const request = this.get<HouseholdKey[]>(`/households/${householdId}/keys`)\r\n .finally(() => {\r\n // Remove from cache when complete (success or failure)\r\n this.inFlightRequests.delete(cacheKey)\r\n })\r\n \r\n this.inFlightRequests.set(cacheKey, request)\r\n return request\r\n }\r\n\r\n async grantHouseholdKey(householdId: string, data: {\r\n userId: string\r\n keyType: string\r\n encryptedKey: string\r\n }): Promise<void> {\r\n await this.post(`/households/${householdId}/keys/grant`, data)\r\n }\r\n\r\n async batchGrantHouseholdKeys(householdId: string, data: {\r\n userId: string\r\n keys: Array<{\r\n keyType: string\r\n encryptedKey: string\r\n userKeyBundleId: string\r\n }>\r\n }): Promise<void> {\r\n await this.post(`/households/${householdId}/keys/grant/batch`, data)\r\n }\r\n\r\n async revokeHouseholdKey(householdId: string, userId: string, keyType: string): Promise<void> {\r\n await this.delete(`/households/${householdId}/keys/${userId}/${keyType}`)\r\n }\r\n\r\n async getMembersKeyAccess(householdId: string): Promise<MemberKeyAccessSummary[]> {\r\n return this.get<MemberKeyAccessSummary[]>(`/households/${householdId}/keys/access`)\r\n }\r\n\r\n async getKeyTypes(householdId: string): Promise<KeyTypeInfo[]> {\r\n return this.get<KeyTypeInfo[]>(`/households/${householdId}/keys/types`)\r\n }\r\n\r\n async deleteKeyType(householdId: string, keyType: string): Promise<void> {\r\n await this.delete(`/households/${householdId}/keys/types/${keyType}`)\r\n }\r\n\r\n // ===== ENCRYPTED ENTITIES METHODS =====\r\n\r\n /**\r\n * Fetch entities for a single type. When batched=true (default), requests are\r\n * automatically combined with other concurrent calls into a single HTTP request.\r\n */\r\n async getEntities(householdId: string | null | undefined, params?: {\r\n entityType?: string\r\n limit?: number\r\n offset?: number\r\n includeDeleted?: boolean\r\n batched?: boolean // Default true - batch with other concurrent requests\r\n }): Promise<EntitiesResponse> {\r\n const batched = params?.batched ?? true\r\n\r\n // Use batcher for single entity type requests (most common case)\r\n if (batched && params?.entityType && !params?.limit && !params?.offset) {\r\n const items = await this.entityBatcher.request(\r\n householdId,\r\n params.entityType,\r\n params.includeDeleted ?? false\r\n )\r\n return { items, total: items.length }\r\n }\r\n\r\n // Fall back to direct request for non-batchable cases\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n if (params?.entityType) queryParams.set('entityType', params.entityType)\r\n if (params?.limit) queryParams.set('limit', params.limit.toString())\r\n if (params?.offset) queryParams.set('offset', params.offset.toString())\r\n if (params?.includeDeleted) queryParams.set('includeDeleted', params.includeDeleted.toString())\r\n\r\n const query = queryParams.toString()\r\n const path = `/entities${query ? `?${query}` : ''}`\r\n\r\n return this.get<EntitiesResponse>(path)\r\n }\r\n\r\n /**\r\n * Internal method to fetch multiple entity types in one request.\r\n * Used by the EntityBatcher.\r\n */\r\n private async getEntitiesMultiple(\r\n householdId: string | null | undefined,\r\n entityTypes: string[],\r\n includeDeleted: boolean\r\n ): Promise<EntitiesResponse> {\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n if (entityTypes.length > 0) queryParams.set('entityTypes', entityTypes.join(','))\r\n if (includeDeleted) queryParams.set('includeDeleted', 'true')\r\n\r\n const query = queryParams.toString()\r\n const path = `/entities${query ? `?${query}` : ''}`\r\n\r\n return this.get<EntitiesResponse>(path)\r\n }\r\n\r\n async getEntity(householdId: string | null | undefined, entityId: string): Promise<EncryptedEntityResponse> {\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n const query = queryParams.toString()\r\n const path = `/entities/${entityId}${query ? `?${query}` : ''}`\r\n\r\n return this.get<EncryptedEntityResponse>(path)\r\n }\r\n\r\n async createEntity(householdId: string | null | undefined, data: CreateEntityRequest): Promise<EncryptedEntityResponse> {\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n const query = queryParams.toString()\r\n const path = `/entities${query ? `?${query}` : ''}`\r\n\r\n return this.post<EncryptedEntityResponse>(path, data)\r\n }\r\n\r\n async updateEntity(\r\n householdId: string | null | undefined,\r\n entityId: string,\r\n data: UpdateEntityRequest,\r\n version: number\r\n ): Promise<EncryptedEntityResponse> {\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n const query = queryParams.toString()\r\n const path = `/entities/${entityId}${query ? `?${query}` : ''}`\r\n\r\n return this.put<EncryptedEntityResponse>(path, data, {\r\n headers: { 'If-Match': `\"${version}\"` }\r\n })\r\n }\r\n\r\n async deleteEntity(\r\n householdId: string | null | undefined,\r\n entityId: string,\r\n version: number\r\n ): Promise<void> {\r\n const queryParams = new URLSearchParams()\r\n if (householdId) queryParams.set('householdId', householdId)\r\n const query = queryParams.toString()\r\n const path = `/entities/${entityId}${query ? `?${query}` : ''}`\r\n await this.delete(path, {\r\n headers: { 'If-Match': `\"${version}\"` }\r\n })\r\n }\r\n\r\n // ===== HOUSEHOLD MEMBERS METHODS =====\r\n\r\n async getHouseholdMembers(householdId: string): Promise<HouseholdMember[]> {\r\n return this.get<HouseholdMember[]>(`/households/${householdId}/members`)\r\n }\r\n\r\n async inviteHouseholdMember(\r\n householdId: string,\r\n email: string,\r\n role: HouseholdMemberRole = HOUSEHOLD_MEMBER_ROLES.MEMBER\r\n ): Promise<HouseholdMember> {\r\n return this.post<HouseholdMember>(`/households/${householdId}/members/invite`, { email, role })\r\n }\r\n\r\n async updateMemberRole(householdId: string, userId: string, data: {\r\n role: HouseholdMemberRole\r\n keysToGrant?: Array<{\r\n keyType: string\r\n encryptedKey: string\r\n userKeyBundleId: string\r\n }>\r\n keysToRevoke?: string[]\r\n }): Promise<void> {\r\n await this.put(`/households/${householdId}/members/${userId}`, data)\r\n }\r\n\r\n async removeHouseholdMember(householdId: string, userId: string): Promise<void> {\r\n await this.delete(`/households/${householdId}/members/${userId}`)\r\n }\r\n\r\n async getHouseholdPeople(householdId: string): Promise<{ people: HouseholdMember[] }> {\r\n const members = await this.getHouseholdMembers(householdId)\r\n return { people: members }\r\n }\r\n\r\n // DEPRECATED: Legacy invitation methods - will be removed\r\n async createHouseholdInvitation(\r\n householdId: string,\r\n role: HouseholdMemberRole = HOUSEHOLD_MEMBER_ROLES.MEMBER,\r\n residentId?: string,\r\n expiresIn?: number,\r\n oneTimeUse?: boolean\r\n ): Promise<unknown> {\r\n return this.post(`/households/${householdId}/invitations`, {\r\n role,\r\n residentId,\r\n expiresIn,\r\n oneTimeUse\r\n })\r\n }\r\n\r\n async revokeInvitation(invitationToken: string): Promise<void> {\r\n await this.delete(`/households/invitations/${invitationToken}`)\r\n }\r\n\r\n // ===== CONTACTS =====\r\n\r\n async getContacts(householdId: string): Promise<Contact[]> {\r\n return this.get<Contact[]>(`/contacts?householdId=${householdId}`)\r\n }\r\n\r\n async createContact(householdId: string, data: CreateContactRequest): Promise<Contact> {\r\n return this.post<Contact>('/contacts', { ...data, household_id: householdId })\r\n }\r\n\r\n async updateContact(id: string, data: Partial<CreateContactRequest>): Promise<Contact> {\r\n return this.put<Contact>(`/contacts/${id}`, data)\r\n }\r\n\r\n async deleteContact(id: string): Promise<void> {\r\n await this.delete<void>(`/contacts/${id}`)\r\n }\r\n\r\n // ===== INSURANCE POLICIES =====\r\n\r\n async getInsurancePolicies(householdId: string): Promise<InsurancePolicy[]> {\r\n return this.get<InsurancePolicy[]>(`/insurance-policies?householdId=${householdId}`)\r\n }\r\n\r\n async createInsurancePolicy(householdId: string, data: {\r\n provider: string\r\n policy_number: string\r\n effective_date: string\r\n expiration_date: string\r\n property_id?: string\r\n vehicle_id?: string\r\n pet_id?: string\r\n }): Promise<InsurancePolicy> {\r\n return this.post<InsurancePolicy>('/insurance-policies', { ...data, household_id: householdId })\r\n }\r\n\r\n async updateInsurancePolicy(id: string, data: {\r\n provider: string\r\n policy_number: string\r\n effective_date: string\r\n expiration_date: string\r\n property_id?: string\r\n vehicle_id?: string\r\n pet_id?: string\r\n type: InsurancePolicy['type']\r\n }, householdId: string): Promise<InsurancePolicy> {\r\n return this.put<InsurancePolicy>(`/insurance-policies/${id}`, { ...data, household_id: householdId })\r\n }\r\n\r\n async deleteInsurancePolicy(id: string): Promise<void> {\r\n await this.delete(`/insurance-policies/${id}`)\r\n }\r\n\r\n // ===== GENERIC RESOURCE METHODS =====\r\n // These are used by entity contexts and can be extended\r\n\r\n async getResources<T>(path: string, householdId: string): Promise<T[]> {\r\n return this.get<T[]>(`${path}?householdId=${householdId}`)\r\n }\r\n\r\n async createResource<T>(path: string, householdId: string, data: object): Promise<T> {\r\n return this.post<T>(path, { ...data, household_id: householdId })\r\n }\r\n\r\n async updateResource<T>(path: string, id: string, data: object): Promise<T> {\r\n return this.put<T>(`${path}/${id}`, data)\r\n }\r\n\r\n async deleteResource(path: string, id: string): Promise<void> {\r\n await this.delete(`${path}/${id}`)\r\n }\r\n\r\n // ===== USER KEY METHODS =====\r\n\r\n async batchAddKeys(data: BatchAddKeysRequest): Promise<BatchAddKeysResponse> {\r\n return this.post<BatchAddKeysResponse>('/users/batch-add-keys', data)\r\n }\r\n\r\n /**\r\n * Get the current user's key bundle (public key, encrypted private key, etc.)\r\n * This is deduplicated automatically - concurrent calls return the same promise.\r\n */\r\n async getCurrentKeyBundle(): Promise<UserKeyBundle> {\r\n return this.get<UserKeyBundle>('/webauthn/key-bundles/current')\r\n }\r\n\r\n // ===== BILLING METHODS =====\r\n\r\n /**\r\n * Create a Stripe Checkout session to start a subscription.\r\n * Only household owners can call this.\r\n */\r\n async createCheckoutSession(householdId: string, planCode: string): Promise<CheckoutSessionResponse> {\r\n return this.post<CheckoutSessionResponse>(`/billing/${householdId}/checkout`, { planCode })\r\n }\r\n\r\n /**\r\n * Create a Stripe Customer Portal session for managing subscription.\r\n * Only household owners can call this.\r\n */\r\n async createPortalSession(householdId: string): Promise<CustomerPortalSessionResponse> {\r\n return this.post<CustomerPortalSessionResponse>(`/billing/${householdId}/portal`, {})\r\n }\r\n\r\n /**\r\n * Get available subscription plans.\r\n * This endpoint is public (no auth required).\r\n */\r\n async getAvailablePlans(): Promise<AvailablePlan[]> {\r\n return this.get<AvailablePlan[]>('/billing/plans')\r\n }\r\n\r\n /**\r\n * Start a free trial for a household.\r\n * No credit card required.\r\n */\r\n async startTrial(householdId: string, tier?: 'household' | 'estate'): Promise<StartTrialResponse> {\r\n return this.post<StartTrialResponse>(`/billing/${householdId}/start-trial`, { tier })\r\n }\r\n\r\n /**\r\n * Upgrade an existing subscription to a new plan.\r\n * This updates the subscription in place instead of creating a new one.\r\n */\r\n async upgradeSubscription(householdId: string, planCode: string): Promise<UpgradeSubscriptionResponse> {\r\n return this.post<UpgradeSubscriptionResponse>(`/billing/${householdId}/upgrade`, { planCode })\r\n }\r\n\r\n /**\r\n * Get billing details for subscription sync.\r\n * Used by frontend to sync EstateHelm subscription entity.\r\n */\r\n async getBillingDetails(householdId: string): Promise<BillingDetailsResponse> {\r\n return this.get<BillingDetailsResponse>(`/billing/${householdId}/details`)\r\n }\r\n\r\n // ===== MOBILE BILLING METHODS (Apple IAP / Google Play) =====\r\n\r\n /**\r\n * Verify an Apple In-App Purchase transaction.\r\n * Called by iOS app after a successful StoreKit purchase.\r\n */\r\n async verifyApplePurchase(householdId: string, data: {\r\n transactionId?: string\r\n signedTransaction?: string\r\n }): Promise<MobilePurchaseVerificationResponse> {\r\n return this.post<MobilePurchaseVerificationResponse>(`/apple-billing/${householdId}/verify-transaction`, data)\r\n }\r\n\r\n /**\r\n * Sync Apple subscription status with the server.\r\n */\r\n async syncAppleSubscription(householdId: string): Promise<MobileSubscriptionSyncResponse> {\r\n return this.post<MobileSubscriptionSyncResponse>(`/apple-billing/${householdId}/sync`, {})\r\n }\r\n\r\n /**\r\n * Get Apple subscription status for a household.\r\n */\r\n async getAppleSubscriptionStatus(householdId: string): Promise<MobileSubscriptionStatusResponse> {\r\n return this.get<MobileSubscriptionStatusResponse>(`/apple-billing/${householdId}/status`)\r\n }\r\n\r\n /**\r\n * Get available Apple products (for StoreKit).\r\n */\r\n async getAppleProducts(): Promise<MobileProductInfo[]> {\r\n return this.get<MobileProductInfo[]>('/apple-billing/products')\r\n }\r\n\r\n /**\r\n * Verify a Google Play purchase.\r\n * Called by Android app after a successful Google Play Billing purchase.\r\n */\r\n async verifyGooglePurchase(householdId: string, data: {\r\n productId: string\r\n purchaseToken: string\r\n }): Promise<MobilePurchaseVerificationResponse> {\r\n return this.post<MobilePurchaseVerificationResponse>(`/google-billing/${householdId}/verify-purchase`, data)\r\n }\r\n\r\n /**\r\n * Sync Google Play subscription status with the server.\r\n */\r\n async syncGoogleSubscription(householdId: string): Promise<MobileSubscriptionSyncResponse> {\r\n return this.post<MobileSubscriptionSyncResponse>(`/google-billing/${householdId}/sync`, {})\r\n }\r\n\r\n /**\r\n * Get Google Play subscription status for a household.\r\n */\r\n async getGoogleSubscriptionStatus(householdId: string): Promise<MobileSubscriptionStatusResponse> {\r\n return this.get<MobileSubscriptionStatusResponse>(`/google-billing/${householdId}/status`)\r\n }\r\n\r\n /**\r\n * Get available Google Play products.\r\n */\r\n async getGoogleProducts(): Promise<MobileProductInfo[]> {\r\n return this.get<MobileProductInfo[]>('/google-billing/products')\r\n }\r\n}\r\n\r\n// Mobile billing response types\r\n\r\nexport interface MobilePurchaseVerificationResponse {\r\n success: boolean\r\n planCode?: string\r\n tier?: string\r\n expiresAt?: string\r\n originalTransactionId?: string // Apple\r\n orderId?: string // Google\r\n acknowledged?: boolean // Google\r\n}\r\n\r\nexport interface MobileSubscriptionSyncResponse {\r\n success: boolean\r\n status?: string\r\n message?: string\r\n expiresAt?: string\r\n autoRenewEnabled?: boolean\r\n productId?: string\r\n}\r\n\r\nexport interface PurchaserInfo {\r\n id: string\r\n email?: string\r\n name?: string\r\n}\r\n\r\nexport interface MobileSubscriptionStatusResponse {\r\n paymentProvider: 'none' | 'stripe' | 'apple' | 'google'\r\n subscriptionStatus?: string\r\n planCode?: string\r\n trialEndsAt?: string\r\n currentPeriodEndsAt?: string\r\n appleProductId?: string\r\n appleAutoRenewEnabled?: boolean\r\n hasAppleSubscription?: boolean\r\n googleProductId?: string\r\n googleAutoRenewEnabled?: boolean\r\n hasGoogleSubscription?: boolean\r\n purchaserUserId?: string\r\n purchaserInfo?: PurchaserInfo\r\n isCurrentUserPurchaser?: boolean\r\n}\r\n\r\nexport interface MobileProductInfo {\r\n productId: string\r\n planCode: string\r\n tier: string\r\n billingCycle: 'monthly' | 'annual'\r\n}\r\n","/**\r\n * Utility functions for encryption operations\r\n * \r\n * This module provides helper functions for encoding/decoding and crypto operations.\r\n * \r\n * @module encryption/utils\r\n */\r\n\r\n/**\r\n * Converts a Uint8Array to a base64 string\r\n * \r\n * @param bytes - The byte array to encode\r\n * @returns Base64-encoded string\r\n * \r\n * @example\r\n * ```ts\r\n * const bytes = new Uint8Array([72, 101, 108, 108, 111]);\r\n * const base64 = base64Encode(bytes);\r\n * console.log(base64); // \"SGVsbG8=\"\r\n * ```\r\n */\r\nexport function base64Encode(bytes: Uint8Array): string {\r\n const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join('');\r\n return btoa(binString);\r\n}\r\n\r\n/**\r\n * Converts a base64 string to a Uint8Array\r\n * \r\n * @param base64 - The base64 string to decode\r\n * @returns Decoded byte array\r\n * @throws {Error} If the base64 string is invalid\r\n * \r\n * @example\r\n * ```ts\r\n * const base64 = \"SGVsbG8=\";\r\n * const bytes = base64Decode(base64);\r\n * console.log(bytes); // Uint8Array [72, 101, 108, 108, 111]\r\n * ```\r\n */\r\nexport function base64Decode(base64: string): Uint8Array {\r\n try {\r\n // Strip any whitespace/newlines that PostgreSQL might add\r\n const cleanBase64 = base64.replace(/\\s/g, '');\r\n const binString = atob(cleanBase64);\r\n return Uint8Array.from(binString, (char) => char.codePointAt(0)!);\r\n } catch (error) {\r\n throw new Error(`Failed to decode base64: ${error instanceof Error ? error.message : 'Unknown error'}`);\r\n }\r\n}\r\n\r\n/**\r\n * Converts a Uint8Array to a base64url string (URL-safe, no padding)\r\n *\r\n * Base64url uses '-' instead of '+', '_' instead of '/', and no '=' padding.\r\n * This is the format required by WebAuthn.\r\n *\r\n * @param bytes - The byte array to encode\r\n * @returns Base64url-encoded string\r\n */\r\nexport function base64UrlEncode(bytes: Uint8Array): string {\r\n const base64 = base64Encode(bytes);\r\n return base64\r\n .replace(/\\+/g, '-')\r\n .replace(/\\//g, '_')\r\n .replace(/=/g, '');\r\n}\r\n\r\n/**\r\n * Converts a base64url string to a Uint8Array\r\n *\r\n * Handles both base64url (no padding) and standard base64 formats.\r\n *\r\n * @param base64url - The base64url string to decode\r\n * @returns Decoded byte array\r\n */\r\nexport function base64UrlDecode(base64url: string): Uint8Array {\r\n // Convert base64url to standard base64\r\n let base64 = base64url\r\n .replace(/-/g, '+')\r\n .replace(/_/g, '/');\r\n\r\n // Add padding if needed\r\n const pad = base64.length % 4;\r\n if (pad) {\r\n base64 += '='.repeat(4 - pad);\r\n }\r\n\r\n return base64Decode(base64);\r\n}\r\n\r\n/**\r\n * Generates cryptographically secure random bytes\r\n * \r\n * @param length - Number of random bytes to generate\r\n * @returns Uint8Array of random bytes\r\n * \r\n * @example\r\n * ```ts\r\n * const salt = generateRandomBytes(32); // 32-byte salt\r\n * const key = generateRandomBytes(32); // 256-bit key\r\n * ```\r\n */\r\nexport function generateRandomBytes(length: number): Uint8Array {\r\n return crypto.getRandomValues(new Uint8Array(length));\r\n}\r\n\r\n/**\r\n * Converts a string to a Uint8Array using UTF-8 encoding\r\n * \r\n * @param str - The string to encode\r\n * @returns UTF-8 encoded byte array\r\n * \r\n * @example\r\n * ```ts\r\n * const bytes = stringToBytes(\"Hello, 世界\");\r\n * ```\r\n */\r\nexport function stringToBytes(str: string): Uint8Array {\r\n return new TextEncoder().encode(str);\r\n}\r\n\r\n/**\r\n * Converts a Uint8Array to a string using UTF-8 decoding\r\n * \r\n * @param bytes - The byte array to decode\r\n * @returns Decoded string\r\n * \r\n * @example\r\n * ```ts\r\n * const str = bytesToString(new Uint8Array([72, 101, 108, 108, 111]));\r\n * console.log(str); // \"Hello\"\r\n * ```\r\n */\r\nexport function bytesToString(bytes: Uint8Array): string {\r\n return new TextDecoder().decode(bytes);\r\n}\r\n\r\n/**\r\n * Validates that a passphrase meets minimum security requirements\r\n * \r\n * @param passphrase - The passphrase to validate\r\n * @param minLength - Minimum required length (default: 16)\r\n * @throws {Error} If passphrase doesn't meet requirements\r\n * \r\n * @example\r\n * ```ts\r\n * validatePassphrase(\"short\"); // throws Error\r\n * validatePassphrase(\"this-is-a-secure-passphrase\"); // ok\r\n * ```\r\n */\r\nexport function validatePassphrase(passphrase: string, minLength: number = 16): void {\r\n if (!passphrase || passphrase.length < minLength) {\r\n throw new Error(`Passphrase must be at least ${minLength} characters long`);\r\n }\r\n \r\n // Optional: Add additional requirements (uppercase, lowercase, numbers, etc.)\r\n // For now, we just check length since the architecture doc only requires 16+ chars\r\n}\r\n\r\n/**\r\n * Securely compares two byte arrays in constant time to prevent timing attacks\r\n * \r\n * @param a - First byte array\r\n * @param b - Second byte array\r\n * @returns true if arrays are equal, false otherwise\r\n * \r\n * @example\r\n * ```ts\r\n * const key1 = new Uint8Array([1, 2, 3]);\r\n * const key2 = new Uint8Array([1, 2, 3]);\r\n * console.log(constantTimeEqual(key1, key2)); // true\r\n * ```\r\n */\r\nexport function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {\r\n if (a.length !== b.length) {\r\n return false;\r\n }\r\n \r\n let result = 0;\r\n for (let i = 0; i < a.length; i++) {\r\n result |= a[i] ^ b[i];\r\n }\r\n \r\n return result === 0;\r\n}\r\n\r\n/**\r\n * Generates a cryptographically secure UUID v4\r\n * \r\n * @returns UUID string in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\r\n * \r\n * @example\r\n * ```ts\r\n * const id = generateUUID();\r\n * console.log(id); // \"550e8400-e29b-41d4-a716-446655440000\"\r\n * ```\r\n */\r\nexport function generateUUID(): string {\r\n return crypto.randomUUID();\r\n}\r\n\r\n/**\r\n * Calibrates PBKDF2 iterations to target duration\r\n * \r\n * This function tests different iteration counts to find the value\r\n * that results in approximately the target duration (default: 500ms).\r\n * \r\n * @param targetMs - Target duration in milliseconds (default: 500)\r\n * @param testSalt - Optional salt for testing (generates random if not provided)\r\n * @returns Recommended iteration count\r\n * \r\n * @example\r\n * ```ts\r\n * const iterations = await calibratePBKDF2Iterations(500);\r\n * console.log(`Use ${iterations} iterations for ~500ms`);\r\n * ```\r\n */\r\nexport async function calibratePBKDF2Iterations(\r\n targetMs: number = 500,\r\n testSalt?: Uint8Array\r\n): Promise<number> {\r\n const salt = testSalt || generateRandomBytes(32);\r\n const testPassphrase = 'test-passphrase-for-calibration';\r\n \r\n // Start with a baseline\r\n let iterations = 100000;\r\n \r\n const testDerivation = async (iters: number): Promise<number> => {\r\n const start = performance.now();\r\n \r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n stringToBytes(testPassphrase) as BufferSource,\r\n 'PBKDF2',\r\n false,\r\n ['deriveBits']\r\n );\r\n \r\n await crypto.subtle.deriveBits(\r\n {\r\n name: 'PBKDF2',\r\n salt: salt as BufferSource,\r\n iterations: iters,\r\n hash: 'SHA-256'\r\n },\r\n keyMaterial,\r\n 256\r\n );\r\n \r\n return performance.now() - start;\r\n };\r\n \r\n // Test initial iterations\r\n const duration = await testDerivation(iterations);\r\n \r\n // Adjust based on result\r\n if (duration < targetMs) {\r\n // Too fast, increase iterations\r\n iterations = Math.floor(iterations * (targetMs / duration));\r\n } else if (duration > targetMs * 1.5) {\r\n // Too slow, decrease iterations\r\n iterations = Math.floor(iterations * (targetMs / duration));\r\n }\r\n \r\n // Round to nearest 10000 for cleaner numbers\r\n return Math.max(100000, Math.round(iterations / 10000) * 10000);\r\n}\r\n","/**\r\n * Household Keys Module\r\n * \r\n * This module handles the generation and encryption of household keys.\r\n * Each household has three keys that encrypt different categories of data:\r\n * \r\n * - General: Properties, Pets, Vehicles, Access Codes (WiFi, door, gate)\r\n * - Financial: Bank Accounts, Tax Docs, Estate Planning\r\n * - Security: Safe Combinations, Alarm Master Codes, Password Manager\r\n * \r\n * Household keys are encrypted with the user's master key and stored locally.\r\n * \r\n * @module encryption/householdKeys\r\n */\r\n\r\nimport { base64Encode, base64Decode, generateRandomBytes } from './utils';\r\nimport type { HouseholdKeyType, EncryptedHouseholdKey } from './types';\r\n\r\n/**\r\n * Household key size in bytes (256-bit AES)\r\n */\r\nexport const HOUSEHOLD_KEY_SIZE = 32;\r\n\r\n/**\r\n * IV size for AES-GCM (96 bits / 12 bytes)\r\n */\r\nexport const IV_SIZE = 12;\r\n\r\n/**\r\n * Generated household keys (unencrypted)\r\n * Map of key type to raw key bytes\r\n */\r\nexport type GeneratedHouseholdKeys = Map<HouseholdKeyType, Uint8Array>;\r\n\r\n/**\r\n * Generates household keys for the specified key types\r\n * \r\n * These keys are generated using cryptographically secure random bytes.\r\n * Each key is 256 bits (32 bytes) for AES-256-GCM encryption.\r\n * \r\n * @param keyTypes - Array of key type identifiers (e.g., ['general', 'financial', 'security'])\r\n * @returns Map of key types to raw key bytes\r\n * \r\n * @example\r\n * ```ts\r\n * // Generate keys when creating a new household\r\n * const keyTypes = ['general', 'financial', 'security'];\r\n * const householdKeys = generateHouseholdKeys(keyTypes);\r\n * \r\n * // Encrypt them with the user's master key\r\n * const encrypted = await encryptAllHouseholdKeys(householdKeys, masterKey);\r\n * \r\n * // Store encrypted keys locally\r\n * await storeEncryptedKeys(householdId, encrypted);\r\n * ```\r\n */\r\nexport function generateHouseholdKeys(keyTypes: HouseholdKeyType[]): GeneratedHouseholdKeys {\r\n const keys = new Map<HouseholdKeyType, Uint8Array>();\r\n \r\n for (const keyType of keyTypes) {\r\n keys.set(keyType, generateRandomBytes(HOUSEHOLD_KEY_SIZE));\r\n }\r\n \r\n return keys;\r\n}\r\n\r\n/**\r\n * Encrypts a single household key with the user's master key\r\n * \r\n * Uses AES-256-GCM for authenticated encryption.\r\n * Each encryption generates a new random IV.\r\n * \r\n * @param householdKey - Raw household key bytes to encrypt\r\n * @param masterKey - User's master key (CryptoKey)\r\n * @param keyType - Type of household key being encrypted\r\n * @param version - Version number for key rotation (default: 1)\r\n * @returns Encrypted household key with metadata\r\n * @throws {Error} If encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * const generalKey = generateRandomBytes(32);\r\n * const encrypted = await encryptHouseholdKey(\r\n * generalKey,\r\n * masterKey,\r\n * 'general',\r\n * 1\r\n * );\r\n * ```\r\n */\r\nexport async function encryptHouseholdKey(\r\n householdKey: Uint8Array,\r\n masterKey: CryptoKey,\r\n keyType: HouseholdKeyType,\r\n version: number = 1\r\n): Promise<EncryptedHouseholdKey> {\r\n if (householdKey.length !== HOUSEHOLD_KEY_SIZE) {\r\n throw new Error(`Invalid household key size: expected ${HOUSEHOLD_KEY_SIZE} bytes, got ${householdKey.length}`);\r\n }\r\n \r\n try {\r\n // Generate random IV\r\n const iv = generateRandomBytes(IV_SIZE);\r\n \r\n // Encrypt household key with master key\r\n const ciphertext = await crypto.subtle.encrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n },\r\n masterKey,\r\n householdKey as BufferSource\r\n );\r\n \r\n // Pack into format: [1-byte version][12-byte IV][ciphertext+auth tag]\r\n const ciphertextArray = new Uint8Array(ciphertext);\r\n const packed = new Uint8Array(1 + IV_SIZE + ciphertextArray.length);\r\n packed[0] = version;\r\n packed.set(iv, 1);\r\n packed.set(ciphertextArray, 1 + IV_SIZE);\r\n \r\n return {\r\n keyType,\r\n encryptedKey: base64Encode(packed),\r\n iv: base64Encode(iv), // Keep for backwards compatibility\r\n version\r\n };\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to encrypt ${keyType} household key: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Encrypts all household keys with the user's master key\r\n * \r\n * @param householdKeys - Generated household keys\r\n * @param masterKey - User's master key (CryptoKey)\r\n * @param version - Version number for key rotation (default: 1)\r\n * @returns Array of encrypted household keys\r\n * @throws {Error} If any encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * const keys = generateHouseholdKeys();\r\n * const encrypted = await encryptAllHouseholdKeys(keys, masterKey);\r\n * \r\n * // Store in local database\r\n * for (const encKey of encrypted) {\r\n * await localDB.insert('household_keys', {\r\n * household_id: householdId,\r\n * key_type: encKey.keyType,\r\n * encrypted_key: encKey.encryptedKey,\r\n * iv: encKey.iv,\r\n * version: encKey.version\r\n * });\r\n * }\r\n * ```\r\n */\r\nexport async function encryptAllHouseholdKeys(\r\n householdKeys: GeneratedHouseholdKeys,\r\n masterKey: CryptoKey,\r\n version: number = 1\r\n): Promise<EncryptedHouseholdKey[]> {\r\n const encrypted: EncryptedHouseholdKey[] = [];\r\n \r\n for (const [keyType, keyBytes] of householdKeys) {\r\n const encryptedKey = await encryptHouseholdKey(\r\n keyBytes,\r\n masterKey,\r\n keyType,\r\n version\r\n );\r\n encrypted.push(encryptedKey);\r\n }\r\n \r\n return encrypted;\r\n}\r\n\r\n/**\r\n * Decrypts a household key using the user's master key\r\n * \r\n * @param encryptedKey - Encrypted household key from storage\r\n * @param masterKey - User's master key (CryptoKey)\r\n * @returns Decrypted household key bytes\r\n * @throws {Error} If decryption fails (wrong key, corrupted data, etc.)\r\n * \r\n * @example\r\n * ```ts\r\n * // Retrieve encrypted key from local database\r\n * const encKey = await localDB.get('household_keys', {\r\n * household_id: householdId,\r\n * key_type: 'general'\r\n * });\r\n * \r\n * // Decrypt it\r\n * const householdKey = await decryptHouseholdKey(encKey, masterKey);\r\n * \r\n * // Use for entity encryption/decryption\r\n * const entityKey = await deriveEntityKey(householdKey, entityId, entityType);\r\n * ```\r\n */\r\nexport async function decryptHouseholdKey(\r\n encryptedKey: EncryptedHouseholdKey,\r\n masterKey: CryptoKey\r\n): Promise<Uint8Array> {\r\n try {\r\n const encryptedData = base64Decode(encryptedKey.encryptedKey);\r\n \r\n // Unpack format: [1-byte version][12-byte IV][ciphertext+auth tag]\r\n if (encryptedData.length < 1 + IV_SIZE) {\r\n throw new Error('Invalid encrypted key format: too short');\r\n }\r\n \r\n const version = encryptedData[0];\r\n const iv = encryptedData.slice(1, 1 + IV_SIZE);\r\n const ciphertext = encryptedData.slice(1 + IV_SIZE);\r\n \r\n if (version !== 1) {\r\n throw new Error(`Unsupported crypto version: ${version}`);\r\n }\r\n \r\n // Decrypt\r\n const plaintext = await crypto.subtle.decrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n },\r\n masterKey,\r\n ciphertext as BufferSource\r\n );\r\n \r\n return new Uint8Array(plaintext);\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to decrypt ${encryptedKey.keyType} household key: ${error instanceof Error ? error.message : 'Unknown error'}. This may indicate a wrong master key or corrupted data.`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Re-encrypts a household key with a new master key\r\n * \r\n * This is used when:\r\n * - User changes their passphrase\r\n * - User accepts an invitation and needs to re-encrypt with their own master key\r\n * - Migrating between security levels\r\n * \r\n * @param encryptedKey - Currently encrypted household key\r\n * @param oldMasterKey - Current master key\r\n * @param newMasterKey - New master key to encrypt with\r\n * @param newVersion - Optional new version number\r\n * @returns Re-encrypted household key\r\n * @throws {Error} If decryption or encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * // User changes passphrase\r\n * const oldMasterKey = await importMasterKey(oldKeyBytes);\r\n * const newMasterKey = await importMasterKey(newKeyBytes);\r\n * \r\n * // Re-encrypt all household keys\r\n * for (const encKey of encryptedKeys) {\r\n * const reEncrypted = await reEncryptHouseholdKey(\r\n * encKey,\r\n * oldMasterKey,\r\n * newMasterKey\r\n * );\r\n * \r\n * await localDB.update('household_keys', reEncrypted, {\r\n * household_id: householdId,\r\n * key_type: encKey.keyType\r\n * });\r\n * }\r\n * ```\r\n */\r\nexport async function reEncryptHouseholdKey(\r\n encryptedKey: EncryptedHouseholdKey,\r\n oldMasterKey: CryptoKey,\r\n newMasterKey: CryptoKey,\r\n newVersion?: number\r\n): Promise<EncryptedHouseholdKey> {\r\n // Decrypt with old master key\r\n const householdKey = await decryptHouseholdKey(encryptedKey, oldMasterKey);\r\n \r\n // Re-encrypt with new master key\r\n return await encryptHouseholdKey(\r\n householdKey,\r\n newMasterKey,\r\n encryptedKey.keyType,\r\n newVersion ?? encryptedKey.version\r\n );\r\n}\r\n\r\n/**\r\n * Imports a household key for use in entity encryption/decryption\r\n * \r\n * @param keyBytes - Raw household key bytes\r\n * @returns CryptoKey ready for HKDF key derivation\r\n * @throws {Error} If import fails\r\n * \r\n * @example\r\n * ```ts\r\n * const keyBytes = await decryptHouseholdKey(encryptedKey, masterKey);\r\n * const householdKey = await importHouseholdKeyForDerivation(keyBytes);\r\n * \r\n * // Now use for HKDF\r\n * const derivedBits = await crypto.subtle.deriveBits(\r\n * { name: 'HKDF', ... },\r\n * householdKey,\r\n * 256\r\n * );\r\n * ```\r\n */\r\nexport async function importHouseholdKeyForDerivation(\r\n keyBytes: Uint8Array\r\n): Promise<CryptoKey> {\r\n if (keyBytes.length !== HOUSEHOLD_KEY_SIZE) {\r\n throw new Error(`Invalid household key size: expected ${HOUSEHOLD_KEY_SIZE} bytes, got ${keyBytes.length}`);\r\n }\r\n \r\n try {\r\n return await crypto.subtle.importKey(\r\n 'raw',\r\n keyBytes as BufferSource,\r\n 'HKDF',\r\n false,\r\n ['deriveBits']\r\n );\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to import household key for derivation: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n\r\n","/**\r\n * Entity Key Derivation Module\r\n * \r\n * This module handles the deterministic derivation of entity-specific encryption keys\r\n * from household keys using HKDF (HMAC-based Key Derivation Function).\r\n * \r\n * Key benefits of deterministic per-entity keys:\r\n * - Enables granular sharing (share one property without sharing household key)\r\n * - No storage overhead (keys derived on-demand from household key + metadata)\r\n * - Simpler than random per-entity keys (no encrypted_entity_key storage)\r\n * - Service can help with sharing (owner shares derived key for specific entity)\r\n * \r\n * Key derivation:\r\n * Entity Key = HKDF(household_key, info: entity_type + \":\" + entity_id)\r\n * \r\n * @module encryption/entityKeys\r\n */\r\n\r\nimport { stringToBytes } from './utils';\r\nimport { importHouseholdKeyForDerivation } from './householdKeys';\r\nimport type { EntityType } from './types';\r\n\r\n/**\r\n * Entity key size in bytes (256-bit AES)\r\n */\r\nexport const ENTITY_KEY_SIZE = 32;\r\n\r\n/**\r\n * Derives a deterministic entity-specific encryption key from a household key\r\n * \r\n * This function uses HKDF (HMAC-based Key Derivation Function) to derive a unique\r\n * encryption key for each entity. The same household key + entity metadata will\r\n * always produce the same entity key, enabling:\r\n * \r\n * 1. Deterministic re-derivation (no need to store entity keys)\r\n * 2. Granular sharing (share derived key for one entity without sharing household key)\r\n * 3. No storage overhead (derive on-demand)\r\n * \r\n * The derived key is unique per entity because the info parameter includes both\r\n * the entity type and entity ID, ensuring different entities get different keys\r\n * even if they share the same household key.\r\n * \r\n * @param householdKey - Raw household key bytes (32 bytes)\r\n * @param entityId - Unique identifier for the entity (UUID)\r\n * @param entityType - Type of entity (property, pet, etc.)\r\n * @returns Derived entity key bytes (32 bytes)\r\n * @throws {Error} If derivation fails or inputs are invalid\r\n * \r\n * @example\r\n * ```ts\r\n * // Encrypt a property\r\n * const householdKey = await decryptHouseholdKey(encryptedKey, masterKey);\r\n * const propertyKey = await deriveEntityKey(\r\n * householdKey,\r\n * 'property-uuid-123',\r\n * 'property'\r\n * );\r\n * \r\n * // Later: decrypt the same property (same inputs = same key)\r\n * const sameKey = await deriveEntityKey(\r\n * householdKey,\r\n * 'property-uuid-123',\r\n * 'property'\r\n * );\r\n * // propertyKey === sameKey ✓\r\n * ```\r\n */\r\nexport async function deriveEntityKey(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType\r\n): Promise<Uint8Array> {\r\n if (!entityId || entityId.trim().length === 0) {\r\n throw new Error('Entity ID cannot be empty');\r\n }\r\n\r\n if (!entityType || entityType.trim().length === 0) {\r\n throw new Error('Entity type cannot be empty');\r\n }\r\n\r\n const infoString = `${entityType}:${entityId}`;\r\n\r\n try {\r\n // Import household key for HKDF\r\n const keyMaterial = await importHouseholdKeyForDerivation(householdKey);\r\n\r\n // Create info parameter: \"entity_type:entity_id\"\r\n // This ensures each entity gets a unique derived key\r\n const info = stringToBytes(infoString);\r\n \r\n // Derive entity key using HKDF\r\n // - No salt (empty salt) because we want deterministic derivation\r\n // - Info parameter provides the uniqueness\r\n const derivedBits = await crypto.subtle.deriveBits(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: new Uint8Array(0) as BufferSource, // Empty salt for deterministic derivation\r\n info: info as BufferSource\r\n },\r\n keyMaterial,\r\n ENTITY_KEY_SIZE * 8 // 256 bits\r\n );\r\n \r\n return new Uint8Array(derivedBits);\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to derive entity key for ${entityType}:${entityId}: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Imports a derived entity key for use in AES-GCM encryption/decryption\r\n * \r\n * @param entityKeyBytes - Derived entity key bytes (32 bytes)\r\n * @returns CryptoKey ready for AES-GCM operations\r\n * @throws {Error} If import fails or key size is invalid\r\n * \r\n * @example\r\n * ```ts\r\n * const entityKeyBytes = await deriveEntityKey(householdKey, entityId, entityType);\r\n * const entityKey = await importEntityKey(entityKeyBytes);\r\n * \r\n * // Use for encryption\r\n * const encrypted = await crypto.subtle.encrypt(\r\n * { name: 'AES-GCM', iv },\r\n * entityKey,\r\n * dataBytes\r\n * );\r\n * ```\r\n */\r\nexport async function importEntityKey(entityKeyBytes: Uint8Array): Promise<CryptoKey> {\r\n if (entityKeyBytes.length !== ENTITY_KEY_SIZE) {\r\n throw new Error(`Invalid entity key size: expected ${ENTITY_KEY_SIZE} bytes, got ${entityKeyBytes.length}`);\r\n }\r\n \r\n try {\r\n return await crypto.subtle.importKey(\r\n 'raw',\r\n entityKeyBytes as BufferSource,\r\n {\r\n name: 'AES-GCM',\r\n length: ENTITY_KEY_SIZE * 8\r\n },\r\n false, // Not extractable (security best practice)\r\n ['encrypt', 'decrypt']\r\n );\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to import entity key: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Derives and imports an entity key in one step\r\n * \r\n * Convenience function that combines deriveEntityKey and importEntityKey.\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - Unique identifier for the entity\r\n * @param entityType - Type of entity\r\n * @returns CryptoKey ready for encryption/decryption\r\n * @throws {Error} If derivation or import fails\r\n * \r\n * @example\r\n * ```ts\r\n * const entityKey = await deriveAndImportEntityKey(\r\n * householdKey,\r\n * 'property-uuid-123',\r\n * 'property'\r\n * );\r\n * \r\n * // Use directly for encryption\r\n * const encrypted = await crypto.subtle.encrypt(\r\n * { name: 'AES-GCM', iv },\r\n * entityKey,\r\n * dataBytes\r\n * );\r\n * ```\r\n */\r\nexport async function deriveAndImportEntityKey(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType\r\n): Promise<CryptoKey> {\r\n const entityKeyBytes = await deriveEntityKey(householdKey, entityId, entityType);\r\n return await importEntityKey(entityKeyBytes);\r\n}\r\n\r\n/**\r\n * Batch derives multiple entity keys for the same household\r\n * \r\n * This is useful for:\r\n * - Initial sync (decrypt many entities at once)\r\n * - Bulk operations (encrypt/decrypt multiple entities)\r\n * - Pre-derivation for offline use\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param entities - Array of entity metadata (id and type)\r\n * @returns Map of entity IDs to derived key bytes\r\n * @throws {Error} If any derivation fails\r\n * \r\n * @example\r\n * ```ts\r\n * const entities = [\r\n * { id: 'prop-1', type: 'property' },\r\n * { id: 'prop-2', type: 'property' },\r\n * { id: 'pet-1', type: 'pet' }\r\n * ];\r\n * \r\n * const keys = await deriveEntityKeysBatch(householdKey, entities);\r\n * \r\n * // Decrypt all entities\r\n * for (const entity of entities) {\r\n * const key = keys.get(entity.id);\r\n * const decrypted = await decryptEntity(entity, key);\r\n * }\r\n * ```\r\n */\r\nexport async function deriveEntityKeysBatch(\r\n householdKey: Uint8Array,\r\n entities: Array<{ id: string; type: EntityType }>\r\n): Promise<Map<string, Uint8Array>> {\r\n const keys = new Map<string, Uint8Array>();\r\n \r\n // Derive keys in parallel for better performance\r\n await Promise.all(\r\n entities.map(async (entity) => {\r\n const key = await deriveEntityKey(householdKey, entity.id, entity.type);\r\n keys.set(entity.id, key);\r\n })\r\n );\r\n \r\n return keys;\r\n}\r\n\r\n/**\r\n * Verifies that a derived entity key is correct by re-deriving it\r\n * \r\n * This can be used to:\r\n * - Validate cached entity keys\r\n * - Verify that household key hasn't changed\r\n * - Test key derivation implementation\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - Entity identifier\r\n * @param entityType - Entity type\r\n * @param expectedKey - Expected derived key bytes\r\n * @returns true if keys match, false otherwise\r\n * \r\n * @example\r\n * ```ts\r\n * const cachedKey = getCachedEntityKey(entityId);\r\n * const isValid = await verifyEntityKey(\r\n * householdKey,\r\n * entityId,\r\n * 'property',\r\n * cachedKey\r\n * );\r\n * \r\n * if (!isValid) {\r\n * // Re-derive and update cache\r\n * const freshKey = await deriveEntityKey(householdKey, entityId, 'property');\r\n * updateCache(entityId, freshKey);\r\n * }\r\n * ```\r\n */\r\nexport async function verifyEntityKey(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType,\r\n expectedKey: Uint8Array\r\n): Promise<boolean> {\r\n try {\r\n const derivedKey = await deriveEntityKey(householdKey, entityId, entityType);\r\n \r\n // Constant-time comparison\r\n if (derivedKey.length !== expectedKey.length) {\r\n return false;\r\n }\r\n \r\n let result = 0;\r\n for (let i = 0; i < derivedKey.length; i++) {\r\n result |= derivedKey[i] ^ expectedKey[i];\r\n }\r\n \r\n return result === 0;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n","/**\r\n * Entity Encryption Module\r\n * \r\n * This module handles the encryption and decryption of entity data using\r\n * AES-256-GCM authenticated encryption.\r\n * \r\n * Entity encryption flow:\r\n * 1. Derive entity-specific key from household key (HKDF)\r\n * 2. Serialize entity data to JSON\r\n * 3. Encrypt with AES-256-GCM using derived key\r\n * 4. Return ciphertext + IV + metadata\r\n * \r\n * Entity decryption flow:\r\n * 1. Derive same entity key from household key\r\n * 2. Decrypt ciphertext with AES-256-GCM\r\n * 3. Deserialize JSON to entity object\r\n * \r\n * @module encryption/entityEncryption\r\n */\r\n\r\nimport { base64Encode, base64Decode, stringToBytes, bytesToString, generateRandomBytes } from './utils';\r\nimport { deriveEntityKey, importEntityKey } from './entityKeys';\r\nimport type { EntityType, EncryptedData, EncryptedEntity, HouseholdKeyType } from './types';\r\n\r\n/**\r\n * IV size for AES-GCM (96 bits / 12 bytes)\r\n */\r\nexport const IV_SIZE = 12;\r\n\r\n/**\r\n * Auth tag size for AES-GCM (128 bits / 16 bytes)\r\n */\r\nexport const AUTH_TAG_SIZE = 16;\r\n\r\n/**\r\n * Pack encrypted data into blob format: [1-byte version][12-byte IV][ciphertext][16-byte auth tag]\r\n * Note: AES-GCM already includes the auth tag in the ciphertext, so we just prepend version + IV\r\n */\r\nexport function packEncryptedBlob(version: number, iv: Uint8Array, ciphertext: Uint8Array): string {\r\n const blob = new Uint8Array(1 + iv.length + ciphertext.length);\r\n blob[0] = version;\r\n blob.set(iv, 1);\r\n blob.set(ciphertext, 1 + iv.length);\r\n return base64Encode(blob);\r\n}\r\n\r\n/**\r\n * Unpack encrypted blob into version, IV, and ciphertext\r\n * Format: [1-byte version][12-byte IV][ciphertext][16-byte auth tag]\r\n */\r\nexport function unpackEncryptedBlob(packedBlob: string): { version: number; iv: Uint8Array; ciphertext: Uint8Array } {\r\n const blob = base64Decode(packedBlob);\r\n \r\n if (blob.length < 1 + IV_SIZE + AUTH_TAG_SIZE) {\r\n throw new Error('Invalid encrypted blob: too short');\r\n }\r\n \r\n const version = blob[0];\r\n const iv = blob.slice(1, 1 + IV_SIZE);\r\n const ciphertext = blob.slice(1 + IV_SIZE);\r\n \r\n return { version, iv, ciphertext };\r\n}\r\n\r\n/**\r\n * Options for entity encryption\r\n */\r\nexport interface EncryptEntityOptions {\r\n /** Additional authenticated data (not encrypted, but integrity-protected) */\r\n additionalData?: Uint8Array;\r\n}\r\n\r\n/**\r\n * Options for decrypting entity data\r\n */\r\nexport interface DecryptEntityOptions {\r\n /** Additional authenticated data (must match what was used during encryption) */\r\n additionalData?: Uint8Array;\r\n \r\n /** Expected entity type (for validation) */\r\n expectedType?: EntityType;\r\n \r\n /** Pre-derived entity key (skips re-derivation for performance) */\r\n entityKey?: Uint8Array;\r\n}\r\n\r\n/**\r\n * Encrypts entity data with a derived entity-specific key\r\n * \r\n * This function:\r\n * 1. Derives a unique key for this specific entity using HKDF\r\n * 2. Serializes the entity data to JSON\r\n * 3. Encrypts using AES-256-GCM (provides both confidentiality and authenticity)\r\n * 4. Returns the encrypted data with metadata\r\n * \r\n * The encrypted data can only be decrypted with:\r\n * - The same household key\r\n * - The same entity ID and type\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - Unique identifier for the entity\r\n * @param entityType - Type of entity being encrypted\r\n * @param keyType - Which household key type was used (for metadata)\r\n * @param entityData - Entity data to encrypt (will be JSON serialized)\r\n * @param options - Optional encryption settings\r\n * @returns Encrypted entity with metadata\r\n * @throws {Error} If encryption fails or inputs are invalid\r\n * \r\n * @example\r\n * ```ts\r\n * const pet = {\r\n * name: 'Fluffy',\r\n * species: 'cat',\r\n * breed: 'Persian',\r\n * microchipId: '123456789'\r\n * };\r\n * \r\n * const encrypted = await encryptEntity(\r\n * householdKeys.get('general')!, // Household key bytes\r\n * 'a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d', // Entity ID (UUID)\r\n * 'pet', // Entity type\r\n * 'general', // Key type (metadata)\r\n * pet\r\n * );\r\n * \r\n * // Now you have:\r\n * // - encrypted.ciphertext (upload to server)\r\n * // - encrypted.derivedEntityKey (cache for performance)\r\n * \r\n * // Upload to server\r\n * await api.post('/encrypted-entities', {\r\n * id: encrypted.entityId,\r\n * entity_type: encrypted.entityType,\r\n * encrypted_data: encrypted.ciphertext,\r\n * iv: encrypted.iv,\r\n * key_type: encrypted.keyType\r\n * });\r\n * ```\r\n */\r\nexport async function encryptEntity<T = unknown>(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType,\r\n keyType: HouseholdKeyType,\r\n entityData: T,\r\n options: EncryptEntityOptions = {}\r\n): Promise<EncryptedEntity> {\r\n try {\r\n // Derive entity-specific key\r\n const entityKeyBytes = await deriveEntityKey(householdKey, entityId, entityType);\r\n const entityKey = await importEntityKey(entityKeyBytes);\r\n \r\n // Serialize entity data to JSON\r\n const jsonData = JSON.stringify(entityData);\r\n const dataBytes = stringToBytes(jsonData);\r\n \r\n // Generate random IV\r\n const iv = generateRandomBytes(IV_SIZE);\r\n \r\n // Prepare encryption parameters\r\n const encryptParams: AesGcmParams = {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n };\r\n \r\n // Add additional authenticated data if provided\r\n if (options.additionalData) {\r\n encryptParams.additionalData = options.additionalData as BufferSource;\r\n }\r\n \r\n // Encrypt with AES-GCM\r\n const ciphertext = await crypto.subtle.encrypt(\r\n encryptParams,\r\n entityKey,\r\n dataBytes as BufferSource\r\n );\r\n \r\n const result: EncryptedEntity = {\r\n entityId,\r\n entityType,\r\n keyType,\r\n ciphertext: base64Encode(new Uint8Array(ciphertext)),\r\n iv: base64Encode(iv),\r\n encryptedAt: new Date(),\r\n derivedEntityKey: entityKeyBytes\r\n };\r\n \r\n return result;\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to encrypt ${entityType} entity ${entityId}: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Decrypts entity data encrypted with encryptEntity\r\n * \r\n * This function:\r\n * 1. Re-derives the same entity key using HKDF\r\n * 2. Decrypts the ciphertext using AES-256-GCM\r\n * 3. Deserializes the JSON data\r\n * 4. Returns the original entity object\r\n * \r\n * @param householdKey - Raw household key bytes (must be the same as used for encryption)\r\n * @param encrypted - Encrypted entity data\r\n * @param options - Optional decryption settings\r\n * @returns Decrypted entity data\r\n * @throws {Error} If decryption fails (wrong key, corrupted data, tampered data, etc.)\r\n * \r\n * @example\r\n * ```ts\r\n * // Fetch from server\r\n * const encryptedEntity = await api.get(`/encrypted-entities/${entityId}`);\r\n * \r\n * // Decrypt\r\n * const property = await decryptEntity(householdKey, {\r\n * entityId: encryptedEntity.id,\r\n * entityType: encryptedEntity.entity_type,\r\n * keyType: encryptedEntity.key_type,\r\n * ciphertext: encryptedEntity.encrypted_data,\r\n * iv: encryptedEntity.iv,\r\n * encryptedAt: new Date(encryptedEntity.created_at)\r\n * });\r\n * \r\n * console.log(property.address); // '123 Main St'\r\n * ```\r\n */\r\nexport async function decryptEntity<T = unknown>(\r\n householdKey: Uint8Array,\r\n encrypted: EncryptedEntity,\r\n options: DecryptEntityOptions = {}\r\n): Promise<T> {\r\n // Validate entity type if provided\r\n if (options.expectedType && encrypted.entityType !== options.expectedType) {\r\n throw new Error(\r\n `Entity type mismatch: expected ${options.expectedType}, got ${encrypted.entityType}`\r\n );\r\n }\r\n\r\n try {\r\n // Use cached entity key if provided, otherwise derive it\r\n const entityKeyBytes = options.entityKey\r\n ? options.entityKey\r\n : await deriveEntityKey(householdKey, encrypted.entityId, encrypted.entityType);\r\n\r\n const entityKey = await importEntityKey(entityKeyBytes);\r\n \r\n // Decode ciphertext and IV\r\n const ciphertext = base64Decode(encrypted.ciphertext);\r\n const iv = base64Decode(encrypted.iv);\r\n \r\n // Prepare decryption parameters\r\n const decryptParams: AesGcmParams = {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n };\r\n \r\n // Add additional authenticated data if provided\r\n if (options.additionalData) {\r\n decryptParams.additionalData = options.additionalData as BufferSource;\r\n }\r\n \r\n // Decrypt with AES-GCM\r\n const plaintext = await crypto.subtle.decrypt(\r\n decryptParams,\r\n entityKey,\r\n ciphertext as BufferSource\r\n );\r\n \r\n // Convert to string and parse JSON\r\n const jsonData = bytesToString(new Uint8Array(plaintext));\r\n return JSON.parse(jsonData) as T;\r\n } catch (error) {\r\n // Provide helpful error messages\r\n if (error instanceof Error && error.name === 'OperationError') {\r\n throw new Error(\r\n `Failed to decrypt ${encrypted.entityType} entity ${encrypted.entityId}: ` +\r\n 'Authentication failed. This may indicate a wrong household key, corrupted data, or tampered ciphertext.'\r\n );\r\n }\r\n \r\n throw new Error(\r\n `Failed to decrypt ${encrypted.entityType} entity ${encrypted.entityId}: ` +\r\n `${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Batch encrypts multiple entities with the same household key\r\n * \r\n * This is more efficient than encrypting one at a time because:\r\n * - Keys can be derived in parallel\r\n * - Reduces overhead of multiple function calls\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param entities - Array of entities to encrypt\r\n * @returns Array of encrypted entities\r\n * @throws {Error} If any encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * const properties = [\r\n * { id: 'prop-1', address: '123 Main St', value: 500000 },\r\n * { id: 'prop-2', address: '456 Oak Ave', value: 750000 }\r\n * ];\r\n * \r\n * const encrypted = await encryptEntitiesBatch(\r\n * householdKey,\r\n * properties.map(p => ({\r\n * id: p.id,\r\n * type: 'property' as const,\r\n * data: p\r\n * }))\r\n * );\r\n * \r\n * // Upload all at once\r\n * await api.post('/encrypted-entities/batch', encrypted);\r\n * ```\r\n */\r\nexport async function encryptEntitiesBatch<T = unknown>(\r\n householdKey: Uint8Array,\r\n entities: Array<{\r\n id: string;\r\n type: EntityType;\r\n keyType: HouseholdKeyType;\r\n data: T;\r\n options?: EncryptEntityOptions;\r\n }>\r\n): Promise<EncryptedEntity[]> {\r\n // Encrypt all entities in parallel\r\n return await Promise.all(\r\n entities.map((entity) =>\r\n encryptEntity(\r\n householdKey,\r\n entity.id,\r\n entity.type,\r\n entity.keyType,\r\n entity.data,\r\n entity.options\r\n )\r\n )\r\n );\r\n}\r\n\r\n/**\r\n * Batch decrypts multiple entities with the same household key\r\n * \r\n * @param householdKey - Raw household key bytes\r\n * @param encryptedEntities - Array of encrypted entities\r\n * @param options - Optional decryption settings (applied to all)\r\n * @returns Array of decrypted entity data\r\n * @throws {Error} If any decryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * // Fetch from server\r\n * const encryptedEntities = await api.get('/encrypted-entities', {\r\n * household_id: householdId\r\n * });\r\n * \r\n * // Decrypt all at once\r\n * const properties = await decryptEntitiesBatch(\r\n * householdKey,\r\n * encryptedEntities.map(e => ({\r\n * entityId: e.id,\r\n * entityType: e.entity_type,\r\n * keyType: e.key_type,\r\n * ciphertext: e.encrypted_data,\r\n * iv: e.iv,\r\n * encryptedAt: new Date(e.created_at)\r\n * }))\r\n * );\r\n * ```\r\n */\r\nexport async function decryptEntitiesBatch<T = unknown>(\r\n householdKey: Uint8Array,\r\n encryptedEntities: EncryptedEntity[],\r\n options: DecryptEntityOptions = {}\r\n): Promise<T[]> {\r\n // Decrypt all entities in parallel\r\n return await Promise.all(\r\n encryptedEntities.map((entity) => decryptEntity<T>(householdKey, entity, options))\r\n );\r\n}\r\n\r\n/**\r\n * Re-encrypts an entity with a new household key\r\n * \r\n * This is used during key rotation:\r\n * 1. Decrypt with old household key\r\n * 2. Re-encrypt with new household key\r\n * 3. Upload updated ciphertext\r\n * \r\n * Note: Entity keys are deterministic, so they don't need to change.\r\n * Only the household key changes, which means we need to re-derive the entity key.\r\n * \r\n * @param oldHouseholdKey - Current household key\r\n * @param newHouseholdKey - New household key (after rotation)\r\n * @param encrypted - Currently encrypted entity\r\n * @returns Re-encrypted entity with new ciphertext\r\n * @throws {Error} If decryption or encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * // During key rotation\r\n * const entities = await fetchEntities(householdId);\r\n * \r\n * for (const entity of entities) {\r\n * const reEncrypted = await reEncryptEntity(\r\n * oldHouseholdKey,\r\n * newHouseholdKey,\r\n * entity\r\n * );\r\n * \r\n * await api.patch(`/encrypted-entities/${entity.entityId}`, {\r\n * encrypted_data: reEncrypted.ciphertext,\r\n * iv: reEncrypted.iv\r\n * });\r\n * }\r\n * ```\r\n */\r\nexport async function reEncryptEntity(\r\n oldHouseholdKey: Uint8Array,\r\n newHouseholdKey: Uint8Array,\r\n encrypted: EncryptedEntity\r\n): Promise<EncryptedEntity> {\r\n // Decrypt with old key\r\n const decryptedData = await decryptEntity(oldHouseholdKey, encrypted);\r\n \r\n // Re-encrypt with new key\r\n return await encryptEntity(\r\n newHouseholdKey,\r\n encrypted.entityId,\r\n encrypted.entityType,\r\n encrypted.keyType,\r\n decryptedData\r\n );\r\n}\r\n\r\n/**\r\n * Encrypts just the data payload without entity metadata\r\n * \r\n * This is a lower-level function useful for:\r\n * - Encrypting arbitrary data (not full entities)\r\n * - Custom encryption flows\r\n * - Testing\r\n * \r\n * @param key - CryptoKey for encryption\r\n * @param data - Data to encrypt (will be JSON serialized)\r\n * @param additionalData - Optional authenticated data\r\n * @returns Encrypted data with IV\r\n * @throws {Error} If encryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * const entityKey = await deriveAndImportEntityKey(householdKey, id, type);\r\n * const encrypted = await encryptData(entityKey, { secret: 'value' });\r\n * ```\r\n */\r\nexport async function encryptData<T = unknown>(\r\n key: CryptoKey,\r\n data: T,\r\n additionalData?: Uint8Array\r\n): Promise<EncryptedData> {\r\n try {\r\n const jsonData = JSON.stringify(data);\r\n const dataBytes = stringToBytes(jsonData);\r\n const iv = generateRandomBytes(IV_SIZE);\r\n \r\n const encryptParams: AesGcmParams = {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n };\r\n \r\n if (additionalData) {\r\n encryptParams.additionalData = additionalData as BufferSource;\r\n }\r\n \r\n const ciphertext = await crypto.subtle.encrypt(\r\n encryptParams,\r\n key,\r\n dataBytes as BufferSource\r\n );\r\n \r\n return {\r\n ciphertext: base64Encode(new Uint8Array(ciphertext)),\r\n iv: base64Encode(iv)\r\n };\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to encrypt data: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Decrypts just the data payload without entity metadata\r\n * \r\n * @param key - CryptoKey for decryption\r\n * @param encrypted - Encrypted data\r\n * @param additionalData - Optional authenticated data (must match encryption)\r\n * @returns Decrypted data\r\n * @throws {Error} If decryption fails\r\n * \r\n * @example\r\n * ```ts\r\n * const entityKey = await deriveAndImportEntityKey(householdKey, id, type);\r\n * const decrypted = await decryptData(entityKey, encrypted);\r\n * ```\r\n */\r\nexport async function decryptData<T = unknown>(\r\n key: CryptoKey,\r\n encrypted: EncryptedData,\r\n additionalData?: Uint8Array\r\n): Promise<T> {\r\n try {\r\n const ciphertext = base64Decode(encrypted.ciphertext);\r\n const iv = base64Decode(encrypted.iv);\r\n \r\n const decryptParams: AesGcmParams = {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n };\r\n \r\n if (additionalData) {\r\n decryptParams.additionalData = additionalData as BufferSource;\r\n }\r\n \r\n const plaintext = await crypto.subtle.decrypt(\r\n decryptParams,\r\n key,\r\n ciphertext as BufferSource\r\n );\r\n \r\n const jsonData = bytesToString(new Uint8Array(plaintext));\r\n return JSON.parse(jsonData) as T;\r\n } catch (error) {\r\n if (error instanceof Error && error.name === 'OperationError') {\r\n throw new Error('Decryption failed: Authentication error (wrong key or tampered data)');\r\n }\r\n \r\n throw new Error(\r\n `Failed to decrypt data: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n","/**\r\n * Entity Type to Key Type Mapping\r\n *\r\n * Single source of truth for which household/personal key encrypts which entity types.\r\n *\r\n * Key Types:\r\n * - general: Properties, Pets, Vehicles, Access Codes (WiFi, door, gate)\r\n * - financial: Bank Accounts, Tax Docs, Estate Planning\r\n * - health: Medical records, prescriptions, health data\r\n * - subscription: Subscriptions\r\n * - access_code: Access Codes (WiFi, door, gate)\r\n * - identity: Digital identities (Apple ID, Google Account, etc.) - Personal vault\r\n */\r\n\r\nimport type { EntityType, HouseholdKeyType } from './types';\r\n\r\n/**\r\n * Map entity types to their corresponding household key types\r\n */\r\nexport const ENTITY_KEY_TYPE_MAP: Record<EntityType, HouseholdKeyType> = {\r\n // General household items\r\n 'property': 'general',\r\n 'maintenance_task': 'task',\r\n 'pet': 'general',\r\n 'vehicle': 'general',\r\n 'device': 'general',\r\n 'valuable': 'general',\r\n 'valuables': 'general', // Route alias\r\n 'access_code': 'access_code',\r\n 'contact': 'general',\r\n 'service': 'general',\r\n 'document': 'general',\r\n 'travel': 'general',\r\n 'resident': 'general',\r\n 'home_improvement': 'general', // Property improvements\r\n 'vehicle_maintenance': 'general', // Vehicle maintenance history\r\n 'vehicle_service_visit': 'general', // Vehicle service visits\r\n 'pet_vet_visit': 'general', // Pet vet visits (like vehicle_service_visit for vehicles)\r\n 'pet_health': 'general', // Pet health records (simple single records)\r\n 'military_record': 'general', // Military service records\r\n 'education_record': 'general', // Education records (diplomas, transcripts, etc.)\r\n 'credential': 'general', // Credentials (professional licenses, government IDs, etc.)\r\n 'credentials': 'general', // Route alias\r\n 'membership_record': 'general', // Membership records (airline, hotel, retail loyalty programs)\r\n\r\n // Health records (sensitive - requires health key)\r\n 'health_record': 'health',\r\n\r\n // Financial\r\n 'bank_account': 'financial',\r\n 'investment': 'financial',\r\n 'tax_document': 'financial',\r\n 'tax_year': 'financial',\r\n 'taxes': 'financial', // Route alias\r\n 'financial_account': 'financial',\r\n 'financial': 'financial', // Route alias\r\n\r\n // Legal (owner-only)\r\n 'legal': 'legal',\r\n\r\n // Insurance (both names for compatibility)\r\n 'insurance': 'general',\r\n 'insurance_policy': 'general',\r\n\r\n 'subscription': 'subscription',\r\n\r\n // Passwords (shared household credentials)\r\n 'password': 'password',\r\n\r\n // Identity (Personal Vault)\r\n 'identity': 'identity',\r\n\r\n // Calendar Connections (Personal Vault - user's OAuth tokens for calendar sync)\r\n 'calendar_connection': 'identity',\r\n\r\n // Continuity (Personal Vault - shared messages for beneficiaries)\r\n 'continuity': 'continuity',\r\n\r\n // Emergency (household-scoped emergency info, encrypted with general key so all members can decrypt)\r\n 'emergency': 'general',\r\n};\r\n\r\n/**\r\n * Get the household key type for a given entity type\r\n * \r\n * @param entityType - The entity type\r\n * @returns The household key type to use for encryption\r\n * @throws {Error} If entity type is not mapped\r\n */\r\nexport function getKeyTypeForEntity(entityType: EntityType): HouseholdKeyType {\r\n const keyType = ENTITY_KEY_TYPE_MAP[entityType];\r\n \r\n if (!keyType) {\r\n throw new Error(\r\n `No key type mapping found for entity type: ${entityType}. ` +\r\n `Please add it to ENTITY_KEY_TYPE_MAP in entityKeyMapping.ts`\r\n );\r\n }\r\n \r\n return keyType;\r\n}\r\n\r\n/**\r\n * Current crypto version for new encryptions\r\n */\r\nexport const CURRENT_CRYPTO_VERSION = 1;\r\n\r\n/**\r\n * Crypto version descriptions for reference\r\n */\r\nexport const CRYPTO_VERSION_INFO = {\r\n 1: {\r\n algorithm: 'AES-256-GCM',\r\n format: '[1-byte version][12-byte IV][ciphertext][16-byte auth tag]',\r\n description: 'Initial version with IV packed in blob'\r\n }\r\n} as const;\r\n","/**\r\n * Recovery Key Generation and Formatting\r\n *\r\n * Generates and formats recovery keys in the format: ABCD-EFGH-IJKL-MNOP-QRST-UVWX\r\n * Recovery keys are 128-bit random values encoded in base32 (for readability).\r\n *\r\n * Recovery Key + server_wrap_secret = WrapKey (via HKDF)\r\n * WrapKey decrypts user_key_bundles.encrypted_private_key\r\n *\r\n * Used for:\r\n * - New device setup (before PRF enrolled)\r\n * - Non-Apple ecosystem devices\r\n * - Backup recovery if all devices lost\r\n *\r\n * @module encryption/recoveryKey\r\n */\r\n\r\nimport { base64Encode, generateRandomBytes } from './utils';\r\n\r\n/**\r\n * Recovery key size in bytes (128 bits = 16 bytes)\r\n * Provides sufficient entropy while remaining manageable for users\r\n */\r\nexport const RECOVERY_KEY_SIZE = 16;\r\n\r\n/**\r\n * Number of characters per group in formatted recovery key\r\n */\r\nconst GROUP_SIZE = 4;\r\n\r\n/**\r\n * Base32 alphabet (RFC 4648) - uses uppercase letters and digits 2-7\r\n * Excludes 0, 1, 8, 9 to avoid confusion with O, I, B, g\r\n */\r\nconst BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';\r\n\r\n/**\r\n * Encode bytes to base32 string\r\n */\r\nfunction encodeBase32(bytes: Uint8Array): string {\r\n let bits = '';\r\n\r\n // Convert bytes to binary string\r\n for (const byte of bytes) {\r\n bits += byte.toString(2).padStart(8, '0');\r\n }\r\n\r\n // Pad to multiple of 5 bits\r\n while (bits.length % 5 !== 0) {\r\n bits += '0';\r\n }\r\n\r\n // Convert 5-bit chunks to base32 characters\r\n let result = '';\r\n for (let i = 0; i < bits.length; i += 5) {\r\n const chunk = bits.substring(i, i + 5);\r\n const index = parseInt(chunk, 2);\r\n result += BASE32_ALPHABET[index];\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Decode base32 string to bytes\r\n */\r\nfunction decodeBase32(base32: string): Uint8Array {\r\n const cleaned = base32.toUpperCase().replace(/[^A-Z2-7]/g, '');\r\n\r\n let bits = '';\r\n\r\n // Convert base32 characters to binary string\r\n for (const char of cleaned) {\r\n const index = BASE32_ALPHABET.indexOf(char);\r\n if (index === -1) {\r\n throw new Error(`Invalid base32 character: ${char}`);\r\n }\r\n bits += index.toString(2).padStart(5, '0');\r\n }\r\n\r\n // Convert binary string to bytes\r\n const bytes: number[] = [];\r\n for (let i = 0; i < bits.length - (bits.length % 8); i += 8) {\r\n const byte = parseInt(bits.substring(i, i + 8), 2);\r\n bytes.push(byte);\r\n }\r\n\r\n return new Uint8Array(bytes);\r\n}\r\n\r\n/**\r\n * Format recovery key with hyphens between groups\r\n *\r\n * Input: \"ABCDEFGHIJKLMNOPQRSTUVWX\"\r\n * Output: \"ABCD-EFGH-IJKL-MNOP-QRST-UVWX\"\r\n */\r\nexport function formatRecoveryKey(base32: string): string {\r\n const groups: string[] = [];\r\n for (let i = 0; i < base32.length; i += GROUP_SIZE) {\r\n groups.push(base32.substring(i, i + GROUP_SIZE));\r\n }\r\n return groups.join('-');\r\n}\r\n\r\n/**\r\n * Convert recovery key bytes to formatted display string\r\n *\r\n * @param bytes - Recovery key bytes (16 bytes)\r\n * @returns Formatted recovery key (e.g., \"ABCD-EFGH-IJKL-MNOP-QRST-UVWX\")\r\n */\r\nexport function bytesToFormattedRecoveryKey(bytes: Uint8Array): string {\r\n const base32 = encodeBase32(bytes);\r\n return formatRecoveryKey(base32);\r\n}\r\n\r\n/**\r\n * Remove formatting from recovery key\r\n */\r\nfunction unformatRecoveryKey(formatted: string): string {\r\n return formatted.toUpperCase().replace(/[^A-Z2-7]/g, '');\r\n}\r\n\r\n/**\r\n * Validate recovery key format\r\n */\r\nexport function validateRecoveryKey(recoveryKey: string): boolean {\r\n try {\r\n const unformatted = unformatRecoveryKey(recoveryKey);\r\n\r\n // Check length (should be ~26 characters for 128-bit key)\r\n if (unformatted.length < 20 || unformatted.length > 30) {\r\n return false;\r\n }\r\n\r\n // Check all characters are valid base32\r\n for (const char of unformatted) {\r\n if (!BASE32_ALPHABET.includes(char)) {\r\n return false;\r\n }\r\n }\r\n\r\n // Try to decode\r\n const bytes = decodeBase32(unformatted);\r\n\r\n // Should decode to approximately RECOVERY_KEY_SIZE bytes\r\n return bytes.length >= RECOVERY_KEY_SIZE - 2 && bytes.length <= RECOVERY_KEY_SIZE + 2;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Generate a new recovery key\r\n *\r\n * @returns Object containing raw bytes and formatted key\r\n *\r\n * @example\r\n * ```ts\r\n * const { bytes, formatted, base64 } = generateRecoveryKey();\r\n * console.log(formatted); // \"ABCD-EFGH-IJKL-MNOP-QRST-UVWX\"\r\n *\r\n * // Show to user for download/printing\r\n * showRecoveryKeyToUser(formatted);\r\n *\r\n * // Use bytes for key derivation\r\n * const wrapKey = await deriveWrapKey(bytes, serverWrapSecret);\r\n * ```\r\n */\r\nexport function generateRecoveryKey(): {\r\n bytes: Uint8Array;\r\n formatted: string;\r\n base64: string;\r\n} {\r\n const bytes = generateRandomBytes(RECOVERY_KEY_SIZE);\r\n const base32 = encodeBase32(bytes);\r\n const formatted = formatRecoveryKey(base32);\r\n const base64 = base64Encode(bytes);\r\n\r\n return {\r\n bytes,\r\n formatted,\r\n base64\r\n };\r\n}\r\n\r\n/**\r\n * Parse recovery key from user input\r\n *\r\n * @param recoveryKey - User-entered recovery key (with or without hyphens)\r\n * @returns Parsed recovery key bytes and formatted version\r\n * @throws {Error} If recovery key is invalid\r\n *\r\n * @example\r\n * ```ts\r\n * const userInput = \"ABCD-EFGH-IJKL-MNOP-QRST-UVWX\";\r\n * const { bytes, formatted } = parseRecoveryKey(userInput);\r\n *\r\n * // Use bytes for key derivation\r\n * const wrapKey = await deriveWrapKey(bytes, serverWrapSecret);\r\n * ```\r\n */\r\nexport function parseRecoveryKey(recoveryKey: string): {\r\n bytes: Uint8Array;\r\n formatted: string;\r\n base64: string;\r\n} {\r\n if (!validateRecoveryKey(recoveryKey)) {\r\n throw new Error('Invalid recovery key format');\r\n }\r\n\r\n const unformatted = unformatRecoveryKey(recoveryKey);\r\n const bytes = decodeBase32(unformatted);\r\n\r\n // Ensure exact size (trim or pad if needed)\r\n const normalizedBytes = new Uint8Array(RECOVERY_KEY_SIZE);\r\n normalizedBytes.set(bytes.slice(0, RECOVERY_KEY_SIZE));\r\n\r\n const base32 = encodeBase32(normalizedBytes);\r\n const formatted = formatRecoveryKey(base32);\r\n const base64 = base64Encode(normalizedBytes);\r\n\r\n return {\r\n bytes: normalizedBytes,\r\n formatted,\r\n base64\r\n };\r\n}\r\n\r\n/**\r\n * Derive WrapKey from Recovery Key + server_wrap_secret\r\n *\r\n * WrapKey = HKDF(recoveryKey, serverWrapSecret, info)\r\n *\r\n * @param recoveryKeyBytes - Recovery key bytes (16 bytes)\r\n * @param serverWrapSecret - Server-side secret (32 bytes)\r\n * @param info - Context info string\r\n * @returns WrapKey for decrypting private key\r\n *\r\n * @example\r\n * ```ts\r\n * // On login with recovery key\r\n * const { bytes } = parseRecoveryKey(userInput);\r\n * const serverSecret = await fetchServerWrapSecret(userId);\r\n * const wrapKey = await deriveWrapKey(bytes, serverSecret);\r\n *\r\n * // Decrypt private key\r\n * const privateKey = await decryptPrivateKey(wrapKey, encryptedPrivateKey);\r\n * ```\r\n */\r\nexport async function deriveWrapKey(\r\n recoveryKeyBytes: Uint8Array,\r\n serverWrapSecret: Uint8Array,\r\n info: string = 'hearthcoo-wrap-key-v1'\r\n): Promise<CryptoKey> {\r\n if (recoveryKeyBytes.length !== RECOVERY_KEY_SIZE) {\r\n throw new Error(`Recovery key must be ${RECOVERY_KEY_SIZE} bytes`);\r\n }\r\n\r\n if (serverWrapSecret.length !== 32) {\r\n throw new Error('Server wrap secret must be 32 bytes');\r\n }\r\n\r\n // Import recovery key as key material\r\n const recoveryKeyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n recoveryKeyBytes as BufferSource,\r\n 'HKDF',\r\n false,\r\n ['deriveKey']\r\n );\r\n\r\n // Derive WrapKey using HKDF\r\n const wrapKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: serverWrapSecret as BufferSource,\r\n info: new TextEncoder().encode(info)\r\n },\r\n recoveryKeyMaterial,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false,\r\n ['encrypt', 'decrypt']\r\n );\r\n\r\n return wrapKey;\r\n}\r\n","/**\r\n * Key Type Constants\r\n *\r\n * Single source of truth for all key type definitions in the application.\r\n */\r\n\r\n/**\r\n * Default algorithm for key bundles (ECDH with P-521 curve).\r\n */\r\nexport const DEFAULT_KEY_BUNDLE_ALG = 'ECDH-P-521'\r\n\r\n/**\r\n * Supported key bundle algorithms.\r\n */\r\nexport type KeyBundleAlgorithm = 'ECDH-ES' | 'RSA-OAEP' | 'ECDH-P-521'\r\n\r\n/**\r\n * Household key types that should exist for every household.\r\n * These are category-level keys that encrypt different types of household data.\r\n */\r\nexport const HOUSEHOLD_KEY_TYPES = ['general', 'financial', 'health', 'legal', 'task', 'subscription', 'access_code', 'password', 'sharing'] as const\r\nexport type HouseholdKeyType = typeof HOUSEHOLD_KEY_TYPES[number]\r\n\r\n/**\r\n * Personal vault key types that should exist for every user.\r\n * These are category-level keys for personal (non-household) data.\r\n */\r\nexport const PERSONAL_KEY_TYPES = ['identity', 'personal'] as const\r\nexport type PersonalKeyType = typeof PERSONAL_KEY_TYPES[number]\r\n\r\n/**\r\n * All possible key types (union of household and personal).\r\n */\r\nexport type AllKeyTypes = HouseholdKeyType | PersonalKeyType\r\n\r\n/**\r\n * Get which household key types a role should have access to by default.\r\n *\r\n * @param role - The household member role (owner, member, executor)\r\n * @returns Array of key types the role should have access to by default\r\n */\r\nexport function getKeyTypesForRole(role: string | undefined | null): HouseholdKeyType[] {\r\n if (!role) return ['general', 'task']\r\n switch (role.toLowerCase()) {\r\n case 'owner':\r\n return ['general', 'financial', 'health', 'legal', 'task', 'subscription', 'access_code', 'password', 'sharing']\r\n case 'member':\r\n return ['general', 'task', 'subscription', 'access_code']\r\n case 'executor':\r\n // Executors have no default key access (can be customized during invitation)\r\n return []\r\n default:\r\n return ['general', 'task']\r\n }\r\n}\r\n\r\n/**\r\n * Friendly names and descriptions for key types.\r\n * Description maps to navigation items in the left sidebar for clarity.\r\n */\r\nexport const KEY_TYPE_INFO: Record<HouseholdKeyType, { name: string; description: string }> = {\r\n general: { name: 'General', description: 'Properties, Pets, Vehicles, Contacts, Insurance, Devices, Valuables, Services' },\r\n financial: { name: 'Financial', description: 'Financial Accounts, Taxes' },\r\n health: { name: 'Health', description: 'Medical Records, Prescriptions' },\r\n legal: { name: 'Legal', description: 'Legal Documents' },\r\n task: { name: 'Tasks', description: 'Maintenance Tasks' },\r\n subscription: { name: 'Subscriptions', description: 'Subscriptions' },\r\n access_code: { name: 'Access Codes', description: 'Access Codes' },\r\n password: { name: 'Passwords', description: 'Passwords' },\r\n sharing: { name: 'Sharing', description: 'Share Packages' }\r\n}\r\n","/**\r\n * Entity Options\r\n *\r\n * Central definitions for all dropdown/select options used in forms.\r\n * Import these in both web and mobile to ensure consistency.\r\n */\r\n\r\n// =============================================================================\r\n// Person/Resident Entity Type Options\r\n// =============================================================================\r\n\r\nexport const PERSON_ENTITY_TYPES = ['individual', 'trust', 'llc'] as const\r\nexport type PersonEntityType = typeof PERSON_ENTITY_TYPES[number]\r\n\r\nexport const PERSON_ENTITY_TYPE_OPTIONS = [\r\n { label: 'Individual', value: 'individual' },\r\n { label: 'Trust', value: 'trust' },\r\n { label: 'LLC', value: 'llc' },\r\n] as const\r\n\r\n/**\r\n * Maps person entity types to Lucide icon names\r\n */\r\nexport const PERSON_ENTITY_TYPE_ICONS: Record<PersonEntityType, string> = {\r\n individual: 'User',\r\n trust: 'Shield',\r\n llc: 'Building2',\r\n}\r\n\r\n// =============================================================================\r\n// Pet Options\r\n// =============================================================================\r\n\r\nexport const PET_SPECIES = ['dog', 'cat', 'bird', 'fish', 'reptile', 'small_mammal', 'rabbit', 'hamster', 'guinea_pig', 'other'] as const\r\nexport type PetSpecies = typeof PET_SPECIES[number]\r\n\r\nexport const PET_SPECIES_OPTIONS = [\r\n { label: 'Dog', value: 'dog' },\r\n { label: 'Cat', value: 'cat' },\r\n { label: 'Bird', value: 'bird' },\r\n { label: 'Fish', value: 'fish' },\r\n { label: 'Reptile', value: 'reptile' },\r\n { label: 'Small Mammal', value: 'small_mammal' },\r\n { label: 'Rabbit', value: 'rabbit' },\r\n { label: 'Hamster', value: 'hamster' },\r\n { label: 'Guinea Pig', value: 'guinea_pig' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const PET_SEXES = ['male', 'female', 'unknown'] as const\r\nexport type PetSex = typeof PET_SEXES[number]\r\n\r\nexport const PET_SEX_OPTIONS = [\r\n { label: 'Male', value: 'male' },\r\n { label: 'Female', value: 'female' },\r\n { label: 'Unknown', value: 'unknown' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Vehicle Options\r\n// =============================================================================\r\n\r\nexport const VEHICLE_TYPES = ['car', 'truck', 'suv', 'motorcycle', 'boat', 'rv', 'bicycle', 'other'] as const\r\nexport type VehicleType = typeof VEHICLE_TYPES[number]\r\n\r\nexport const VEHICLE_TYPE_OPTIONS = [\r\n { label: 'Car', value: 'car' },\r\n { label: 'Truck', value: 'truck' },\r\n { label: 'SUV', value: 'suv' },\r\n { label: 'Motorcycle', value: 'motorcycle' },\r\n { label: 'Boat', value: 'boat' },\r\n { label: 'RV', value: 'rv' },\r\n { label: 'Bicycle', value: 'bicycle' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const VEHICLE_STATUSES = ['owned', 'leased', 'financed', 'sold', 'other'] as const\r\nexport type VehicleStatus = typeof VEHICLE_STATUSES[number]\r\n\r\nexport const VEHICLE_STATUS_OPTIONS = [\r\n { label: 'Owned', value: 'owned' },\r\n { label: 'Leased', value: 'leased' },\r\n { label: 'Financed', value: 'financed' },\r\n { label: 'Sold', value: 'sold' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const VEHICLE_DOCUMENT_TYPES = ['registration', 'title', 'lease', 'bill_of_sale', 'loan', 'other'] as const\r\nexport type VehicleDocumentType = typeof VEHICLE_DOCUMENT_TYPES[number]\r\n\r\nexport const VEHICLE_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'Registration', value: 'registration' },\r\n { label: 'Title', value: 'title' },\r\n { label: 'Lease Agreement', value: 'lease' },\r\n { label: 'Bill of Sale', value: 'bill_of_sale' },\r\n { label: 'Loan Documents', value: 'loan' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps vehicle document types to Lucide icon names\r\n */\r\nexport const VEHICLE_DOCUMENT_TYPE_ICONS: Record<VehicleDocumentType, string> = {\r\n registration: 'FileCheck',\r\n title: 'ScrollText',\r\n lease: 'FileSignature',\r\n bill_of_sale: 'Receipt',\r\n loan: 'FileText',\r\n other: 'File',\r\n}\r\n\r\n// =============================================================================\r\n// Contact Options\r\n// =============================================================================\r\n\r\nexport const CONTACT_TYPES = [\r\n 'accountant', 'babysitter', 'car_dealership', 'chiropractor', 'contractor', 'dentist', 'doctor',\r\n 'electrician', 'emergency', 'financial_advisor', 'friend_family', 'handyman',\r\n 'home_organizer', 'house_cleaner', 'hvac_technician', 'insurance_agent', 'landscaper', 'lawyer',\r\n 'mechanic', 'notary', 'pest_control', 'pet_service', 'pharmacist', 'physical_therapist',\r\n 'plumber', 'pool_service', 'professional', 'real_estate_agent', 'service_provider',\r\n 'therapist', 'tutor', 'vendor', 'vet_clinic', 'veterinarian', 'other'\r\n] as const\r\nexport type ContactType = typeof CONTACT_TYPES[number]\r\n\r\nexport const CONTACT_TYPE_OPTIONS = [\r\n { label: 'Accountant', value: 'accountant' },\r\n { label: 'Babysitter/Nanny', value: 'babysitter' },\r\n { label: 'Car Dealership', value: 'car_dealership' },\r\n { label: 'Chiropractor', value: 'chiropractor' },\r\n { label: 'Contractor', value: 'contractor' },\r\n { label: 'Dentist', value: 'dentist' },\r\n { label: 'Doctor', value: 'doctor' },\r\n { label: 'Electrician', value: 'electrician' },\r\n { label: 'Emergency', value: 'emergency' },\r\n { label: 'Financial Advisor', value: 'financial_advisor' },\r\n { label: 'Friend/Family', value: 'friend_family' },\r\n { label: 'Handyman', value: 'handyman' },\r\n { label: 'Home Organizer', value: 'home_organizer' },\r\n { label: 'House Cleaner', value: 'house_cleaner' },\r\n { label: 'HVAC Technician', value: 'hvac_technician' },\r\n { label: 'Insurance Agent', value: 'insurance_agent' },\r\n { label: 'Landscaper/Gardener', value: 'landscaper' },\r\n { label: 'Lawyer', value: 'lawyer' },\r\n { label: 'Mechanic', value: 'mechanic' },\r\n { label: 'Notary', value: 'notary' },\r\n { label: 'Pest Control', value: 'pest_control' },\r\n { label: 'Pet Service', value: 'pet_service' },\r\n { label: 'Pharmacist', value: 'pharmacist' },\r\n { label: 'Physical Therapist', value: 'physical_therapist' },\r\n { label: 'Plumber', value: 'plumber' },\r\n { label: 'Pool Service', value: 'pool_service' },\r\n { label: 'Professional', value: 'professional' },\r\n { label: 'Real Estate Agent', value: 'real_estate_agent' },\r\n { label: 'Service Provider', value: 'service_provider' },\r\n { label: 'Therapist/Counselor', value: 'therapist' },\r\n { label: 'Tutor', value: 'tutor' },\r\n { label: 'Vendor', value: 'vendor' },\r\n { label: 'Vet Clinic/Hospital', value: 'vet_clinic' },\r\n { label: 'Veterinarian', value: 'veterinarian' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Access Code Options\r\n// =============================================================================\r\n\r\nexport const ACCESS_CODE_TYPES = ['garage', 'alarm', 'gate', 'door_lock', 'door', 'safe', 'wifi', 'elevator', 'mailbox', 'storage', 'other'] as const\r\nexport type AccessCodeType = typeof ACCESS_CODE_TYPES[number]\r\n\r\nexport const ACCESS_CODE_TYPE_OPTIONS = [\r\n { label: 'Garage', value: 'garage' },\r\n { label: 'Alarm', value: 'alarm' },\r\n { label: 'Gate', value: 'gate' },\r\n { label: 'Door Lock', value: 'door_lock' },\r\n { label: 'Door', value: 'door' },\r\n { label: 'Safe', value: 'safe' },\r\n { label: 'WiFi', value: 'wifi' },\r\n { label: 'Elevator', value: 'elevator' },\r\n { label: 'Mailbox', value: 'mailbox' },\r\n { label: 'Storage', value: 'storage' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Insurance Options\r\n// =============================================================================\r\n\r\nexport const INSURANCE_TYPES = ['home', 'auto', 'umbrella', 'life', 'health', 'pet', 'collection', 'other'] as const\r\nexport type InsuranceType = typeof INSURANCE_TYPES[number]\r\n\r\nexport const INSURANCE_TYPE_OPTIONS = [\r\n { label: 'Home', value: 'home' },\r\n { label: 'Auto', value: 'auto' },\r\n { label: 'Umbrella', value: 'umbrella' },\r\n { label: 'Life', value: 'life' },\r\n { label: 'Health', value: 'health' },\r\n { label: 'Pet', value: 'pet' },\r\n { label: 'Collection', value: 'collection' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Service Options\r\n// =============================================================================\r\n\r\nexport const SERVICE_TYPES = ['cleaning', 'gardening', 'lawn_care', 'pool', 'pest_control', 'hvac', 'plumbing', 'electrical', 'security', 'other'] as const\r\nexport type ServiceType = typeof SERVICE_TYPES[number]\r\n\r\nexport const SERVICE_TYPE_OPTIONS = [\r\n { label: 'Cleaning', value: 'cleaning' },\r\n { label: 'Gardening', value: 'gardening' },\r\n { label: 'Lawn Care', value: 'lawn_care' },\r\n { label: 'Pool', value: 'pool' },\r\n { label: 'Pest Control', value: 'pest_control' },\r\n { label: 'HVAC', value: 'hvac' },\r\n { label: 'Plumbing', value: 'plumbing' },\r\n { label: 'Electrical', value: 'electrical' },\r\n { label: 'Security', value: 'security' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Schedule/Frequency Options\r\n// =============================================================================\r\n\r\nexport const SCHEDULE_FREQUENCIES = ['daily', 'weekly', 'biweekly', 'monthly', 'bimonthly', 'quarterly', 'yearly', 'one_time'] as const\r\nexport type ScheduleFrequency = typeof SCHEDULE_FREQUENCIES[number]\r\n\r\nexport const SCHEDULE_FREQUENCY_OPTIONS = [\r\n { label: 'Daily', value: 'daily' },\r\n { label: 'Weekly', value: 'weekly' },\r\n { label: 'Biweekly', value: 'biweekly' },\r\n { label: 'Monthly', value: 'monthly' },\r\n { label: 'Every 2 Months', value: 'bimonthly' },\r\n { label: 'Quarterly', value: 'quarterly' },\r\n { label: 'Yearly', value: 'yearly' },\r\n { label: 'One-Time', value: 'one_time' },\r\n] as const\r\n\r\nexport const SCHEDULE_STATUSES = ['active', 'paused', 'completed', 'cancelled'] as const\r\nexport type ScheduleStatus = typeof SCHEDULE_STATUSES[number]\r\n\r\nexport const SCHEDULE_STATUS_OPTIONS = [\r\n { label: 'Active', value: 'active' },\r\n { label: 'Paused', value: 'paused' },\r\n { label: 'Completed', value: 'completed' },\r\n { label: 'Cancelled', value: 'cancelled' },\r\n] as const\r\n\r\nexport const DAYS_OF_WEEK = [\r\n { label: 'Sunday', value: 0 },\r\n { label: 'Monday', value: 1 },\r\n { label: 'Tuesday', value: 2 },\r\n { label: 'Wednesday', value: 3 },\r\n { label: 'Thursday', value: 4 },\r\n { label: 'Friday', value: 5 },\r\n { label: 'Saturday', value: 6 },\r\n] as const\r\n\r\n// =============================================================================\r\n// Subscription Options\r\n// =============================================================================\r\n\r\nexport const SUBSCRIPTION_TYPES = ['streaming', 'software', 'membership', 'utility', 'other'] as const\r\nexport type SubscriptionType = typeof SUBSCRIPTION_TYPES[number]\r\n\r\nexport const SUBSCRIPTION_TYPE_OPTIONS = [\r\n { label: 'Streaming', value: 'streaming' },\r\n { label: 'Software', value: 'software' },\r\n { label: 'Membership', value: 'membership' },\r\n { label: 'Utility', value: 'utility' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const BILLING_CYCLES = ['monthly', 'quarterly', 'annual'] as const\r\nexport type BillingCycle = typeof BILLING_CYCLES[number]\r\n\r\nexport const BILLING_CYCLE_OPTIONS = [\r\n { label: 'Monthly', value: 'monthly' },\r\n { label: 'Quarterly', value: 'quarterly' },\r\n { label: 'Annual', value: 'annual' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Maintenance Task Options\r\n// =============================================================================\r\n\r\nexport const MAINTENANCE_TASK_TYPES = ['scheduled', 'one_time'] as const\r\nexport type MaintenanceTaskType = typeof MAINTENANCE_TASK_TYPES[number]\r\n\r\nexport const MAINTENANCE_TASK_TYPE_OPTIONS = [\r\n { label: 'Scheduled (Recurring)', value: 'scheduled' },\r\n { label: 'One-Time', value: 'one_time' },\r\n] as const\r\n\r\nexport const MAINTENANCE_CATEGORIES = ['hvac', 'plumbing', 'electrical', 'appliance', 'appliances', 'exterior', 'interior', 'vehicle', 'safety', 'roof', 'pool', 'landscaping', 'solar', 'other'] as const\r\nexport type MaintenanceCategory = typeof MAINTENANCE_CATEGORIES[number]\r\n\r\nexport const MAINTENANCE_CATEGORY_OPTIONS = [\r\n { label: 'HVAC', value: 'hvac' },\r\n { label: 'Plumbing', value: 'plumbing' },\r\n { label: 'Electrical', value: 'electrical' },\r\n { label: 'Appliance', value: 'appliance' },\r\n { label: 'Appliances', value: 'appliances' },\r\n { label: 'Exterior', value: 'exterior' },\r\n { label: 'Interior', value: 'interior' },\r\n { label: 'Vehicle', value: 'vehicle' },\r\n { label: 'Safety', value: 'safety' },\r\n { label: 'Roof', value: 'roof' },\r\n { label: 'Pool', value: 'pool' },\r\n { label: 'Landscaping', value: 'landscaping' },\r\n { label: 'Solar', value: 'solar' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const PRIORITY_LEVELS = ['low', 'medium', 'high'] as const\r\nexport type PriorityLevel = typeof PRIORITY_LEVELS[number]\r\n\r\nexport const PRIORITY_OPTIONS = [\r\n { label: 'Low', value: 'low' },\r\n { label: 'Medium', value: 'medium' },\r\n { label: 'High', value: 'high' },\r\n] as const\r\n\r\nexport const TASK_STATUSES = ['pending', 'in_progress', 'completed'] as const\r\nexport type TaskStatus = typeof TASK_STATUSES[number]\r\n\r\nexport const TASK_STATUS_OPTIONS = [\r\n { label: 'Pending', value: 'pending' },\r\n { label: 'In Progress', value: 'in_progress' },\r\n { label: 'Completed', value: 'completed' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Password Category Options\r\n// =============================================================================\r\n\r\nexport const PASSWORD_CATEGORIES = [\r\n 'identity', 'social', 'financial', 'shopping', 'streaming', 'gaming', 'work', 'email',\r\n 'utilities', 'government', 'healthcare', 'education', 'travel', 'device', 'fitness', 'dev_tools', 'other'\r\n] as const\r\nexport type PasswordCategory = typeof PASSWORD_CATEGORIES[number]\r\n\r\nexport const PASSWORD_CATEGORY_OPTIONS = [\r\n { label: 'Identity', value: 'identity' },\r\n { label: 'Social Media', value: 'social' },\r\n { label: 'Financial', value: 'financial' },\r\n { label: 'Shopping', value: 'shopping' },\r\n { label: 'Streaming', value: 'streaming' },\r\n { label: 'Gaming', value: 'gaming' },\r\n { label: 'Work', value: 'work' },\r\n { label: 'Email', value: 'email' },\r\n { label: 'Utilities', value: 'utilities' },\r\n { label: 'Government', value: 'government' },\r\n { label: 'Healthcare', value: 'healthcare' },\r\n { label: 'Education', value: 'education' },\r\n { label: 'Travel', value: 'travel' },\r\n { label: 'Device', value: 'device' },\r\n { label: 'Fitness', value: 'fitness' },\r\n { label: 'Dev Tools', value: 'dev_tools' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps password categories to Lucide icon names\r\n */\r\nexport const PASSWORD_CATEGORY_ICONS: Record<PasswordCategory, string> = {\r\n identity: 'Fingerprint',\r\n social: 'Users',\r\n financial: 'Landmark',\r\n shopping: 'ShoppingCart',\r\n streaming: 'Play',\r\n gaming: 'Gamepad2',\r\n work: 'Briefcase',\r\n email: 'Mail',\r\n utilities: 'Plug',\r\n government: 'Building',\r\n healthcare: 'HeartPulse',\r\n education: 'GraduationCap',\r\n travel: 'Plane',\r\n device: 'Router',\r\n fitness: 'Dumbbell',\r\n dev_tools: 'Github',\r\n other: 'Key',\r\n}\r\n\r\n// =============================================================================\r\n// Digital Identity Options\r\n// =============================================================================\r\n\r\nexport const DIGITAL_IDENTITY_TYPES = ['website', 'app', 'email', 'financial', 'social', 'other'] as const\r\nexport type DigitalIdentityType = typeof DIGITAL_IDENTITY_TYPES[number]\r\n\r\nexport const DIGITAL_IDENTITY_TYPE_OPTIONS = [\r\n { label: 'Website', value: 'website' },\r\n { label: 'App', value: 'app' },\r\n { label: 'Email', value: 'email' },\r\n { label: 'Financial', value: 'financial' },\r\n { label: 'Social', value: 'social' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Icon Mappings\r\n// =============================================================================\r\n\r\n/**\r\n * Maps contact types to Lucide icon names\r\n */\r\nexport const CONTACT_TYPE_ICONS: Record<ContactType, string> = {\r\n accountant: 'Calculator',\r\n babysitter: 'Baby',\r\n car_dealership: 'Car',\r\n chiropractor: 'Bone',\r\n contractor: 'HardHat',\r\n dentist: 'Smile',\r\n doctor: 'Stethoscope',\r\n electrician: 'Zap',\r\n emergency: 'AlertTriangle',\r\n financial_advisor: 'Landmark',\r\n friend_family: 'User',\r\n handyman: 'Hammer',\r\n home_organizer: 'LayoutGrid',\r\n house_cleaner: 'Sparkles',\r\n hvac_technician: 'Thermometer',\r\n insurance_agent: 'Shield',\r\n landscaper: 'TreeDeciduous',\r\n lawyer: 'Scale',\r\n mechanic: 'Wrench',\r\n notary: 'Stamp',\r\n pest_control: 'Bug',\r\n pet_service: 'PawPrint',\r\n pharmacist: 'Pill',\r\n physical_therapist: 'Activity',\r\n plumber: 'Droplet',\r\n pool_service: 'Waves',\r\n professional: 'User',\r\n real_estate_agent: 'Home',\r\n service_provider: 'Wrench',\r\n therapist: 'Brain',\r\n tutor: 'GraduationCap',\r\n vendor: 'Building2',\r\n vet_clinic: 'Building2',\r\n veterinarian: 'Heart',\r\n other: 'UserCircle',\r\n}\r\n\r\n/**\r\n * Maps contact types to colors for UI display\r\n */\r\nexport const CONTACT_TYPE_COLORS: Record<ContactType, string> = {\r\n accountant: 'blue',\r\n babysitter: 'pink',\r\n car_dealership: 'slate',\r\n chiropractor: 'teal',\r\n contractor: 'orange',\r\n dentist: 'cyan',\r\n doctor: 'green',\r\n electrician: 'yellow',\r\n emergency: 'red',\r\n financial_advisor: 'indigo',\r\n friend_family: 'purple',\r\n handyman: 'amber',\r\n home_organizer: 'teal',\r\n house_cleaner: 'lime',\r\n hvac_technician: 'orange',\r\n insurance_agent: 'blue',\r\n landscaper: 'green',\r\n lawyer: 'slate',\r\n mechanic: 'gray',\r\n notary: 'violet',\r\n pest_control: 'amber',\r\n pet_service: 'pink',\r\n pharmacist: 'teal',\r\n physical_therapist: 'cyan',\r\n plumber: 'blue',\r\n pool_service: 'sky',\r\n professional: 'slate',\r\n real_estate_agent: 'emerald',\r\n service_provider: 'gray',\r\n therapist: 'purple',\r\n tutor: 'indigo',\r\n vendor: 'slate',\r\n vet_clinic: 'green',\r\n veterinarian: 'green',\r\n other: 'gray',\r\n}\r\n\r\n/**\r\n * Maps pet species to Lucide icon names\r\n */\r\nexport const PET_SPECIES_ICONS: Record<PetSpecies, string> = {\r\n dog: 'Dog',\r\n cat: 'Cat',\r\n bird: 'Bird',\r\n fish: 'Fish',\r\n reptile: 'Bug',\r\n small_mammal: 'Rabbit',\r\n rabbit: 'Rabbit',\r\n hamster: 'Rat',\r\n guinea_pig: 'Rat',\r\n other: 'PawPrint',\r\n}\r\n\r\n/**\r\n * Maps vehicle types to Lucide icon names\r\n */\r\nexport const VEHICLE_TYPE_ICONS: Record<VehicleType, string> = {\r\n car: 'Car',\r\n truck: 'Truck',\r\n suv: 'CarFront',\r\n motorcycle: 'Bike',\r\n boat: 'Ship',\r\n rv: 'Caravan',\r\n bicycle: 'Bike',\r\n other: 'CircleDot',\r\n}\r\n\r\n/**\r\n * Maps service types to Lucide icon names\r\n */\r\nexport const SERVICE_TYPE_ICONS: Record<ServiceType, string> = {\r\n cleaning: 'Sparkles',\r\n gardening: 'Flower2',\r\n lawn_care: 'TreeDeciduous',\r\n pool: 'Waves',\r\n pest_control: 'Bug',\r\n hvac: 'Wind',\r\n plumbing: 'Droplet',\r\n electrical: 'Zap',\r\n security: 'Shield',\r\n other: 'Wrench',\r\n}\r\n\r\n/**\r\n * Maps access code types to Lucide icon names\r\n */\r\nexport const ACCESS_CODE_TYPE_ICONS: Record<AccessCodeType, string> = {\r\n garage: 'Warehouse',\r\n alarm: 'Bell',\r\n gate: 'DoorOpen',\r\n door_lock: 'Lock',\r\n door: 'DoorClosed',\r\n safe: 'Lock',\r\n wifi: 'Wifi',\r\n elevator: 'ArrowUpDown',\r\n mailbox: 'Mail',\r\n storage: 'Archive',\r\n other: 'Key',\r\n}\r\n\r\n/**\r\n * Maps insurance types to Lucide icon names\r\n */\r\nexport const INSURANCE_TYPE_ICONS: Record<InsuranceType, string> = {\r\n home: 'Home',\r\n auto: 'Car',\r\n umbrella: 'Umbrella',\r\n life: 'Heart',\r\n health: 'Activity',\r\n pet: 'PawPrint',\r\n collection: 'Gem',\r\n other: 'Shield',\r\n}\r\n\r\n/**\r\n * Maps subscription types to Lucide icon names\r\n */\r\nexport const SUBSCRIPTION_TYPE_ICONS: Record<SubscriptionType, string> = {\r\n streaming: 'Play',\r\n software: 'Laptop',\r\n membership: 'CreditCard',\r\n utility: 'Plug',\r\n other: 'Receipt',\r\n}\r\n\r\n/**\r\n * Maps maintenance categories to Lucide icon names\r\n */\r\nexport const MAINTENANCE_CATEGORY_ICONS: Record<MaintenanceCategory, string> = {\r\n hvac: 'Wind',\r\n plumbing: 'Droplet',\r\n electrical: 'Zap',\r\n appliance: 'Microwave',\r\n appliances: 'Microwave',\r\n exterior: 'TreeDeciduous',\r\n interior: 'Sofa',\r\n vehicle: 'Car',\r\n safety: 'ShieldCheck',\r\n roof: 'Home',\r\n pool: 'Waves',\r\n landscaping: 'Leaf',\r\n solar: 'Sun',\r\n other: 'Wrench',\r\n}\r\n\r\n/**\r\n * Maps digital identity types to Lucide icon names\r\n */\r\nexport const DIGITAL_IDENTITY_TYPE_ICONS: Record<DigitalIdentityType, string> = {\r\n website: 'Globe',\r\n app: 'Smartphone',\r\n email: 'Mail',\r\n financial: 'Landmark',\r\n social: 'Users',\r\n other: 'Key',\r\n}\r\n\r\n// =============================================================================\r\n// Financial Account Options\r\n// =============================================================================\r\n\r\nexport const FINANCIAL_ACCOUNT_TYPES = ['bank', 'credit_card', 'mortgage', 'loan', 'investment', 'retirement', 'insurance', 'crypto', 'hsa', 'education_529', 'custodial', 'daf', 'fund', 'other'] as const\r\nexport type FinancialAccountType = typeof FINANCIAL_ACCOUNT_TYPES[number]\r\n\r\nexport const FINANCIAL_ACCOUNT_TYPE_OPTIONS = [\r\n { label: 'Bank Account', value: 'bank' },\r\n { label: 'Credit Card', value: 'credit_card' },\r\n { label: 'Mortgage', value: 'mortgage' },\r\n { label: 'Loan', value: 'loan' },\r\n { label: 'Investment', value: 'investment' },\r\n { label: 'Retirement', value: 'retirement' },\r\n { label: 'Insurance', value: 'insurance' },\r\n { label: 'Crypto', value: 'crypto' },\r\n { label: 'Health Savings (HSA)', value: 'hsa' },\r\n { label: '529 Education Savings', value: 'education_529' },\r\n { label: 'Custodial (UGMA/UTMA)', value: 'custodial' },\r\n { label: 'Donor-Advised Fund (DAF)', value: 'daf' },\r\n { label: 'Fund (LP/PE/VC)', value: 'fund' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// Crypto wallet types (for crypto accounts)\r\nexport const CRYPTO_WALLET_TYPES = ['hardware', 'software', 'exchange', 'paper'] as const\r\nexport type CryptoWalletType = typeof CRYPTO_WALLET_TYPES[number]\r\n\r\nexport const CRYPTO_WALLET_TYPE_OPTIONS = [\r\n { label: 'Hardware Wallet', value: 'hardware' },\r\n { label: 'Software Wallet', value: 'software' },\r\n { label: 'Exchange', value: 'exchange' },\r\n { label: 'Paper Wallet', value: 'paper' },\r\n] as const\r\n\r\n/**\r\n * Maps crypto wallet types to Lucide icon names\r\n */\r\nexport const CRYPTO_WALLET_TYPE_ICONS: Record<CryptoWalletType, string> = {\r\n hardware: 'HardDrive',\r\n software: 'Smartphone',\r\n exchange: 'Building2',\r\n paper: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Legal Document Options\r\n// =============================================================================\r\n\r\n// Personal legal document types (for individuals)\r\nexport const PERSONAL_LEGAL_DOCUMENT_TYPES = [\r\n 'birth_certificate', 'citizenship_certificate', 'divorce_decree',\r\n 'healthcare_directive', 'living_will', 'marriage_certificate',\r\n 'power_of_attorney', 'social_security_card', 'will',\r\n // Shared types\r\n 'property_deed', 'vehicle_title', 'other'\r\n] as const\r\n\r\n// Business/LLC legal document types\r\nexport const LLC_LEGAL_DOCUMENT_TYPES = [\r\n 'llc_formation', 'operating_agreement', 'annual_report',\r\n 'sales_tax_registration', 'business_license', 'registered_agent',\r\n 'dba_certificate', 'business_insurance_certificate',\r\n // Shared types\r\n 'ein_certificate', 'property_deed', 'vehicle_title', 'other'\r\n] as const\r\n\r\n// Trust legal document types\r\nexport const TRUST_LEGAL_DOCUMENT_TYPES = [\r\n 'trust_agreement', 'certificate_of_trust', 'trust_amendment',\r\n 'schedule_of_assets', 'trustee_acceptance', 'trustee_resignation',\r\n 'pour_over_will', 'trust_restatement', 'beneficiary_designation',\r\n // Shared types\r\n 'ein_certificate', 'property_deed', 'vehicle_title', 'other'\r\n] as const\r\n\r\n// Combined type for all legal documents\r\nexport const LEGAL_DOCUMENT_TYPES = [\r\n // Personal\r\n 'birth_certificate', 'citizenship_certificate', 'divorce_decree',\r\n 'healthcare_directive', 'living_will', 'marriage_certificate',\r\n 'power_of_attorney', 'social_security_card', 'will',\r\n // LLC/Business\r\n 'llc_formation', 'operating_agreement', 'annual_report',\r\n 'sales_tax_registration', 'business_license', 'registered_agent',\r\n 'dba_certificate', 'business_insurance_certificate',\r\n // Trust\r\n 'trust_agreement', 'certificate_of_trust', 'trust_amendment',\r\n 'schedule_of_assets', 'trustee_acceptance', 'trustee_resignation',\r\n 'pour_over_will', 'trust_restatement', 'beneficiary_designation',\r\n // Shared\r\n 'ein_certificate', 'property_deed', 'vehicle_title', 'other',\r\n // Legacy (kept for backwards compatibility)\r\n 'trust',\r\n] as const\r\nexport type LegalDocumentType = typeof LEGAL_DOCUMENT_TYPES[number]\r\n\r\nexport const PERSONAL_LEGAL_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'Birth Certificate', value: 'birth_certificate' },\r\n { label: 'Citizenship Certificate', value: 'citizenship_certificate' },\r\n { label: 'Divorce Decree', value: 'divorce_decree' },\r\n { label: 'Healthcare Directive', value: 'healthcare_directive' },\r\n { label: 'Living Will', value: 'living_will' },\r\n { label: 'Marriage Certificate', value: 'marriage_certificate' },\r\n { label: 'Power of Attorney', value: 'power_of_attorney' },\r\n { label: 'Social Security Card', value: 'social_security_card' },\r\n { label: 'Will', value: 'will' },\r\n { label: 'Property Deed', value: 'property_deed' },\r\n { label: 'Vehicle Title', value: 'vehicle_title' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const LLC_LEGAL_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'LLC Formation Certificate', value: 'llc_formation' },\r\n { label: 'Operating Agreement', value: 'operating_agreement' },\r\n { label: 'Annual Report', value: 'annual_report' },\r\n { label: 'Sales Tax Registration', value: 'sales_tax_registration' },\r\n { label: 'Business License', value: 'business_license' },\r\n { label: 'Registered Agent Certificate', value: 'registered_agent' },\r\n { label: 'DBA Certificate', value: 'dba_certificate' },\r\n { label: 'Business Insurance Certificate', value: 'business_insurance_certificate' },\r\n { label: 'EIN Certificate', value: 'ein_certificate' },\r\n { label: 'Property Deed', value: 'property_deed' },\r\n { label: 'Vehicle Title', value: 'vehicle_title' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const TRUST_LEGAL_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'Trust Agreement', value: 'trust_agreement' },\r\n { label: 'Certificate of Trust', value: 'certificate_of_trust' },\r\n { label: 'Trust Amendment', value: 'trust_amendment' },\r\n { label: 'Schedule of Assets', value: 'schedule_of_assets' },\r\n { label: 'Trustee Acceptance', value: 'trustee_acceptance' },\r\n { label: 'Trustee Resignation', value: 'trustee_resignation' },\r\n { label: 'Pour-Over Will', value: 'pour_over_will' },\r\n { label: 'Trust Restatement', value: 'trust_restatement' },\r\n { label: 'Beneficiary Designation', value: 'beneficiary_designation' },\r\n { label: 'EIN Certificate', value: 'ein_certificate' },\r\n { label: 'Property Deed', value: 'property_deed' },\r\n { label: 'Vehicle Title', value: 'vehicle_title' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// Alias for backwards compatibility\r\nexport const BUSINESS_LEGAL_DOCUMENT_TYPE_OPTIONS = LLC_LEGAL_DOCUMENT_TYPE_OPTIONS\r\n\r\n// Combined for backwards compatibility\r\nexport const LEGAL_DOCUMENT_TYPE_OPTIONS = [\r\n ...PERSONAL_LEGAL_DOCUMENT_TYPE_OPTIONS.filter(o => o.value !== 'other'),\r\n ...LLC_LEGAL_DOCUMENT_TYPE_OPTIONS.filter(o => !['property_deed', 'vehicle_title', 'other', 'ein_certificate'].includes(o.value)),\r\n ...TRUST_LEGAL_DOCUMENT_TYPE_OPTIONS.filter(o => !['property_deed', 'vehicle_title', 'other', 'ein_certificate'].includes(o.value)),\r\n { label: 'EIN Certificate', value: 'ein_certificate' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps legal document types to Lucide icon names\r\n */\r\nexport const LEGAL_DOCUMENT_TYPE_ICONS: Record<LegalDocumentType, string> = {\r\n // Personal documents\r\n birth_certificate: 'Baby',\r\n citizenship_certificate: 'Flag',\r\n divorce_decree: 'FileX',\r\n healthcare_directive: 'Stethoscope',\r\n living_will: 'Heart',\r\n marriage_certificate: 'Heart',\r\n power_of_attorney: 'FileSignature',\r\n social_security_card: 'CreditCard',\r\n will: 'ScrollText',\r\n // LLC/Business documents\r\n llc_formation: 'FileCheck',\r\n operating_agreement: 'FileText',\r\n annual_report: 'FileSpreadsheet',\r\n sales_tax_registration: 'Receipt',\r\n business_license: 'BadgeCheck',\r\n registered_agent: 'UserCheck',\r\n dba_certificate: 'FileSignature',\r\n business_insurance_certificate: 'Shield',\r\n // Trust documents\r\n trust_agreement: 'ScrollText',\r\n certificate_of_trust: 'FileCheck',\r\n trust_amendment: 'FilePen',\r\n schedule_of_assets: 'ClipboardList',\r\n trustee_acceptance: 'UserCheck',\r\n trustee_resignation: 'UserMinus',\r\n pour_over_will: 'ScrollText',\r\n trust_restatement: 'FileText',\r\n beneficiary_designation: 'Users',\r\n // Shared documents\r\n ein_certificate: 'Building2',\r\n property_deed: 'Home',\r\n vehicle_title: 'Car',\r\n other: 'FileText',\r\n // Legacy\r\n trust: 'Briefcase',\r\n}\r\n\r\n/**\r\n * Maps financial account types to Lucide icon names\r\n */\r\nexport const FINANCIAL_ACCOUNT_TYPE_ICONS: Record<FinancialAccountType, string> = {\r\n bank: 'Landmark',\r\n credit_card: 'CreditCard',\r\n investment: 'TrendingUp',\r\n retirement: 'PiggyBank',\r\n hsa: 'HeartPulse',\r\n education_529: 'GraduationCap',\r\n custodial: 'Baby',\r\n daf: 'HeartHandshake',\r\n fund: 'Briefcase',\r\n loan: 'Banknote',\r\n mortgage: 'Home',\r\n insurance: 'Shield',\r\n crypto: 'Bitcoin',\r\n other: 'Wallet',\r\n}\r\n\r\n// =============================================================================\r\n// Valuable Options\r\n// =============================================================================\r\n\r\nexport const VALUABLE_CATEGORIES = ['art_other', 'collectible', 'electronics', 'jewelry', 'major_appliance', 'painting', 'photography_equipment', 'sculpture', 'watch', 'other'] as const\r\nexport type ValuableCategory = typeof VALUABLE_CATEGORIES[number]\r\n\r\nexport const VALUABLE_CATEGORY_OPTIONS = [\r\n { label: 'Art', value: 'art_other' },\r\n { label: 'Collectible', value: 'collectible' },\r\n { label: 'Electronics', value: 'electronics' },\r\n { label: 'Jewelry', value: 'jewelry' },\r\n { label: 'Major Appliance', value: 'major_appliance' },\r\n { label: 'Painting', value: 'painting' },\r\n { label: 'Photography Equipment', value: 'photography_equipment' },\r\n { label: 'Sculpture', value: 'sculpture' },\r\n { label: 'Watch', value: 'watch' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const VALUABLE_DOCUMENT_TYPES = ['photo', 'certificate_of_authenticity', 'receipt', 'appraisal'] as const\r\nexport type ValuableDocumentType = typeof VALUABLE_DOCUMENT_TYPES[number]\r\n\r\nexport const VALUABLE_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'Photo', value: 'photo' },\r\n { label: 'Certificate of Authenticity', value: 'certificate_of_authenticity' },\r\n { label: 'Receipt', value: 'receipt' },\r\n { label: 'Appraisal', value: 'appraisal' },\r\n] as const\r\n\r\n/**\r\n * Maps valuable categories to Lucide icon names\r\n */\r\nexport const VALUABLE_CATEGORY_ICONS: Record<ValuableCategory, string> = {\r\n art_other: 'Palette',\r\n collectible: 'Trophy',\r\n electronics: 'Smartphone',\r\n jewelry: 'Gem',\r\n major_appliance: 'Refrigerator',\r\n painting: 'Frame',\r\n photography_equipment: 'Camera',\r\n sculpture: 'Cuboid',\r\n watch: 'Watch',\r\n other: 'Package',\r\n}\r\n\r\n/**\r\n * Maps valuable document types to Lucide icon names\r\n */\r\nexport const VALUABLE_DOCUMENT_TYPE_ICONS: Record<ValuableDocumentType, string> = {\r\n photo: 'Image',\r\n certificate_of_authenticity: 'Award',\r\n receipt: 'Receipt',\r\n appraisal: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Device Options\r\n// =============================================================================\r\n\r\nexport const DEVICE_TYPES = ['router', 'camera', 'smart_light', 'thermostat', 'doorbell', 'lock', 'speaker', 'tv', 'hub', 'sensor', 'appliance', 'other'] as const\r\nexport type DeviceType = typeof DEVICE_TYPES[number]\r\n\r\nexport const DEVICE_TYPE_OPTIONS = [\r\n { label: 'Router/Network', value: 'router' },\r\n { label: 'Camera', value: 'camera' },\r\n { label: 'Smart Light', value: 'smart_light' },\r\n { label: 'Thermostat', value: 'thermostat' },\r\n { label: 'Doorbell', value: 'doorbell' },\r\n { label: 'Smart Lock', value: 'lock' },\r\n { label: 'Speaker/Assistant', value: 'speaker' },\r\n { label: 'Smart TV', value: 'tv' },\r\n { label: 'Hub/Bridge', value: 'hub' },\r\n { label: 'Sensor', value: 'sensor' },\r\n { label: 'Smart Appliance', value: 'appliance' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps device types to Lucide icon names\r\n */\r\nexport const DEVICE_TYPE_ICONS: Record<DeviceType, string> = {\r\n router: 'Wifi',\r\n camera: 'Camera',\r\n smart_light: 'Lightbulb',\r\n thermostat: 'Thermometer',\r\n doorbell: 'BellRing',\r\n lock: 'Lock',\r\n speaker: 'Speaker',\r\n tv: 'Tv',\r\n hub: 'CircuitBoard',\r\n sensor: 'Radio',\r\n appliance: 'Refrigerator',\r\n other: 'Smartphone',\r\n}\r\n\r\n// =============================================================================\r\n// Tax Options\r\n// =============================================================================\r\n\r\nexport const TAX_COUNTRIES = ['USA', 'CAN'] as const\r\nexport type TaxCountry = typeof TAX_COUNTRIES[number]\r\n\r\nexport const TAX_COUNTRY_OPTIONS = [\r\n { label: 'United States', value: 'USA' },\r\n { label: 'Canada', value: 'CAN' },\r\n] as const\r\n\r\n// USA Tax Document Types\r\nexport const USA_TAX_DOCUMENT_TYPES = ['w2', '1099', '1040', 'k1', 'other'] as const\r\nexport type UsaTaxDocumentType = typeof USA_TAX_DOCUMENT_TYPES[number]\r\n\r\nexport const USA_TAX_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'W-2', value: 'w2' },\r\n { label: '1099', value: '1099' },\r\n { label: '1040', value: '1040' },\r\n { label: 'K-1', value: 'k1' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// Canada Tax Document Types\r\nexport const CAN_TAX_DOCUMENT_TYPES = ['t4', 't5', 't3', 't1', 'other'] as const\r\nexport type CanTaxDocumentType = typeof CAN_TAX_DOCUMENT_TYPES[number]\r\n\r\nexport const CAN_TAX_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'T4 (Employment Income)', value: 't4' },\r\n { label: 'T5 (Investment Income)', value: 't5' },\r\n { label: 'T3 (Trust Income)', value: 't3' },\r\n { label: 'T1 (Tax Return)', value: 't1' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport type TaxDocumentType = UsaTaxDocumentType | CanTaxDocumentType\r\n\r\n/**\r\n * Maps tax document types to Lucide icon names\r\n */\r\nexport const TAX_DOCUMENT_TYPE_ICONS: Record<TaxDocumentType, string> = {\r\n // USA\r\n w2: 'FileText',\r\n '1099': 'FileText',\r\n '1040': 'FileCheck',\r\n k1: 'FileSpreadsheet',\r\n // Canada\r\n t4: 'FileText',\r\n t5: 'FileText',\r\n t3: 'FileSpreadsheet',\r\n t1: 'FileCheck',\r\n // Shared\r\n other: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Home Improvement Options\r\n// =============================================================================\r\n\r\nexport const HOME_IMPROVEMENT_TYPES = [\r\n 'roof', 'electrical', 'plumbing', 'hvac', 'water_heater', 'windows', 'doors',\r\n 'flooring', 'painting', 'siding', 'foundation', 'insulation', 'landscaping',\r\n 'fencing', 'pool', 'deck_patio', 'garage', 'bathroom', 'kitchen', 'basement', 'addition',\r\n 'solar', 'security', 'smart_home', 'other'\r\n] as const\r\nexport type HomeImprovementType = typeof HOME_IMPROVEMENT_TYPES[number]\r\n\r\nexport const HOME_IMPROVEMENT_TYPE_OPTIONS = [\r\n { label: 'Roof', value: 'roof' },\r\n { label: 'Electrical', value: 'electrical' },\r\n { label: 'Plumbing', value: 'plumbing' },\r\n { label: 'HVAC', value: 'hvac' },\r\n { label: 'Water Heater', value: 'water_heater' },\r\n { label: 'Windows', value: 'windows' },\r\n { label: 'Doors', value: 'doors' },\r\n { label: 'Flooring', value: 'flooring' },\r\n { label: 'Painting', value: 'painting' },\r\n { label: 'Siding', value: 'siding' },\r\n { label: 'Foundation', value: 'foundation' },\r\n { label: 'Insulation', value: 'insulation' },\r\n { label: 'Landscaping', value: 'landscaping' },\r\n { label: 'Fencing', value: 'fencing' },\r\n { label: 'Pool', value: 'pool' },\r\n { label: 'Deck/Patio', value: 'deck_patio' },\r\n { label: 'Garage', value: 'garage' },\r\n { label: 'Bathroom Remodel', value: 'bathroom' },\r\n { label: 'Kitchen Remodel', value: 'kitchen' },\r\n { label: 'Basement', value: 'basement' },\r\n { label: 'Addition', value: 'addition' },\r\n { label: 'Solar', value: 'solar' },\r\n { label: 'Security System', value: 'security' },\r\n { label: 'Smart Home', value: 'smart_home' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps home improvement types to Lucide icon names\r\n */\r\nexport const HOME_IMPROVEMENT_TYPE_ICONS: Record<HomeImprovementType, string> = {\r\n roof: 'Home',\r\n electrical: 'Zap',\r\n plumbing: 'Droplet',\r\n hvac: 'Wind',\r\n water_heater: 'Flame',\r\n windows: 'LayoutGrid',\r\n doors: 'DoorOpen',\r\n flooring: 'Grid3x3',\r\n painting: 'PaintBucket',\r\n siding: 'PanelTop',\r\n foundation: 'Layers',\r\n insulation: 'Thermometer',\r\n landscaping: 'TreeDeciduous',\r\n fencing: 'Fence',\r\n pool: 'Waves',\r\n deck_patio: 'Fence',\r\n garage: 'Warehouse',\r\n bathroom: 'Bath',\r\n kitchen: 'ChefHat',\r\n basement: 'ArrowDown',\r\n addition: 'Plus',\r\n solar: 'Sun',\r\n security: 'Shield',\r\n smart_home: 'Wifi',\r\n other: 'Hammer',\r\n}\r\n\r\nexport const HOME_IMPROVEMENT_DOCUMENT_TYPES = ['permit', 'inspection', 'warranty', 'invoice', 'contract', 'photo', 'other'] as const\r\nexport type HomeImprovementDocumentType = typeof HOME_IMPROVEMENT_DOCUMENT_TYPES[number]\r\n\r\nexport const HOME_IMPROVEMENT_DOCUMENT_TYPE_OPTIONS = [\r\n { label: 'Permit', value: 'permit' },\r\n { label: 'Inspection', value: 'inspection' },\r\n { label: 'Warranty', value: 'warranty' },\r\n { label: 'Invoice', value: 'invoice' },\r\n { label: 'Contract', value: 'contract' },\r\n { label: 'Photo', value: 'photo' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps home improvement document types to Lucide icon names\r\n */\r\nexport const HOME_IMPROVEMENT_DOCUMENT_TYPE_ICONS: Record<HomeImprovementDocumentType, string> = {\r\n permit: 'FileCheck',\r\n inspection: 'ClipboardCheck',\r\n warranty: 'Shield',\r\n invoice: 'Receipt',\r\n contract: 'FileText',\r\n photo: 'Image',\r\n other: 'File',\r\n}\r\n\r\n// =============================================================================\r\n// Vehicle Maintenance Options\r\n// =============================================================================\r\n\r\nexport const VEHICLE_MAINTENANCE_TYPES = [\r\n 'oil_change', 'tire_rotation', 'tire_replacement', 'brake_pads', 'brake_rotors',\r\n 'battery', 'transmission', 'coolant', 'air_filter', 'cabin_filter', 'spark_plugs',\r\n 'timing_belt', 'suspension', 'alignment', 'exhaust', 'electrical', 'body_work',\r\n 'windshield', 'inspection', 'emissions', 'recall', 'warranty_repair',\r\n 'accident_repair', 'detailing', 'other'\r\n] as const\r\nexport type VehicleMaintenanceType = typeof VEHICLE_MAINTENANCE_TYPES[number]\r\n\r\nexport const VEHICLE_MAINTENANCE_TYPE_OPTIONS = [\r\n { label: 'Oil Change', value: 'oil_change' },\r\n { label: 'Tire Rotation', value: 'tire_rotation' },\r\n { label: 'Tire Replacement', value: 'tire_replacement' },\r\n { label: 'Brake Pads', value: 'brake_pads' },\r\n { label: 'Brake Rotors', value: 'brake_rotors' },\r\n { label: 'Battery', value: 'battery' },\r\n { label: 'Transmission Service', value: 'transmission' },\r\n { label: 'Coolant Flush', value: 'coolant' },\r\n { label: 'Air Filter', value: 'air_filter' },\r\n { label: 'Cabin Air Filter', value: 'cabin_filter' },\r\n { label: 'Spark Plugs', value: 'spark_plugs' },\r\n { label: 'Timing Belt', value: 'timing_belt' },\r\n { label: 'Suspension', value: 'suspension' },\r\n { label: 'Wheel Alignment', value: 'alignment' },\r\n { label: 'Exhaust', value: 'exhaust' },\r\n { label: 'Electrical', value: 'electrical' },\r\n { label: 'Body Work', value: 'body_work' },\r\n { label: 'Windshield', value: 'windshield' },\r\n { label: 'State Inspection', value: 'inspection' },\r\n { label: 'Emissions Test', value: 'emissions' },\r\n { label: 'Recall Service', value: 'recall' },\r\n { label: 'Warranty Repair', value: 'warranty_repair' },\r\n { label: 'Accident Repair', value: 'accident_repair' },\r\n { label: 'Detailing', value: 'detailing' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps vehicle maintenance types to Lucide icon names\r\n */\r\nexport const VEHICLE_MAINTENANCE_TYPE_ICONS: Record<VehicleMaintenanceType, string> = {\r\n oil_change: 'Droplet',\r\n tire_rotation: 'CircleDot',\r\n tire_replacement: 'CircleDot',\r\n brake_pads: 'CircleSlash',\r\n brake_rotors: 'CircleSlash',\r\n battery: 'Battery',\r\n transmission: 'Cog',\r\n coolant: 'Thermometer',\r\n air_filter: 'Wind',\r\n cabin_filter: 'Wind',\r\n spark_plugs: 'Zap',\r\n timing_belt: 'Timer',\r\n suspension: 'ArrowUpDown',\r\n alignment: 'Crosshair',\r\n exhaust: 'CloudCog',\r\n electrical: 'Zap',\r\n body_work: 'Car',\r\n windshield: 'LayoutGrid',\r\n inspection: 'ClipboardCheck',\r\n emissions: 'CloudCog',\r\n recall: 'AlertTriangle',\r\n warranty_repair: 'Shield',\r\n accident_repair: 'AlertOctagon',\r\n detailing: 'Sparkles',\r\n other: 'Wrench',\r\n}\r\n\r\n// =============================================================================\r\n// Health Record Options\r\n// =============================================================================\r\n\r\nexport const HEALTH_RECORD_TYPES_PERSON = [\r\n 'vaccination', 'checkup', 'physical', 'dental', 'vision', 'procedure', 'surgery',\r\n 'lab_work', 'imaging', 'prescription', 'therapy', 'mental_health', 'specialist',\r\n 'emergency', 'hospitalization', 'allergy_test', 'screening', 'other'\r\n] as const\r\nexport type HealthRecordTypePerson = typeof HEALTH_RECORD_TYPES_PERSON[number]\r\n\r\nexport const HEALTH_RECORD_TYPE_PERSON_OPTIONS = [\r\n { label: 'Vaccination', value: 'vaccination' },\r\n { label: 'Checkup', value: 'checkup' },\r\n { label: 'Physical Exam', value: 'physical' },\r\n { label: 'Dental', value: 'dental' },\r\n { label: 'Vision/Eye Exam', value: 'vision' },\r\n { label: 'Procedure', value: 'procedure' },\r\n { label: 'Surgery', value: 'surgery' },\r\n { label: 'Lab Work', value: 'lab_work' },\r\n { label: 'Imaging', value: 'imaging' },\r\n { label: 'Prescription', value: 'prescription' },\r\n { label: 'Physical/Occupational Therapy', value: 'therapy' },\r\n { label: 'Mental Health', value: 'mental_health' },\r\n { label: 'Specialist Visit', value: 'specialist' },\r\n { label: 'Emergency/Urgent Care', value: 'emergency' },\r\n { label: 'Hospitalization', value: 'hospitalization' },\r\n { label: 'Allergy Test', value: 'allergy_test' },\r\n { label: 'Screening', value: 'screening' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const HEALTH_RECORD_TYPES_PET = [\r\n 'vaccination', 'checkup', 'dental', 'surgery', 'grooming', 'microchip',\r\n 'spay_neuter', 'lab_work', 'imaging', 'prescription', 'emergency', 'specialist',\r\n 'parasite_prevention', 'boarding_exam', 'other'\r\n] as const\r\nexport type HealthRecordTypePet = typeof HEALTH_RECORD_TYPES_PET[number]\r\n\r\nexport const HEALTH_RECORD_TYPE_PET_OPTIONS = [\r\n { label: 'Vaccination', value: 'vaccination' },\r\n { label: 'Wellness Exam', value: 'checkup' },\r\n { label: 'Dental Cleaning', value: 'dental' },\r\n { label: 'Surgery', value: 'surgery' },\r\n { label: 'Grooming', value: 'grooming' },\r\n { label: 'Microchip', value: 'microchip' },\r\n { label: 'Spay/Neuter', value: 'spay_neuter' },\r\n { label: 'Lab Work', value: 'lab_work' },\r\n { label: 'Imaging', value: 'imaging' },\r\n { label: 'Prescription', value: 'prescription' },\r\n { label: 'Emergency', value: 'emergency' },\r\n { label: 'Specialist', value: 'specialist' },\r\n { label: 'Parasite Prevention', value: 'parasite_prevention' },\r\n { label: 'Boarding Exam', value: 'boarding_exam' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport type HealthRecordType = HealthRecordTypePerson | HealthRecordTypePet\r\n\r\n/**\r\n * Maps health record types to Lucide icon names\r\n */\r\nexport const HEALTH_RECORD_TYPE_ICONS: Record<HealthRecordType, string> = {\r\n vaccination: 'Syringe',\r\n checkup: 'Stethoscope',\r\n physical: 'Activity',\r\n dental: 'Smile',\r\n vision: 'Eye',\r\n procedure: 'Scissors',\r\n surgery: 'Scissors',\r\n lab_work: 'TestTube',\r\n imaging: 'Scan',\r\n prescription: 'Pill',\r\n therapy: 'Activity',\r\n mental_health: 'Brain',\r\n specialist: 'UserCog',\r\n emergency: 'AlertTriangle',\r\n hospitalization: 'Building2',\r\n allergy_test: 'Flower2',\r\n screening: 'Search',\r\n grooming: 'Scissors',\r\n microchip: 'Cpu',\r\n spay_neuter: 'Scissors',\r\n parasite_prevention: 'Bug',\r\n boarding_exam: 'ClipboardCheck',\r\n other: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Pet Procedure Options (for multi-procedure vet visits)\r\n// =============================================================================\r\n\r\n/**\r\n * Pet procedure types for multi-procedure vet visits\r\n * Parallel to VehicleMaintenanceType for service invoices\r\n */\r\nexport const PET_PROCEDURE_TYPES = [\r\n 'vaccination', 'checkup', 'dental', 'surgery', 'grooming', 'microchip',\r\n 'spay_neuter', 'lab_work', 'imaging', 'prescription', 'emergency',\r\n 'specialist', 'parasite_prevention', 'boarding_exam', 'euthanasia', 'other'\r\n] as const\r\n\r\nexport const PET_PROCEDURE_TYPE_OPTIONS = [\r\n { label: 'Vaccination', value: 'vaccination' },\r\n { label: 'Wellness Exam', value: 'checkup' },\r\n { label: 'Dental Cleaning', value: 'dental' },\r\n { label: 'Surgery', value: 'surgery' },\r\n { label: 'Grooming', value: 'grooming' },\r\n { label: 'Microchip', value: 'microchip' },\r\n { label: 'Spay/Neuter', value: 'spay_neuter' },\r\n { label: 'Lab Work', value: 'lab_work' },\r\n { label: 'Imaging/X-Ray', value: 'imaging' },\r\n { label: 'Prescription', value: 'prescription' },\r\n { label: 'Emergency', value: 'emergency' },\r\n { label: 'Specialist Visit', value: 'specialist' },\r\n { label: 'Parasite Prevention', value: 'parasite_prevention' },\r\n { label: 'Boarding Exam', value: 'boarding_exam' },\r\n { label: 'Euthanasia', value: 'euthanasia' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n/**\r\n * Maps pet procedure types to Lucide icon names\r\n */\r\nexport const PET_PROCEDURE_TYPE_ICONS: Record<string, string> = {\r\n vaccination: 'Syringe',\r\n checkup: 'Stethoscope',\r\n dental: 'Smile',\r\n surgery: 'Scissors',\r\n grooming: 'Scissors',\r\n microchip: 'Cpu',\r\n spay_neuter: 'Scissors',\r\n lab_work: 'TestTube',\r\n imaging: 'Scan',\r\n prescription: 'Pill',\r\n emergency: 'AlertTriangle',\r\n specialist: 'UserCog',\r\n parasite_prevention: 'Bug',\r\n boarding_exam: 'ClipboardCheck',\r\n euthanasia: 'Heart',\r\n other: 'FileText',\r\n}\r\n\r\n/**\r\n * Valid icon background colors for UI display\r\n * Used by UniversalListItem and other components\r\n */\r\nexport type IconBgColor = 'blue' | 'green' | 'purple' | 'red' | 'orange' | 'gray' | 'yellow' | 'pink' | 'cyan' | 'teal' | 'indigo' | 'violet' | 'amber' | 'lime' | 'slate' | 'emerald' | 'sky' | 'zinc'\r\n\r\n/**\r\n * Maps pet procedure types to colors for UI display\r\n */\r\nexport const PET_PROCEDURE_TYPE_COLORS: Record<string, IconBgColor> = {\r\n vaccination: 'blue',\r\n checkup: 'green',\r\n dental: 'cyan',\r\n surgery: 'red',\r\n grooming: 'pink',\r\n microchip: 'purple',\r\n spay_neuter: 'orange',\r\n lab_work: 'indigo',\r\n imaging: 'slate',\r\n prescription: 'teal',\r\n emergency: 'red',\r\n specialist: 'violet',\r\n parasite_prevention: 'amber',\r\n boarding_exam: 'lime',\r\n euthanasia: 'gray',\r\n other: 'gray',\r\n}\r\n\r\n// =============================================================================\r\n// Military Record Options\r\n// =============================================================================\r\n\r\nexport const MILITARY_RECORD_TYPES = [\r\n 'dd214',\r\n 'service_record',\r\n 'discharge_papers',\r\n 'enlistment_contract',\r\n 'promotion_orders',\r\n 'awards_decorations',\r\n 'medical_records',\r\n 'training_certificate',\r\n 'other',\r\n] as const\r\nexport type MilitaryRecordType = typeof MILITARY_RECORD_TYPES[number]\r\n\r\nexport const MILITARY_RECORD_TYPE_OPTIONS = [\r\n { label: 'DD-214 (Discharge)', value: 'dd214' },\r\n { label: 'Service Record', value: 'service_record' },\r\n { label: 'Discharge Papers', value: 'discharge_papers' },\r\n { label: 'Enlistment Contract', value: 'enlistment_contract' },\r\n { label: 'Promotion Orders', value: 'promotion_orders' },\r\n { label: 'Awards & Decorations', value: 'awards_decorations' },\r\n { label: 'Medical Records', value: 'medical_records' },\r\n { label: 'Training Certificate', value: 'training_certificate' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const MILITARY_BRANCHES = [\r\n 'army',\r\n 'navy',\r\n 'air_force',\r\n 'marines',\r\n 'coast_guard',\r\n 'space_force',\r\n 'national_guard',\r\n 'reserves',\r\n 'other',\r\n] as const\r\nexport type MilitaryBranch = typeof MILITARY_BRANCHES[number]\r\n\r\nexport const MILITARY_BRANCH_OPTIONS = [\r\n { label: 'Army', value: 'army' },\r\n { label: 'Navy', value: 'navy' },\r\n { label: 'Air Force', value: 'air_force' },\r\n { label: 'Marines', value: 'marines' },\r\n { label: 'Coast Guard', value: 'coast_guard' },\r\n { label: 'Space Force', value: 'space_force' },\r\n { label: 'National Guard', value: 'national_guard' },\r\n { label: 'Reserves', value: 'reserves' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const MILITARY_RECORD_TYPE_ICONS: Record<MilitaryRecordType, string> = {\r\n dd214: 'FileText',\r\n service_record: 'ClipboardList',\r\n discharge_papers: 'FileSignature',\r\n enlistment_contract: 'FileText',\r\n promotion_orders: 'Award',\r\n awards_decorations: 'Medal',\r\n medical_records: 'Stethoscope',\r\n training_certificate: 'GraduationCap',\r\n other: 'FileText',\r\n}\r\n\r\nexport const MILITARY_COUNTRIES = ['USA', 'CAN'] as const\r\nexport type MilitaryCountry = typeof MILITARY_COUNTRIES[number]\r\n\r\nexport const MILITARY_COUNTRY_OPTIONS = [\r\n { label: 'United States', value: 'USA' },\r\n { label: 'Canada', value: 'CAN' },\r\n] as const\r\n\r\n// Canadian military branches\r\nexport const CANADIAN_MILITARY_BRANCHES = [\r\n 'canadian_army',\r\n 'royal_canadian_navy',\r\n 'royal_canadian_air_force',\r\n 'canadian_special_operations',\r\n 'canadian_reserves',\r\n 'other',\r\n] as const\r\nexport type CanadianMilitaryBranch = typeof CANADIAN_MILITARY_BRANCHES[number]\r\n\r\nexport const CANADIAN_MILITARY_BRANCH_OPTIONS = [\r\n { label: 'Canadian Army', value: 'canadian_army' },\r\n { label: 'Royal Canadian Navy', value: 'royal_canadian_navy' },\r\n { label: 'Royal Canadian Air Force', value: 'royal_canadian_air_force' },\r\n { label: 'Canadian Special Operations', value: 'canadian_special_operations' },\r\n { label: 'Canadian Reserves', value: 'canadian_reserves' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\n// =============================================================================\r\n// Education Record Options\r\n// =============================================================================\r\n\r\nexport const EDUCATION_RECORD_TYPES = [\r\n 'diploma',\r\n 'degree',\r\n 'transcript',\r\n 'certification',\r\n 'license',\r\n 'training_completion',\r\n 'continuing_education',\r\n 'other',\r\n] as const\r\nexport type EducationRecordType = typeof EDUCATION_RECORD_TYPES[number]\r\n\r\nexport const EDUCATION_RECORD_TYPE_OPTIONS = [\r\n { label: 'Diploma', value: 'diploma' },\r\n { label: 'Degree', value: 'degree' },\r\n { label: 'Transcript', value: 'transcript' },\r\n { label: 'Certification', value: 'certification' },\r\n { label: 'Professional License', value: 'license' },\r\n { label: 'Training Completion', value: 'training_completion' },\r\n { label: 'Continuing Education', value: 'continuing_education' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const EDUCATION_LEVELS = [\r\n 'high_school',\r\n 'associate',\r\n 'bachelor',\r\n 'master',\r\n 'doctorate',\r\n 'professional',\r\n 'vocational',\r\n 'certificate',\r\n 'other',\r\n] as const\r\nexport type EducationLevel = typeof EDUCATION_LEVELS[number]\r\n\r\nexport const EDUCATION_LEVEL_OPTIONS = [\r\n { label: 'High School', value: 'high_school' },\r\n { label: 'Associate Degree', value: 'associate' },\r\n { label: 'Bachelor\\'s Degree', value: 'bachelor' },\r\n { label: 'Master\\'s Degree', value: 'master' },\r\n { label: 'Doctorate', value: 'doctorate' },\r\n { label: 'Professional Degree', value: 'professional' },\r\n { label: 'Vocational/Trade', value: 'vocational' },\r\n { label: 'Certificate Program', value: 'certificate' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const EDUCATION_RECORD_TYPE_ICONS: Record<EducationRecordType, string> = {\r\n diploma: 'ScrollText',\r\n degree: 'GraduationCap',\r\n transcript: 'FileText',\r\n certification: 'Award',\r\n license: 'BadgeCheck',\r\n training_completion: 'CheckCircle',\r\n continuing_education: 'BookOpen',\r\n other: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Credential Options (Professional Licenses, Government IDs, Travel Credentials)\r\n// =============================================================================\r\n\r\nexport const CREDENTIAL_TYPES = [\r\n 'professional_license',\r\n 'government_id',\r\n 'travel_credential',\r\n 'permit',\r\n 'security_clearance',\r\n] as const\r\nexport type CredentialType = typeof CREDENTIAL_TYPES[number]\r\n\r\nexport const CREDENTIAL_TYPE_OPTIONS = [\r\n { label: 'Professional License', value: 'professional_license' },\r\n { label: 'Government ID', value: 'government_id' },\r\n { label: 'Travel Credential', value: 'travel_credential' },\r\n { label: 'Permit', value: 'permit' },\r\n { label: 'Security Clearance', value: 'security_clearance' },\r\n] as const\r\n\r\nexport const CREDENTIAL_TYPE_ICONS: Record<CredentialType, string> = {\r\n professional_license: 'BadgeCheck',\r\n government_id: 'CreditCard',\r\n travel_credential: 'Plane',\r\n permit: 'FileCheck',\r\n security_clearance: 'ShieldCheck',\r\n}\r\n\r\n// Subtypes for each credential type\r\nexport const CREDENTIAL_SUBTYPES = {\r\n professional_license: [\r\n 'cpa',\r\n 'bar_admission',\r\n 'medical_license',\r\n 'nursing_license',\r\n 'real_estate_license',\r\n 'insurance_license',\r\n 'engineering_license',\r\n 'teaching_license',\r\n 'financial_advisor',\r\n 'other',\r\n ],\r\n government_id: [\r\n 'passport',\r\n 'drivers_license',\r\n 'state_id',\r\n 'national_id',\r\n 'green_card',\r\n 'visa',\r\n 'military_id',\r\n 'other',\r\n ],\r\n travel_credential: [\r\n 'tsa_precheck',\r\n 'global_entry',\r\n 'nexus',\r\n 'sentri',\r\n 'clear',\r\n 'other',\r\n ],\r\n permit: [\r\n 'concealed_carry',\r\n 'business_license',\r\n 'hunting_license',\r\n 'fishing_license',\r\n 'other',\r\n ],\r\n security_clearance: [\r\n 'confidential',\r\n 'secret',\r\n 'top_secret',\r\n 'top_secret_sci',\r\n 'other',\r\n ],\r\n} as const\r\n\r\nexport type CredentialSubtype<T extends CredentialType> = typeof CREDENTIAL_SUBTYPES[T][number]\r\nexport type AnyCredentialSubtype = CredentialSubtype<CredentialType>\r\n\r\nexport const CREDENTIAL_SUBTYPE_OPTIONS: Record<CredentialType, Array<{ label: string; value: string }>> = {\r\n professional_license: [\r\n { label: 'CPA (Certified Public Accountant)', value: 'cpa' },\r\n { label: 'Bar Admission (Attorney)', value: 'bar_admission' },\r\n { label: 'Medical License (MD/DO)', value: 'medical_license' },\r\n { label: 'Nursing License (RN/LPN)', value: 'nursing_license' },\r\n { label: 'Real Estate License', value: 'real_estate_license' },\r\n { label: 'Insurance License', value: 'insurance_license' },\r\n { label: 'Engineering License (PE)', value: 'engineering_license' },\r\n { label: 'Teaching License', value: 'teaching_license' },\r\n { label: 'Financial Advisor', value: 'financial_advisor' },\r\n { label: 'Other', value: 'other' },\r\n ],\r\n government_id: [\r\n { label: 'Passport', value: 'passport' },\r\n { label: \"Driver's License\", value: 'drivers_license' },\r\n { label: 'State ID', value: 'state_id' },\r\n { label: 'National ID (Cédula, etc.)', value: 'national_id' },\r\n { label: 'Green Card', value: 'green_card' },\r\n { label: 'Visa', value: 'visa' },\r\n { label: 'Military ID', value: 'military_id' },\r\n { label: 'Other', value: 'other' },\r\n ],\r\n travel_credential: [\r\n { label: 'TSA PreCheck', value: 'tsa_precheck' },\r\n { label: 'Global Entry', value: 'global_entry' },\r\n { label: 'NEXUS', value: 'nexus' },\r\n { label: 'SENTRI', value: 'sentri' },\r\n { label: 'CLEAR', value: 'clear' },\r\n { label: 'Other', value: 'other' },\r\n ],\r\n permit: [\r\n { label: 'Concealed Carry Permit', value: 'concealed_carry' },\r\n { label: 'Business License', value: 'business_license' },\r\n { label: 'Hunting License', value: 'hunting_license' },\r\n { label: 'Fishing License', value: 'fishing_license' },\r\n { label: 'Other', value: 'other' },\r\n ],\r\n security_clearance: [\r\n { label: 'Confidential', value: 'confidential' },\r\n { label: 'Secret', value: 'secret' },\r\n { label: 'Top Secret', value: 'top_secret' },\r\n { label: 'Top Secret/SCI', value: 'top_secret_sci' },\r\n { label: 'Other', value: 'other' },\r\n ],\r\n}\r\n\r\nexport const CREDENTIAL_SUBTYPE_ICONS: Record<string, string> = {\r\n // Professional licenses\r\n cpa: 'Calculator',\r\n bar_admission: 'Scale',\r\n medical_license: 'Stethoscope',\r\n nursing_license: 'HeartPulse',\r\n real_estate_license: 'Home',\r\n insurance_license: 'Shield',\r\n engineering_license: 'Wrench',\r\n teaching_license: 'GraduationCap',\r\n financial_advisor: 'TrendingUp',\r\n // Government IDs\r\n passport: 'BookOpen',\r\n drivers_license: 'Car',\r\n state_id: 'CreditCard',\r\n national_id: 'CreditCard',\r\n green_card: 'Wallet',\r\n visa: 'Stamp',\r\n military_id: 'Shield',\r\n // Travel credentials\r\n tsa_precheck: 'Plane',\r\n global_entry: 'Globe',\r\n nexus: 'ArrowLeftRight',\r\n sentri: 'ArrowLeftRight',\r\n clear: 'Scan',\r\n // Permits\r\n concealed_carry: 'Target',\r\n business_license: 'Briefcase',\r\n hunting_license: 'Crosshair',\r\n fishing_license: 'Fish',\r\n // Security clearances\r\n confidential: 'Lock',\r\n secret: 'Lock',\r\n top_secret: 'ShieldAlert',\r\n top_secret_sci: 'ShieldAlert',\r\n // Default\r\n other: 'FileText',\r\n}\r\n\r\n// =============================================================================\r\n// Membership Record Options\r\n// =============================================================================\r\n\r\nexport const MEMBERSHIP_TYPES = [\r\n 'airline',\r\n 'hotel',\r\n 'rental_car',\r\n 'retail',\r\n 'restaurant',\r\n 'entertainment',\r\n 'travel',\r\n 'rewards',\r\n 'other',\r\n] as const\r\nexport type MembershipType = typeof MEMBERSHIP_TYPES[number]\r\n\r\nexport const MEMBERSHIP_TYPE_OPTIONS = [\r\n { label: 'Airline', value: 'airline' },\r\n { label: 'Hotel', value: 'hotel' },\r\n { label: 'Rental Car', value: 'rental_car' },\r\n { label: 'Retail', value: 'retail' },\r\n { label: 'Restaurant', value: 'restaurant' },\r\n { label: 'Entertainment', value: 'entertainment' },\r\n { label: 'Travel', value: 'travel' },\r\n { label: 'Rewards Program', value: 'rewards' },\r\n { label: 'Other', value: 'other' },\r\n] as const\r\n\r\nexport const MEMBERSHIP_TYPE_ICONS: Record<MembershipType, string> = {\r\n airline: 'Plane',\r\n hotel: 'Hotel',\r\n rental_car: 'Car',\r\n retail: 'ShoppingBag',\r\n restaurant: 'UtensilsCrossed',\r\n entertainment: 'Ticket',\r\n travel: 'MapPin',\r\n rewards: 'Gift',\r\n other: 'CreditCard',\r\n}\r\n\r\nexport const MEMBERSHIP_TYPE_COLORS: Record<MembershipType, IconBgColor> = {\r\n airline: 'blue',\r\n hotel: 'purple',\r\n rental_car: 'orange',\r\n retail: 'pink',\r\n restaurant: 'amber',\r\n entertainment: 'indigo',\r\n travel: 'teal',\r\n rewards: 'green',\r\n other: 'gray',\r\n}\r\n\r\n","{\r\n \"limits\": {\r\n \"property\": { \"household\": 1, \"estate\": 50, \"requiredPlan\": \"estate\" },\r\n \"vehicle\": { \"household\": 3, \"estate\": 50, \"requiredPlan\": \"household\" },\r\n \"pet\": { \"household\": 10, \"estate\": 50, \"requiredPlan\": \"household\" },\r\n \"contact\": { \"household\": 200, \"estate\": 1000, \"requiredPlan\": \"household\" },\r\n \"resident\": { \"household\": 6, \"estate\": 50, \"requiredPlan\": \"household\" },\r\n \"maintenance_task\": { \"household\": 100, \"estate\": 1000, \"requiredPlan\": \"household\" },\r\n \"subscription\": { \"household\": 100, \"estate\": 1000, \"requiredPlan\": \"household\" },\r\n \"valuable\": { \"household\": 20, \"estate\": 2000, \"requiredPlan\": \"household\" },\r\n \"legal\": { \"household\": 5, \"estate\": 500, \"requiredPlan\": \"household\" },\r\n \"financial_account\": { \"household\": 100, \"estate\": 1000, \"requiredPlan\": \"household\" },\r\n \"service\": { \"household\": 20, \"estate\": 100, \"requiredPlan\": \"household\" },\r\n \"insurance\": { \"household\": 20, \"estate\": 100, \"requiredPlan\": \"household\" },\r\n \"device\": { \"household\": 50, \"estate\": 1000, \"requiredPlan\": \"household\" },\r\n \"identity\": { \"household\": 10, \"estate\": 50, \"requiredPlan\": \"household\" }\r\n }\r\n}\r\n","/**\r\n * Subscription plan definitions shared between marketing site and app.\r\n * Note: Free tier has been removed. All new households start with a 14-day trial.\r\n */\r\n\r\nexport type PlanCode = 'household_monthly' | 'household_annual' | 'estate_monthly' | 'estate_annual'\r\nexport type PlanTier = 'household' | 'estate'\r\n\r\nexport interface PlanDefinition {\r\n planCode: PlanCode\r\n tier: PlanTier\r\n name: string\r\n description: string\r\n descriptionAnnual?: string\r\n amountCents: number\r\n billingCycle: 'monthly' | 'annual'\r\n features: readonly string[]\r\n tagline?: string\r\n}\r\n\r\nexport const PLAN_FEATURES = {\r\n household: [\r\n '1 property',\r\n 'Up to 3 vehicles',\r\n 'Up to 5 family members',\r\n\r\n 'Store contractors, providers, and emergency contacts',\r\n 'Track cleaners, gardeners, and other service visits',\r\n 'Maintenance task wizard for your property and vehicles',\r\n 'Maintenance reminders',\r\n 'Emergency procedures',\r\n\r\n 'Subscription cost and renewal tracking',\r\n 'Link insurance to assets, expiration reminders',\r\n 'Valuables cataloging',\r\n 'Encrypted legal documents',\r\n\r\n 'Secure sharing'\r\n ],\r\n\r\n estate: [\r\n 'Dozens of properties and vehicles',\r\n 'Dozens of members including family and staff',\r\n\r\n 'Link LLCs and Trusts to your assets',\r\n 'Continuity Protocol (automatic contingency access)',\r\n\r\n 'Everything in Household',\r\n ],\r\n};\r\n\r\nexport const PLAN_DEFINITIONS: Record<PlanCode, PlanDefinition> = {\r\n household_monthly: {\r\n planCode: 'household_monthly',\r\n tier: 'household',\r\n name: 'Household',\r\n description: 'For managing your home and family with confidence',\r\n amountCents: 999,\r\n billingCycle: 'monthly',\r\n features: PLAN_FEATURES.household,\r\n },\r\n household_annual: {\r\n planCode: 'household_annual',\r\n tier: 'household',\r\n name: 'Household',\r\n description: 'For managing your home and family with confidence',\r\n descriptionAnnual: 'For managing your home and family with confidence - Save 17%',\r\n amountCents: 9900,\r\n billingCycle: 'annual',\r\n features: PLAN_FEATURES.household,\r\n },\r\n estate_monthly: {\r\n planCode: 'estate_monthly',\r\n tier: 'estate',\r\n name: 'Estate',\r\n description: 'For estates, multiple properties, and long term planning',\r\n amountCents: 1999,\r\n billingCycle: 'monthly',\r\n features: PLAN_FEATURES.estate,\r\n tagline: 'Designed for families who want their home information, digital assets, and legal documents to stay accessible — even if something happens.',\r\n },\r\n estate_annual: {\r\n planCode: 'estate_annual',\r\n tier: 'estate',\r\n name: 'Estate',\r\n description: 'For estates, multiple properties, and long term planning',\r\n descriptionAnnual: 'For estates, multiple properties, and long term planning - Save 17%',\r\n amountCents: 19900,\r\n billingCycle: 'annual',\r\n features: PLAN_FEATURES.estate,\r\n tagline: 'Designed for families who want their home information, digital assets, and legal documents to stay accessible — even if something happens.',\r\n },\r\n}\r\n\r\n/**\r\n * Get plan definition by plan code\r\n */\r\nexport function getPlanDefinition(planCode: PlanCode): PlanDefinition | null {\r\n return PLAN_DEFINITIONS[planCode] ?? null\r\n}\r\n\r\n/**\r\n * Get features for a plan tier\r\n */\r\nexport function getPlanFeatures(tier: PlanTier): readonly string[] {\r\n return PLAN_FEATURES[tier]\r\n}\r\n\r\n/**\r\n * Format price for display\r\n */\r\nexport function formatPlanPrice(amountCents: number, billingCycle: 'monthly' | 'annual'): string {\r\n const amount = (amountCents / 100).toFixed(amountCents % 100 === 0 ? 0 : 2)\r\n return billingCycle === 'annual' ? `$${amount}/year` : `$${amount}/mo`\r\n}\r\n\r\n/**\r\n * Entity type strings used in the database/API.\r\n * Also used as feature limit keys - single naming convention.\r\n */\r\nexport type EntityType =\r\n | 'property'\r\n | 'vehicle'\r\n | 'pet'\r\n | 'contact'\r\n | 'resident'\r\n | 'maintenance_task'\r\n | 'subscription'\r\n | 'valuable'\r\n | 'legal'\r\n | 'financial_account'\r\n | 'service'\r\n | 'insurance'\r\n | 'device'\r\n | 'identity'\r\n\r\n/**\r\n * Feature limit configuration per tier\r\n * Note: Free tier has been removed. All new households start with a Household trial.\r\n */\r\nexport interface FeatureLimitConfig {\r\n household: number\r\n estate: number\r\n /** The minimum plan required to use this feature */\r\n requiredPlan: PlanTier\r\n}\r\n\r\n// Import from JSON - single source of truth\r\nimport featureLimitsJson from './feature-limits.json'\r\n\r\n/**\r\n * Feature limits by entity type.\r\n * Used by both frontend (to show upgrade prompts) and backend (to enforce limits on writes).\r\n */\r\nexport const FEATURE_LIMITS: Record<EntityType, FeatureLimitConfig> =\r\n featureLimitsJson.limits as Record<EntityType, FeatureLimitConfig>\r\n\r\n/**\r\n * Get the limit for an entity type based on the user's plan tier\r\n */\r\nexport function getFeatureLimit(entityType: EntityType, tier: PlanTier): number {\r\n return FEATURE_LIMITS[entityType][tier]\r\n}\r\n\r\n/**\r\n * Check if a user can add more of an entity type based on current count and plan tier\r\n */\r\nexport function canAddFeature(entityType: EntityType, tier: PlanTier, currentCount: number): boolean {\r\n const limit = getFeatureLimit(entityType, tier)\r\n return currentCount < limit\r\n}\r\n\r\n/**\r\n * Get the required plan to use an entity type (for upgrade prompts)\r\n */\r\nexport function getRequiredPlan(entityType: EntityType): PlanTier {\r\n return FEATURE_LIMITS[entityType].requiredPlan\r\n}\r\n\r\n/**\r\n * Check if an entity type is fully gated (limit is 0) for the given tier\r\n */\r\nexport function isFeatureGated(entityType: EntityType, tier: PlanTier): boolean {\r\n return getFeatureLimit(entityType, tier) === 0\r\n}\r\n\r\n/**\r\n * Check if an entity type string is a valid EntityType with limits\r\n */\r\nexport function isLimitedEntityType(entityType: string): entityType is EntityType {\r\n return entityType in FEATURE_LIMITS\r\n}\r\n\r\n/**\r\n * Get the limit for an entity type based on the user's plan tier\r\n * Returns undefined if the entity type doesn't have limits\r\n */\r\nexport function getEntityLimit(entityType: string, tier: PlanTier): number | undefined {\r\n if (!isLimitedEntityType(entityType)) return undefined\r\n return getFeatureLimit(entityType, tier)\r\n}\r\n","/**\r\n * File type utilities\r\n *\r\n * Shared constants and helpers for file handling across web and mobile.\r\n */\r\n\r\n// =============================================================================\r\n// File Size Limits\r\n// IMPORTANT: Keep in sync with backend-v2/Configuration/AttachmentConfig.cs\r\n// =============================================================================\r\n\r\n/** Maximum file size in MB for regular uploads */\r\nexport const MAX_FILE_SIZE_MB = 200\r\n\r\n/** Maximum file size in bytes for regular uploads */\r\nexport const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024\r\n\r\n/** Maximum file size in MB for heartbeat/continuity videos */\r\nexport const MAX_HEARTBEAT_VIDEO_SIZE_MB = 500\r\n\r\n/** Maximum file size in bytes for heartbeat/continuity videos */\r\nexport const MAX_HEARTBEAT_VIDEO_SIZE_BYTES = MAX_HEARTBEAT_VIDEO_SIZE_MB * 1024 * 1024\r\n\r\n// =============================================================================\r\n// MIME Type Mappings\r\n// =============================================================================\r\n\r\n/**\r\n * Mapping of MIME types to file extensions.\r\n * Used for both download (adding extensions) and display purposes.\r\n */\r\nexport const MIME_TO_EXTENSION: Record<string, string> = {\r\n // Images\r\n 'image/jpeg': '.jpg',\r\n 'image/jpg': '.jpg',\r\n 'image/png': '.png',\r\n 'image/gif': '.gif',\r\n 'image/webp': '.webp',\r\n 'image/svg+xml': '.svg',\r\n 'image/bmp': '.bmp',\r\n 'image/heic': '.heic',\r\n 'image/heif': '.heif',\r\n // Documents\r\n 'application/pdf': '.pdf',\r\n 'application/msword': '.doc',\r\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',\r\n 'text/plain': '.txt',\r\n // Spreadsheets\r\n 'application/vnd.ms-excel': '.xls',\r\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',\r\n 'text/csv': '.csv',\r\n // Archives\r\n 'application/zip': '.zip',\r\n 'application/x-zip-compressed': '.zip',\r\n // Videos\r\n 'video/mp4': '.mp4',\r\n 'video/webm': '.webm',\r\n 'video/ogg': '.ogv',\r\n 'video/quicktime': '.mov',\r\n 'video/x-m4v': '.m4v',\r\n 'video/mpeg': '.mpeg',\r\n 'video/x-mpeg': '.mpeg',\r\n // Audio\r\n 'audio/mpeg': '.mp3',\r\n 'audio/wav': '.wav',\r\n}\r\n\r\n/**\r\n * Get file extension from MIME type (with leading dot).\r\n * Returns empty string if MIME type is unknown.\r\n */\r\nexport function getExtensionFromMimeType(mimeType: string): string {\r\n return MIME_TO_EXTENSION[mimeType.toLowerCase()] || ''\r\n}\r\n\r\n/**\r\n * Get file extension from MIME type (without leading dot).\r\n * Returns 'bin' if MIME type is unknown.\r\n */\r\nexport function getExtensionWithoutDot(mimeType: string): string {\r\n const ext = MIME_TO_EXTENSION[mimeType.toLowerCase()]\r\n return ext ? ext.substring(1) : 'bin'\r\n}\r\n\r\n/**\r\n * Add extension to filename if not already present.\r\n * Does nothing if MIME type is unknown.\r\n */\r\nexport function ensureFileExtension(fileName: string, mimeType: string): string {\r\n const ext = getExtensionFromMimeType(mimeType)\r\n if (!ext) return fileName\r\n // Check if fileName already has this extension (case-insensitive)\r\n if (fileName.toLowerCase().endsWith(ext.toLowerCase())) return fileName\r\n return fileName + ext\r\n}\r\n","/**\r\n * Navigation Icon Colors\r\n *\r\n * Color mapping for sidebar navigation icons.\r\n * Each category has a unique color to help with visual identification.\r\n * Supports holiday themes when colorful icons are enabled.\r\n */\r\n\r\nexport type HolidayTheme = 'default' | 'christmas' | 'halloween' | 'valentines' | 'pride' | 'stpatricks' | 'july4th' | 'canadaday' | 'thanksgiving' | 'earthday' | 'newyear' | 'easter' | 'costarica'\r\n\r\n/**\r\n * Detect current holiday theme based on date.\r\n * Returns 'default' if no holiday is active.\r\n *\r\n * For testing, add ?theme=christmas (or halloween, valentines, pride, stpatricks, july4th, canadaday, thanksgiving, earthday, newyear, easter, costarica) to the URL.\r\n */\r\nexport function getCurrentHolidayTheme(): HolidayTheme {\r\n // Check for URL override (for testing)\r\n if (typeof window !== 'undefined') {\r\n const params = new URLSearchParams(window.location.search)\r\n const themeOverride = params.get('theme') as HolidayTheme | null\r\n if (themeOverride && ['christmas', 'halloween', 'valentines', 'pride', 'stpatricks', 'july4th', 'canadaday', 'thanksgiving', 'earthday', 'newyear', 'easter', 'costarica', 'default'].includes(themeOverride)) {\r\n return themeOverride\r\n }\r\n }\r\n\r\n const now = new Date()\r\n const month = now.getMonth() + 1 // 1-indexed\r\n const day = now.getDate()\r\n\r\n // New Year's: Dec 31 and Jan 1\r\n if ((month === 12 && day === 31) || (month === 1 && day === 1)) return 'newyear'\r\n // Valentine's: Feb 14\r\n if (month === 2 && day === 14) return 'valentines'\r\n // St. Patrick's Day: March 17\r\n if (month === 3 && day === 17) return 'stpatricks'\r\n // Earth Day: April 22\r\n if (month === 4 && day === 22) return 'earthday'\r\n // Easter: Calculate using Anonymous Gregorian algorithm\r\n const year = now.getFullYear()\r\n const a = year % 19\r\n const b = Math.floor(year / 100)\r\n const c = year % 100\r\n const d = Math.floor(b / 4)\r\n const e = b % 4\r\n const f = Math.floor((b + 8) / 25)\r\n const g = Math.floor((b - f + 1) / 3)\r\n const h = (19 * a + b - d - g + 15) % 30\r\n const i = Math.floor(c / 4)\r\n const k = c % 4\r\n const l = (32 + 2 * e + 2 * i - h - k) % 7\r\n const m = Math.floor((a + 11 * h + 22 * l) / 451)\r\n const easterMonth = Math.floor((h + l - 7 * m + 114) / 31)\r\n const easterDay = ((h + l - 7 * m + 114) % 31) + 1\r\n if (month === easterMonth && day === easterDay) return 'easter'\r\n // Pride: June 28 (Stonewall anniversary)\r\n if (month === 6 && day === 28) return 'pride'\r\n // Canada Day: July 1\r\n if (month === 7 && day === 1) return 'canadaday'\r\n // Costa Rica Independence Day: September 15\r\n if (month === 9 && day === 15) return 'costarica'\r\n // Independence Day: July 4\r\n if (month === 7 && day === 4) return 'july4th'\r\n // Halloween: Oct 30-31\r\n if (month === 10 && (day === 30 || day === 31)) return 'halloween'\r\n // Thanksgiving: 4th Thursday of November\r\n if (month === 11) {\r\n const firstDay = new Date(now.getFullYear(), 10, 1).getDay() // Day of week for Nov 1\r\n const fourthThursday = 1 + ((11 - firstDay) % 7) + 21 // Calculate 4th Thursday\r\n if (day === fourthThursday) return 'thanksgiving'\r\n }\r\n // Christmas: Dec 24-25\r\n if (month === 12 && (day === 24 || day === 25)) return 'christmas'\r\n\r\n return 'default'\r\n}\r\n\r\n/** Default nav icon colors */\r\nexport const NAV_ICON_COLORS: Record<string, string> = {\r\n // Primary navigation\r\n '/dashboard': 'text-blue-500',\r\n '/people': 'text-violet-500',\r\n '/share': 'text-cyan-500',\r\n '/access_code': 'text-amber-500',\r\n '/contact': 'text-emerald-500',\r\n '/credentials': 'text-amber-600',\r\n '/device': 'text-sky-500',\r\n '/insurance': 'text-indigo-500',\r\n '/legal': 'text-purple-500',\r\n '/financial': 'text-teal-500',\r\n '/pet': 'text-orange-500',\r\n '/password': 'text-amber-600',\r\n '/property': 'text-blue-500',\r\n '/service': 'text-pink-500',\r\n '/subscription': 'text-fuchsia-500',\r\n '/taxes': 'text-lime-500',\r\n '/valuables': 'text-yellow-500',\r\n '/vehicle': 'text-rose-500',\r\n '/continuity': 'text-red-500',\r\n '/search': 'text-sky-500',\r\n '/more': 'text-purple-500',\r\n '/signout': 'text-slate-500',\r\n '/settings': 'text-zinc-500',\r\n // Secondary tabs (sub-sections with different icons)\r\n 'records': 'text-slate-500',\r\n 'maintenance': 'text-amber-500',\r\n 'improvements': 'text-emerald-500',\r\n}\r\n\r\n/** Nav keys in display order for generating themed colors (optimized for mobile bottom nav + drawer) */\r\nconst NAV_KEYS = [\r\n // Bottom nav (always visible)\r\n '/dashboard', '/people', '/password', '/search', '/more',\r\n // More drawer - Group 1 (Assets)\r\n '/property', '/pet', '/valuables', '/vehicle',\r\n // More drawer - Group 2 (Financial & Services)\r\n '/subscription', '/financial', '/service', '/contact', '/insurance',\r\n // More drawer - Group 3 (Bottom row)\r\n '/signout', '/settings', '/continuity', '/share',\r\n // Other routes (sidebar, less common)\r\n '/access_code', '/credentials', '/device', '/legal', '/taxes',\r\n] as const\r\n\r\nconst SECONDARY_KEYS = ['records', 'maintenance', 'improvements'] as const\r\n\r\n/** Generate nav colors by cycling through a color palette */\r\nfunction cycleColors(colors: string[], secondary?: string[]): Record<string, string> {\r\n const result: Record<string, string> = {}\r\n NAV_KEYS.forEach((key, i) => { result[key] = colors[i % colors.length] })\r\n const sec = secondary || colors\r\n SECONDARY_KEYS.forEach((key, i) => { result[key] = sec[i % sec.length] })\r\n return result\r\n}\r\n\r\n/** Generate nav colors using stripe pattern (for flags) */\r\nfunction stripeColors(stripes: string[][]): Record<string, string> {\r\n const result: Record<string, string> = {}\r\n const itemsPerStripe = Math.ceil(NAV_KEYS.length / stripes.length)\r\n NAV_KEYS.forEach((key, i) => {\r\n const stripeIndex = Math.floor(i / itemsPerStripe)\r\n const stripe = stripes[Math.min(stripeIndex, stripes.length - 1)]\r\n result[key] = stripe[i % stripe.length]\r\n })\r\n SECONDARY_KEYS.forEach((key, i) => {\r\n result[key] = stripes[i % stripes.length][0]\r\n })\r\n return result\r\n}\r\n\r\n/** Christmas: red, green, gold */\r\nconst CHRISTMAS_NAV_COLORS = cycleColors([\r\n 'text-red-600', 'text-green-600', 'text-red-500', 'text-yellow-500',\r\n 'text-green-500', 'text-red-500', 'text-green-600',\r\n])\r\n\r\n/** Halloween: orange, purple, black */\r\nconst HALLOWEEN_NAV_COLORS = cycleColors([\r\n 'text-orange-500', 'text-purple-600', 'text-orange-600', 'text-purple-500',\r\n 'text-orange-500', 'text-purple-600', 'text-zinc-800',\r\n])\r\n\r\n/** Valentine's: pink, red, rose */\r\nconst VALENTINES_NAV_COLORS = cycleColors([\r\n 'text-pink-500', 'text-red-500', 'text-rose-500', 'text-pink-600',\r\n 'text-red-500', 'text-rose-500', 'text-red-600',\r\n])\r\n\r\n/** Pride: rainbow */\r\nconst PRIDE_NAV_COLORS = cycleColors([\r\n 'text-red-500', 'text-orange-500', 'text-yellow-500',\r\n 'text-green-500', 'text-blue-500', 'text-purple-500',\r\n])\r\n\r\n/** St. Patrick's: greens and gold */\r\nconst STPATRICKS_NAV_COLORS = cycleColors([\r\n 'text-green-600', 'text-green-500', 'text-yellow-500', 'text-emerald-500',\r\n])\r\n\r\n/** Canada Day: red and white */\r\nconst CANADADAY_NAV_COLORS = cycleColors(['text-red-600', 'text-slate-400', 'text-red-500'])\r\n\r\n/** July 4th: red, white, blue */\r\nconst JULY4TH_NAV_COLORS = cycleColors(['text-red-500', 'text-blue-600', 'text-slate-400', 'text-red-600', 'text-blue-500'])\r\n\r\n/** Thanksgiving: autumn colors */\r\nconst THANKSGIVING_NAV_COLORS = cycleColors(['text-orange-600', 'text-amber-700', 'text-yellow-600', 'text-orange-500', 'text-amber-600'])\r\n\r\n/** Earth Day: greens and blues */\r\nconst EARTHDAY_NAV_COLORS = cycleColors(['text-green-600', 'text-blue-500', 'text-emerald-500', 'text-sky-500', 'text-green-500', 'text-blue-600'])\r\n\r\n/** New Year's: gold and silver */\r\nconst NEWYEAR_NAV_COLORS = cycleColors(['text-yellow-500', 'text-slate-400', 'text-yellow-600', 'text-slate-500'])\r\n\r\n/** Easter: pastel spring colors */\r\nconst EASTER_NAV_COLORS = cycleColors(['text-pink-500', 'text-purple-500', 'text-yellow-500', 'text-emerald-500', 'text-sky-500'])\r\n\r\n/** Costa Rica: blue, white, red stripes (flag pattern) */\r\nconst COSTARICA_NAV_COLORS = stripeColors([\r\n ['text-blue-600', 'text-blue-500'], // Blue stripe\r\n ['text-slate-400'], // White stripe \r\n ['text-red-600', 'text-red-500'], // Red stripe (center)\r\n ['text-slate-400'], // White stripe\r\n ['text-blue-600', 'text-blue-500'], // Blue stripe\r\n])\r\n\r\n/** Map of holiday themes to their color palettes */\r\nexport const HOLIDAY_NAV_COLORS: Record<HolidayTheme, Record<string, string>> = {\r\n default: NAV_ICON_COLORS,\r\n christmas: CHRISTMAS_NAV_COLORS,\r\n halloween: HALLOWEEN_NAV_COLORS,\r\n valentines: VALENTINES_NAV_COLORS,\r\n pride: PRIDE_NAV_COLORS,\r\n stpatricks: STPATRICKS_NAV_COLORS,\r\n canadaday: CANADADAY_NAV_COLORS,\r\n july4th: JULY4TH_NAV_COLORS,\r\n thanksgiving: THANKSGIVING_NAV_COLORS,\r\n earthday: EARTHDAY_NAV_COLORS,\r\n newyear: NEWYEAR_NAV_COLORS,\r\n easter: EASTER_NAV_COLORS,\r\n costarica: COSTARICA_NAV_COLORS,\r\n}\r\n\r\n/**\r\n * Get nav icon colors for the current holiday theme.\r\n * Falls back to default colors if path not found in holiday theme.\r\n */\r\nexport function getNavIconColor(path: string): string {\r\n const theme = getCurrentHolidayTheme()\r\n const colors = HOLIDAY_NAV_COLORS[theme]\r\n return colors[path] || NAV_ICON_COLORS[path] || ''\r\n}\r\n\r\n/**\r\n * Animal icon names for pet nav rotation easter egg.\r\n * Hovering over the pet nav item will randomly change the icon to one of these animals.\r\n */\r\nexport const ANIMAL_ICON_NAMES = [\r\n 'PawPrint',\r\n 'Bird',\r\n 'Cat',\r\n 'Dog',\r\n 'Fish',\r\n 'Rabbit',\r\n 'Rat',\r\n 'Snail',\r\n 'Squirrel',\r\n 'Turtle',\r\n 'Bug',\r\n] as const\r\n\r\nexport type AnimalIconName = typeof ANIMAL_ICON_NAMES[number]\r\n\r\n/**\r\n * Feature flag for pet icon rotation easter egg.\r\n * Set to false to disable the rotating animal icons on pet nav hover.\r\n */\r\nexport const PET_ICON_ROTATION_ENABLED = true\r\n","/**\r\n * User Key Bundle Utilities\r\n *\r\n * Handles generation and encryption of user root keypairs (asymmetric keys).\r\n * These keys are used to wrap/unwrap household symmetric keys.\r\n *\r\n * Key Hierarchy:\r\n * - Recovery Key + server_wrap_secret → WrapKey → encrypts private key\r\n * - PRF output → PRF_key → encrypts private key (alternative path)\r\n * - Private key → unwraps household keys\r\n *\r\n * @module encryption/keyBundle\r\n */\r\n\r\nimport { base64Encode, base64Decode } from './utils';\r\nimport { DEFAULT_KEY_BUNDLE_ALG } from '@hearthcoo/types';\r\n\r\n/**\r\n * Supported asymmetric key algorithms\r\n */\r\nexport type KeyAlgorithm = 'P-521' | 'P-384' | 'P-256' | 'Ed25519';\r\n\r\n/**\r\n * Generate asymmetric keypair for user root key bundle\r\n *\r\n * Uses P-521 (ECDH) for maximum security and quantum resistance.\r\n *\r\n * @param algorithm - Key algorithm (default: P-521 for maximum security)\r\n * @returns Generated keypair with raw bytes\r\n *\r\n * @example\r\n * ```ts\r\n * const { publicKey, privateKey, publicKeyBytes, privateKeyBytes } =\r\n * await generateKeyPair();\r\n *\r\n * // Store public key in user_key_bundles.public_key\r\n * // Encrypt private key with WrapKey or PRF_key\r\n * ```\r\n */\r\nexport async function generateKeyPair(algorithm: KeyAlgorithm = 'P-521'): Promise<{\r\n publicKey: CryptoKey;\r\n privateKey: CryptoKey;\r\n publicKeyBytes: Uint8Array;\r\n privateKeyBytes: Uint8Array;\r\n algorithm: string;\r\n}> {\r\n let keyPair: CryptoKeyPair;\r\n let alg: string;\r\n\r\n switch (algorithm) {\r\n case 'P-521':\r\n // P-521 (NIST P-521) - Maximum security, quantum-resistant\r\n // ~256-bit security level\r\n keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-521'\r\n },\r\n true, // extractable\r\n ['deriveKey']\r\n ) as CryptoKeyPair;\r\n alg = 'ECDH-P-521';\r\n break;\r\n\r\n case 'P-384':\r\n // P-384 (NIST P-384) - High security\r\n // ~192-bit security level\r\n keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-384'\r\n },\r\n true,\r\n ['deriveKey']\r\n ) as CryptoKeyPair;\r\n alg = 'ECDH-P-384';\r\n break;\r\n\r\n case 'P-256':\r\n // P-256 (NIST P-256) - Standard security\r\n // ~128-bit security level\r\n keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-256'\r\n },\r\n true,\r\n ['deriveKey']\r\n ) as CryptoKeyPair;\r\n alg = 'ECDH-P-256';\r\n break;\r\n\r\n case 'Ed25519':\r\n // Note: Ed25519 is for signing, not encryption\r\n // Would need X25519 for ECDH, which isn't widely supported in WebCrypto yet\r\n throw new Error('Ed25519 not yet supported - use P-521 for maximum security');\r\n\r\n default:\r\n throw new Error(`Unsupported algorithm: ${algorithm}`);\r\n }\r\n\r\n // Export keys to raw bytes\r\n const publicKeyBytes = new Uint8Array(\r\n await crypto.subtle.exportKey('raw', keyPair.publicKey)\r\n );\r\n\r\n const privateKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey);\r\n const privateKeyBytes = new TextEncoder().encode(JSON.stringify(privateKeyJwk));\r\n\r\n return {\r\n publicKey: keyPair.publicKey,\r\n privateKey: keyPair.privateKey,\r\n publicKeyBytes,\r\n privateKeyBytes,\r\n algorithm: alg\r\n };\r\n}\r\n\r\n/**\r\n * Encrypt private key with WrapKey (derived from Recovery Key + server_wrap_secret)\r\n *\r\n * @param privateKeyBytes - Private key raw bytes\r\n * @param wrapKey - WrapKey from deriveWrapKey()\r\n * @returns Encrypted private key (base64-encoded packed blob)\r\n *\r\n * @example\r\n * ```ts\r\n * const wrapKey = await deriveWrapKey(recoveryKey, serverWrapSecret);\r\n * const encryptedPrivateKey = await encryptPrivateKeyWithWrapKey(\r\n * privateKeyBytes,\r\n * wrapKey\r\n * );\r\n *\r\n * // Store in user_key_bundles.encrypted_private_key\r\n * ```\r\n */\r\nexport async function encryptPrivateKeyWithWrapKey(\r\n privateKeyBytes: Uint8Array,\r\n wrapKey: CryptoKey\r\n): Promise<string> {\r\n // Generate IV\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n\r\n // Encrypt with AES-GCM\r\n const ciphertext = await crypto.subtle.encrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n },\r\n wrapKey,\r\n privateKeyBytes as BufferSource\r\n );\r\n\r\n // Pack: [version (1 byte)][IV (12 bytes)][ciphertext + auth tag]\r\n const version = new Uint8Array([1]);\r\n const packed = new Uint8Array(1 + 12 + ciphertext.byteLength);\r\n packed.set(version, 0);\r\n packed.set(iv, 1);\r\n packed.set(new Uint8Array(ciphertext), 13);\r\n\r\n return base64Encode(packed);\r\n}\r\n\r\n/**\r\n * Decrypt private key with WrapKey\r\n *\r\n * @param encryptedPrivateKey - Encrypted private key (base64-encoded)\r\n * @param wrapKey - WrapKey from deriveWrapKey()\r\n * @returns Decrypted private key bytes\r\n *\r\n * @example\r\n * ```ts\r\n * const wrapKey = await deriveWrapKey(recoveryKey, serverWrapSecret);\r\n * const privateKeyBytes = await decryptPrivateKeyWithWrapKey(\r\n * encryptedPrivateKey,\r\n * wrapKey\r\n * );\r\n * ```\r\n */\r\nexport async function decryptPrivateKeyWithWrapKey(\r\n encryptedPrivateKey: string,\r\n wrapKey: CryptoKey\r\n): Promise<Uint8Array> {\r\n const packed = base64Decode(encryptedPrivateKey);\r\n\r\n // Unpack\r\n const version = packed[0];\r\n if (version !== 1) {\r\n throw new Error(`Unsupported encryption version: ${version}`);\r\n }\r\n\r\n const iv = packed.slice(1, 13);\r\n const ciphertext = packed.slice(13);\r\n\r\n // Decrypt with AES-GCM\r\n const plaintextBuffer = await crypto.subtle.decrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv\r\n },\r\n wrapKey,\r\n ciphertext\r\n );\r\n\r\n return new Uint8Array(plaintextBuffer);\r\n}\r\n\r\n/**\r\n * Import private key from bytes for use in key derivation\r\n *\r\n * @param privateKeyBytes - Private key bytes (JWK format as JSON string)\r\n * @param algorithm - Key algorithm\r\n * @returns Imported CryptoKey\r\n */\r\nexport async function importPrivateKey(\r\n privateKeyBytes: Uint8Array,\r\n algorithm: string = DEFAULT_KEY_BUNDLE_ALG\r\n): Promise<CryptoKey> {\r\n // Parse JWK from bytes\r\n const jwkString = new TextDecoder().decode(privateKeyBytes);\r\n const jwk = JSON.parse(jwkString);\r\n\r\n // Determine curve from algorithm\r\n let namedCurve: string;\r\n if (algorithm.includes('P-521')) {\r\n namedCurve = 'P-521';\r\n } else if (algorithm.includes('P-384')) {\r\n namedCurve = 'P-384';\r\n } else if (algorithm.includes('P-256')) {\r\n namedCurve = 'P-256';\r\n } else {\r\n throw new Error(`Unsupported algorithm: ${algorithm}`);\r\n }\r\n\r\n // Import private key\r\n return await crypto.subtle.importKey(\r\n 'jwk',\r\n jwk,\r\n {\r\n name: 'ECDH',\r\n namedCurve\r\n },\r\n true,\r\n ['deriveKey']\r\n );\r\n}\r\n\r\n/**\r\n * Import public key from bytes\r\n *\r\n * @param publicKeyBytes - Public key raw bytes\r\n * @param algorithm - Key algorithm\r\n * @returns Imported CryptoKey\r\n */\r\nexport async function importPublicKey(\r\n publicKeyBytes: Uint8Array,\r\n algorithm: string = DEFAULT_KEY_BUNDLE_ALG\r\n): Promise<CryptoKey> {\r\n // Determine curve from algorithm\r\n let namedCurve: string;\r\n if (algorithm.includes('P-521')) {\r\n namedCurve = 'P-521';\r\n } else if (algorithm.includes('P-384')) {\r\n namedCurve = 'P-384';\r\n } else if (algorithm.includes('P-256')) {\r\n namedCurve = 'P-256';\r\n } else {\r\n throw new Error(`Unsupported algorithm: ${algorithm}`);\r\n }\r\n\r\n return await crypto.subtle.importKey(\r\n 'raw',\r\n publicKeyBytes as BufferSource,\r\n {\r\n name: 'ECDH',\r\n namedCurve\r\n },\r\n true,\r\n []\r\n );\r\n}\r\n","/**\r\n * Asymmetric Key Wrapping with ECDH\r\n *\r\n * Implements ECIES (Elliptic Curve Integrated Encryption Scheme) to wrap/unwrap\r\n * symmetric household keys using user's P-521 public/private keys.\r\n *\r\n * Algorithm:\r\n * 1. Generate ephemeral P-521 keypair\r\n * 2. Perform ECDH with (ephemeral_private + user_public) → shared_secret\r\n * 3. Derive AES-256-GCM key from shared_secret using HKDF\r\n * 4. Encrypt household_key with AES-256-GCM\r\n * 5. Output: [ephemeral_public][IV][ciphertext][auth_tag]\r\n *\r\n * @module encryption/asymmetricWrap\r\n */\r\n\r\nimport { base64Encode, base64Decode } from './utils';\r\nimport { DEFAULT_KEY_BUNDLE_ALG } from '@hearthcoo/types';\r\n\r\n/**\r\n * Wrap (encrypt) a household key using a user's public key (ECIES)\r\n *\r\n * @param householdKey - Symmetric household key to wrap (32 bytes AES-256)\r\n * @param publicKey - User's P-521 public key\r\n * @param algorithm - Algorithm string (e.g., 'ECDH-P-521')\r\n * @returns Base64-encoded wrapped key blob\r\n *\r\n * @example\r\n * ```ts\r\n * const publicKey = await importPublicKey(publicKeyBytes, 'ECDH-P-521');\r\n * const wrappedKey = await wrapHouseholdKey(householdKeyBytes, publicKey, 'ECDH-P-521');\r\n * // Store wrappedKey in member_key_access.wrapped_key\r\n * ```\r\n */\r\nexport async function wrapHouseholdKey(\r\n householdKey: Uint8Array,\r\n publicKey: CryptoKey,\r\n algorithm: string = DEFAULT_KEY_BUNDLE_ALG\r\n): Promise<string> {\r\n // Determine curve from algorithm\r\n const namedCurve = algorithm.includes('P-521') ? 'P-521'\r\n : algorithm.includes('P-384') ? 'P-384'\r\n : algorithm.includes('P-256') ? 'P-256'\r\n : (() => { throw new Error(`Unsupported algorithm: ${algorithm}`); })();\r\n\r\n // 1. Generate ephemeral keypair\r\n const ephemeralKeyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve\r\n },\r\n true, // extractable\r\n ['deriveKey']\r\n ) as CryptoKeyPair;\r\n\r\n // 2. Perform ECDH: derive shared secret\r\n const sharedSecret = await crypto.subtle.deriveKey(\r\n {\r\n name: 'ECDH',\r\n public: publicKey\r\n },\r\n ephemeralKeyPair.privateKey,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false, // not extractable (security)\r\n ['encrypt']\r\n );\r\n\r\n // 3. Generate IV for AES-GCM\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n\r\n // 4. Encrypt household key with derived AES key\r\n const ciphertext = await crypto.subtle.encrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv: iv as BufferSource\r\n },\r\n sharedSecret,\r\n householdKey as BufferSource\r\n );\r\n\r\n // 5. Export ephemeral public key\r\n const ephemeralPublicKeyBytes = new Uint8Array(\r\n await crypto.subtle.exportKey('raw', ephemeralKeyPair.publicKey)\r\n );\r\n\r\n // Pack: [version (1 byte)][ephemeral_public_key][IV (12 bytes)][ciphertext + auth_tag]\r\n const version = new Uint8Array([1]);\r\n const packed = new Uint8Array(\r\n 1 + ephemeralPublicKeyBytes.length + 12 + ciphertext.byteLength\r\n );\r\n\r\n let offset = 0;\r\n packed.set(version, offset);\r\n offset += 1;\r\n packed.set(ephemeralPublicKeyBytes, offset);\r\n offset += ephemeralPublicKeyBytes.length;\r\n packed.set(iv, offset);\r\n offset += 12;\r\n packed.set(new Uint8Array(ciphertext), offset);\r\n\r\n return base64Encode(packed);\r\n}\r\n\r\n/**\r\n * Unwrap (decrypt) a household key using a user's private key (ECIES)\r\n *\r\n * @param wrappedKey - Base64-encoded wrapped key blob\r\n * @param privateKey - User's P-521 private key\r\n * @param algorithm - Algorithm string (e.g., 'ECDH-P-521')\r\n * @returns Decrypted household key bytes\r\n *\r\n * @example\r\n * ```ts\r\n * const privateKey = await importPrivateKey(privateKeyBytes, 'ECDH-P-521');\r\n * const householdKey = await unwrapHouseholdKey(wrappedKeyBase64, privateKey, 'ECDH-P-521');\r\n * ```\r\n */\r\nexport async function unwrapHouseholdKey(\r\n wrappedKey: string,\r\n privateKey: CryptoKey,\r\n algorithm: string = DEFAULT_KEY_BUNDLE_ALG\r\n): Promise<Uint8Array> {\r\n const packed = base64Decode(wrappedKey);\r\n\r\n // Unpack version\r\n const version = packed[0];\r\n if (version !== 1) {\r\n throw new Error(`Unsupported wrap version: ${version}`);\r\n }\r\n\r\n // Determine curve and ephemeral public key size\r\n const namedCurve = algorithm.includes('P-521') ? 'P-521'\r\n : algorithm.includes('P-384') ? 'P-384'\r\n : algorithm.includes('P-256') ? 'P-256'\r\n : (() => { throw new Error(`Unsupported algorithm: ${algorithm}`); })();\r\n\r\n // Public key sizes (uncompressed point format: 0x04 + x + y)\r\n const publicKeySize = namedCurve === 'P-521' ? 133 // 1 + 66 + 66\r\n : namedCurve === 'P-384' ? 97 // 1 + 48 + 48\r\n : 65; // P-256: 1 + 32 + 32\r\n\r\n // Unpack components\r\n let offset = 1;\r\n const ephemeralPublicKeyBytes = packed.slice(offset, offset + publicKeySize);\r\n offset += publicKeySize;\r\n\r\n const iv = packed.slice(offset, offset + 12);\r\n offset += 12;\r\n\r\n const ciphertext = packed.slice(offset);\r\n\r\n // Import ephemeral public key\r\n const ephemeralPublicKey = await crypto.subtle.importKey(\r\n 'raw',\r\n ephemeralPublicKeyBytes as BufferSource,\r\n {\r\n name: 'ECDH',\r\n namedCurve\r\n },\r\n false,\r\n []\r\n );\r\n\r\n // Perform ECDH: derive shared secret\r\n const sharedSecret = await crypto.subtle.deriveKey(\r\n {\r\n name: 'ECDH',\r\n public: ephemeralPublicKey\r\n },\r\n privateKey,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false,\r\n ['decrypt']\r\n );\r\n\r\n // Decrypt household key\r\n const plaintextBuffer = await crypto.subtle.decrypt(\r\n {\r\n name: 'AES-GCM',\r\n iv\r\n },\r\n sharedSecret,\r\n ciphertext\r\n );\r\n\r\n return new Uint8Array(plaintextBuffer);\r\n}\r\n","/**\r\n * WebAuthn Device-Bound Storage (Windows Hello Alternative to PRF)\r\n *\r\n * Provides biometric unlock on platforms without PRF support (Windows).\r\n * Uses WebAuthn for authentication + device fingerprinting for key derivation.\r\n *\r\n * ## Security Model\r\n *\r\n * Without PRF, we can't get deterministic secrets from WebAuthn directly.\r\n * Instead, we use a \"device binding\" approach:\r\n *\r\n * 1. Generate random \"device unlock key\" (DUK) - 32 bytes\r\n * 2. Derive \"device binding key\" (DBK) from:\r\n * - credentialId (deterministic, not secret)\r\n * - device fingerprint (browser/OS info, not secret)\r\n * - random salt (stored, not secret)\r\n * 3. Encrypt DUK with DBK → store in localStorage\r\n * 4. Encrypt user private key with DUK → store on server\r\n *\r\n * On unlock:\r\n * - User authenticates with Windows Hello (proves identity)\r\n * - Derive DBK from credentialId (obtained after auth) + fingerprint + salt\r\n * - Decrypt DUK\r\n * - Download encrypted private key from server\r\n * - Decrypt private key with DUK\r\n *\r\n * ## Security Properties\r\n *\r\n * - ✅ Biometric authentication required (Windows Hello)\r\n * - ✅ Server-side access control (encrypted key requires auth to download)\r\n * - ✅ Device-bound (can't easily export)\r\n * - ⚠️ If attacker compromises account AND clones device, could decrypt\r\n * - ⚠️ Recovery key still needed for device loss/reset\r\n *\r\n * ## Platform Support\r\n *\r\n * - ✅ Windows (all browsers with WebAuthn)\r\n * - ✅ Linux\r\n * - ✅ Android (fingerprint/face unlock)\r\n * - ⚠️ Don't use on Apple devices - use PRF instead (more secure)\r\n *\r\n * @module encryption/webauthnDeviceBound\r\n */\r\n\r\nimport { base64Encode, base64Decode } from './utils';\r\n\r\n/**\r\n * Relying Party ID for WebAuthn\r\n */\r\nexport const RP_ID = typeof window !== 'undefined'\r\n ? window.location.hostname\r\n : 'estatehelm.com';\r\n\r\n/**\r\n * Relying Party Name\r\n */\r\nexport const RP_NAME = 'EstateHelm';\r\n\r\n/**\r\n * Storage key prefix for device-bound credentials\r\n * Used by webauthn device-bound and trusted-device modules\r\n */\r\nexport const DEVICE_BOUND_STORAGE_PREFIX = 'hearthcoo_device_bound_';\r\n\r\n// Note: Device fingerprint functions removed in v2\r\n// They were too unstable (changed with browser updates, screen resolution, etc.)\r\n// Windows Hello authentication via TPM is sufficient for device binding\r\n\r\n/**\r\n * Device-bound credential data stored in localStorage\r\n */\r\ninterface DeviceBoundCredential {\r\n /** Credential ID (base64) */\r\n credentialId: string;\r\n\r\n /** Random salt for key derivation (base64) */\r\n salt: string;\r\n\r\n /** Encrypted device unlock key (base64) */\r\n encryptedDeviceKey: string;\r\n\r\n /** Public key (base64, for server verification) */\r\n publicKey: string;\r\n\r\n /** Relying Party ID */\r\n rpId: string;\r\n}\r\n\r\n/**\r\n * Registration result\r\n */\r\nexport interface DeviceBoundRegistrationResult {\r\n /** Credential ID */\r\n credentialId: Uint8Array;\r\n\r\n /** Public key */\r\n publicKey: Uint8Array;\r\n\r\n /** Device unlock key (use this to encrypt user's private key) */\r\n deviceUnlockKey: Uint8Array;\r\n\r\n /** Relying Party ID */\r\n rpId: string;\r\n\r\n /** Authenticator Attestation GUID - identifies the authenticator type */\r\n aaguid: string | null;\r\n\r\n /** Salt used for key derivation (needed to reconstruct binding key) */\r\n salt: Uint8Array;\r\n\r\n /** Encrypted device key (deviceUnlockKey encrypted with binding key) */\r\n encryptedDeviceKey: string;\r\n}\r\n\r\n/**\r\n * Register a new Windows Hello credential with device binding\r\n *\r\n * @param userId - User's Kratos identity ID\r\n * @param userEmail - User's email\r\n * @param excludeCredentials - Optional list of credential IDs to exclude (prevent duplicates)\r\n * @returns Registration result with device unlock key\r\n *\r\n * @example\r\n * ```ts\r\n * // During household setup on Windows\r\n * const result = await registerDeviceBoundCredential(\r\n * user.id,\r\n * user.traits.email\r\n * );\r\n *\r\n * // Use device unlock key to encrypt private key\r\n * const { key: deviceKey, salt } = await deriveDeviceKey(result.deviceUnlockKey);\r\n * const encrypted = await encryptWithDeviceKey(deviceKey, salt, privateKeyBytes);\r\n *\r\n * // Store on server\r\n * await api.post('/webauthn/device-bound-envelopes', {\r\n * credentialId: base64Encode(result.credentialId),\r\n * publicKey: base64Encode(result.publicKey),\r\n * encryptedPrivateKey: encrypted\r\n * });\r\n * ```\r\n */\r\nexport async function registerDeviceBoundCredential(\r\n userId: string,\r\n userEmail: string,\r\n excludeCredentials?: Uint8Array[]\r\n): Promise<DeviceBoundRegistrationResult> {\r\n // Generate device unlock key (this will encrypt the user's private key)\r\n const deviceUnlockKey = crypto.getRandomValues(new Uint8Array(32));\r\n\r\n // Generate salt for key derivation\r\n const salt = crypto.getRandomValues(new Uint8Array(32));\r\n\r\n // Build excludeCredentials list to prevent duplicates\r\n const excludeCredentialsList = excludeCredentials?.map(id => ({\r\n id: id as BufferSource,\r\n type: 'public-key' as const,\r\n transports: ['internal' as AuthenticatorTransport]\r\n })) || [];\r\n\r\n // Create WebAuthn credential\r\n const userIdBytes = new TextEncoder().encode(userId);\r\n\r\n const publicKeyOptions: PublicKeyCredentialCreationOptions = {\r\n challenge: crypto.getRandomValues(new Uint8Array(32)),\r\n\r\n rp: {\r\n id: RP_ID,\r\n name: RP_NAME\r\n },\r\n\r\n user: {\r\n id: userIdBytes,\r\n name: userEmail,\r\n displayName: userEmail\r\n },\r\n\r\n pubKeyCredParams: [\r\n { alg: -7, type: 'public-key' }, // ES256\r\n { alg: -257, type: 'public-key' } // RS256\r\n ],\r\n\r\n authenticatorSelection: {\r\n authenticatorAttachment: 'platform',\r\n // Use 'discouraged' to force local Windows Hello (non-syncing)\r\n // 'required' creates discoverable credentials which Windows treats as passkeys\r\n requireResidentKey: false,\r\n residentKey: 'discouraged',\r\n userVerification: 'required'\r\n },\r\n\r\n excludeCredentials: excludeCredentialsList,\r\n\r\n timeout: 60000\r\n };\r\n\r\n console.log('[DeviceBound] Creating credential...', {\r\n rpId: RP_ID,\r\n userId,\r\n userEmail,\r\n options: publicKeyOptions\r\n });\r\n\r\n let credential: Credential | null;\r\n try {\r\n credential = await navigator.credentials.create({\r\n publicKey: publicKeyOptions\r\n });\r\n } catch (error: any) {\r\n console.error('[DeviceBound] Credential creation failed:', error);\r\n\r\n // Provide more helpful error messages\r\n if (error.name === 'NotAllowedError') {\r\n throw new Error('Windows Hello prompt was cancelled or timed out. Please try again and approve the Windows Hello prompt when it appears.');\r\n } else if (error.name === 'SecurityError') {\r\n throw new Error('Security error - WebAuthn may not be available on this domain. Try using localhost or HTTPS.');\r\n } else if (error.name === 'InvalidStateError') {\r\n throw new Error('This credential already exists. Try clearing existing credentials first.');\r\n } else {\r\n throw new Error(`Failed to create device-bound credential: ${error.message || 'Unknown error'}`);\r\n }\r\n }\r\n\r\n if (!credential) {\r\n throw new Error('Credential creation returned null');\r\n }\r\n\r\n const publicKeyCredential = credential as PublicKeyCredential;\r\n const response = publicKeyCredential.response as AuthenticatorAttestationResponse;\r\n\r\n const credentialId = new Uint8Array(publicKeyCredential.rawId);\r\n const publicKey = new Uint8Array(response.getPublicKey() || []);\r\n\r\n if (publicKey.length === 0) {\r\n throw new Error('Failed to extract public key from credential');\r\n }\r\n\r\n // Extract aaguid from authenticatorData\r\n // Structure: rpIdHash (32) + flags (1) + signCount (4) + aaguid (16) + ...\r\n let aaguid: string | null = null;\r\n try {\r\n const authData = new Uint8Array(response.getAuthenticatorData());\r\n if (authData.length >= 53) {\r\n const aaguidBytes = authData.slice(37, 53);\r\n // Format as UUID string: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\r\n const hex = Array.from(aaguidBytes).map(b => b.toString(16).padStart(2, '0')).join('');\r\n aaguid = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\r\n console.log('[DeviceBound] Extracted aaguid:', aaguid);\r\n }\r\n } catch (err) {\r\n console.warn('[DeviceBound] Failed to extract aaguid:', err);\r\n }\r\n\r\n // Derive device binding key from credentialId + device fingerprint + salt\r\n const deviceBindingKey = await deriveDeviceBindingKey(credentialId, salt);\r\n\r\n // Encrypt device unlock key with device binding key\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n const encryptedDeviceKey = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n deviceBindingKey,\r\n deviceUnlockKey as BufferSource\r\n );\r\n\r\n // Pack encrypted key: [iv (12 bytes)][ciphertext + auth tag]\r\n const packed = new Uint8Array(12 + encryptedDeviceKey.byteLength);\r\n packed.set(iv, 0);\r\n packed.set(new Uint8Array(encryptedDeviceKey), 12);\r\n\r\n // Store in localStorage\r\n const storageData: DeviceBoundCredential = {\r\n credentialId: base64Encode(credentialId),\r\n salt: base64Encode(salt),\r\n encryptedDeviceKey: base64Encode(packed),\r\n publicKey: base64Encode(publicKey),\r\n rpId: RP_ID\r\n };\r\n\r\n localStorage.setItem(\r\n `${DEVICE_BOUND_STORAGE_PREFIX}${userId}`,\r\n JSON.stringify(storageData)\r\n );\r\n\r\n console.log('[DeviceBound] ✓ Credential registered and stored locally');\r\n\r\n return {\r\n credentialId,\r\n publicKey,\r\n deviceUnlockKey,\r\n rpId: RP_ID,\r\n aaguid,\r\n salt,\r\n encryptedDeviceKey: base64Encode(packed)\r\n };\r\n}\r\n\r\n/**\r\n * Register a trusted device (low-security fallback, no WebAuthn)\r\n * Stores device unlock key in localStorage - anyone with device access can unlock\r\n *\r\n * @param userId - User's Kratos identity ID\r\n * @param userEmail - User's email\r\n * @returns Registration result with device unlock key and fake credential info\r\n */\r\nexport async function registerTrustedDeviceCredential(\r\n userId: string,\r\n _userEmail: string\r\n): Promise<{\r\n credentialId: Uint8Array;\r\n publicKey: Uint8Array;\r\n deviceUnlockKey: Uint8Array;\r\n deviceId: string;\r\n rpId: string;\r\n}> {\r\n console.log('[TrustedDevice] Creating trusted device credential (low-security)...');\r\n\r\n // Generate device unlock key\r\n const deviceUnlockKey = crypto.getRandomValues(new Uint8Array(32));\r\n\r\n // Generate device ID (UUID-like)\r\n const deviceId = `trusted-${crypto.randomUUID()}`;\r\n\r\n // Generate fake credential ID (for API consistency)\r\n const credentialId = crypto.getRandomValues(new Uint8Array(16));\r\n\r\n // Generate fake public key (for API consistency)\r\n const publicKey = crypto.getRandomValues(new Uint8Array(32));\r\n\r\n // Store in localStorage (plaintext - this is low security!)\r\n const storageData = {\r\n deviceId,\r\n deviceUnlockKey: base64Encode(deviceUnlockKey),\r\n credentialId: base64Encode(credentialId),\r\n publicKey: base64Encode(publicKey),\r\n rpId: RP_ID,\r\n type: 'trusted-device'\r\n };\r\n\r\n localStorage.setItem(\r\n `${DEVICE_BOUND_STORAGE_PREFIX}${userId}`,\r\n JSON.stringify(storageData)\r\n );\r\n\r\n console.log('[TrustedDevice] ✓ Trusted device credential registered (stored in localStorage)');\r\n\r\n return {\r\n credentialId,\r\n publicKey,\r\n deviceUnlockKey,\r\n deviceId,\r\n rpId: RP_ID\r\n };\r\n}\r\n\r\n/**\r\n * Derive device binding key from credentialId + salt\r\n *\r\n * Note: Previously included device fingerprint, but that was too unstable\r\n * (changed with browser updates, screen resolution, etc.). Windows Hello\r\n * authentication itself already proves device identity via TPM.\r\n */\r\nasync function deriveDeviceBindingKey(\r\n credentialId: Uint8Array,\r\n salt: Uint8Array\r\n): Promise<CryptoKey> {\r\n // Import credentialId as key material\r\n // The credentialId is unique per device and tied to the TPM\r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n credentialId as BufferSource,\r\n 'HKDF',\r\n false,\r\n ['deriveKey']\r\n );\r\n\r\n // Derive AES-GCM key using HKDF\r\n // Using v2 info string since we removed fingerprint from derivation\r\n const bindingKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: salt as BufferSource,\r\n info: new TextEncoder().encode('hearthcoo-device-binding-v2')\r\n },\r\n keyMaterial,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false,\r\n ['encrypt', 'decrypt']\r\n );\r\n\r\n return bindingKey;\r\n}\r\n\r\n/**\r\n * Authentication result\r\n */\r\nexport interface DeviceBoundAuthenticationResult {\r\n /** Device unlock key (use this to decrypt user's private key) */\r\n deviceUnlockKey: Uint8Array;\r\n\r\n /** Signature (for server verification if needed, not present for trusted-device) */\r\n signature?: Uint8Array;\r\n\r\n /** Authenticator data (not present for trusted-device) */\r\n authenticatorData?: Uint8Array;\r\n\r\n /** Client data JSON (not present for trusted-device) */\r\n clientDataJSON?: Uint8Array;\r\n}\r\n\r\n/**\r\n * Authenticate with Windows Hello and get device unlock key\r\n *\r\n * @param userId - User's Kratos identity ID\r\n * @returns Authentication result with device unlock key\r\n *\r\n * @example\r\n * ```ts\r\n * try {\r\n * // User taps Windows Hello\r\n * const result = await authenticateDeviceBound(user.id);\r\n *\r\n * // Use device unlock key to decrypt private key (from server)\r\n * const privateKey = await decryptWithDeviceKey(\r\n * result.deviceUnlockKey,\r\n * encryptedPrivateKey\r\n * );\r\n * } catch (err) {\r\n * console.error('Windows Hello auth failed:', err);\r\n * }\r\n * ```\r\n */\r\nexport async function authenticateDeviceBound(\r\n userId: string\r\n): Promise<DeviceBoundAuthenticationResult> {\r\n // Load stored credential data\r\n const storageData = localStorage.getItem(`${DEVICE_BOUND_STORAGE_PREFIX}${userId}`);\r\n if (!storageData) {\r\n throw new Error('No device-bound credential found for this user. Please set up biometric unlock first.');\r\n }\r\n\r\n const credentialData = JSON.parse(storageData);\r\n\r\n // Check if this is a trusted-device (no WebAuthn, just localStorage)\r\n if (credentialData.type === 'trusted-device') {\r\n console.log('[TrustedDevice] Using trusted device unlock (no biometric prompt)');\r\n\r\n const deviceUnlockKey = base64Decode(credentialData.deviceUnlockKey);\r\n\r\n return {\r\n deviceUnlockKey\r\n };\r\n }\r\n\r\n // For real device-bound credentials, proceed with WebAuthn\r\n const credential: DeviceBoundCredential = credentialData;\r\n const credentialId = base64Decode(credential.credentialId);\r\n const salt = base64Decode(credential.salt);\r\n const encryptedDeviceKeyPacked = base64Decode(credential.encryptedDeviceKey);\r\n\r\n // Authenticate with WebAuthn\r\n const publicKeyOptions: PublicKeyCredentialRequestOptions = {\r\n challenge: crypto.getRandomValues(new Uint8Array(32)),\r\n rpId: RP_ID,\r\n allowCredentials: [{\r\n id: credentialId as BufferSource,\r\n type: 'public-key',\r\n transports: ['internal']\r\n }],\r\n timeout: 60000,\r\n userVerification: 'required'\r\n };\r\n\r\n console.log('[DeviceBound] Authenticating...', {\r\n credentialIdLength: credentialId.length\r\n });\r\n\r\n let authCredential: Credential | null;\r\n try {\r\n authCredential = await navigator.credentials.get({\r\n publicKey: publicKeyOptions\r\n });\r\n } catch (error) {\r\n console.error('[DeviceBound] Authentication failed:', error);\r\n throw new Error(`Failed to authenticate with device-bound credential: ${error instanceof Error ? error.message : 'Unknown error'}`);\r\n }\r\n\r\n if (!authCredential) {\r\n throw new Error('Authentication returned null');\r\n }\r\n\r\n const publicKeyCredential = authCredential as PublicKeyCredential;\r\n const response = publicKeyCredential.response as AuthenticatorAssertionResponse;\r\n\r\n // Derive device binding key (same as registration)\r\n const deviceBindingKey = await deriveDeviceBindingKey(credentialId, salt);\r\n\r\n // Decrypt device unlock key\r\n const iv = encryptedDeviceKeyPacked.slice(0, 12);\r\n const encryptedDeviceKey = encryptedDeviceKeyPacked.slice(12);\r\n\r\n let deviceUnlockKeyBuffer: ArrayBuffer;\r\n try {\r\n deviceUnlockKeyBuffer = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n deviceBindingKey,\r\n encryptedDeviceKey as BufferSource\r\n );\r\n } catch (error) {\r\n console.error('[DeviceBound] Failed to decrypt device unlock key:', error);\r\n throw new Error('Failed to decrypt device unlock key. Device fingerprint may have changed.');\r\n }\r\n\r\n const deviceUnlockKey = new Uint8Array(deviceUnlockKeyBuffer);\r\n\r\n console.log('[DeviceBound] ✓ Authentication successful, device unlock key retrieved');\r\n\r\n return {\r\n deviceUnlockKey,\r\n signature: new Uint8Array(response.signature),\r\n authenticatorData: new Uint8Array(response.authenticatorData),\r\n clientDataJSON: new Uint8Array(response.clientDataJSON)\r\n };\r\n}\r\n\r\n/**\r\n * Derive encryption key from device unlock key using HKDF\r\n *\r\n * Similar to derivePRFKey but for device-bound keys.\r\n *\r\n * @param deviceUnlockKey - Device unlock key from registration/authentication\r\n * @param salt - Optional salt for HKDF (default: random 32 bytes)\r\n * @param info - Context info string\r\n * @returns Object with derived key and the salt used\r\n */\r\nexport async function deriveDeviceKey(\r\n deviceUnlockKey: Uint8Array,\r\n salt?: Uint8Array,\r\n info: string = 'hearthcoo-device-key-v1'\r\n): Promise<{ key: CryptoKey; salt: Uint8Array }> {\r\n if (deviceUnlockKey.length !== 32) {\r\n throw new Error('Device unlock key must be 32 bytes');\r\n }\r\n\r\n // Use random salt for maximum security\r\n const hkdfSalt = salt || crypto.getRandomValues(new Uint8Array(32));\r\n\r\n // Import device unlock key as key material\r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n deviceUnlockKey as BufferSource,\r\n 'HKDF',\r\n false,\r\n ['deriveKey']\r\n );\r\n\r\n // Derive AES-GCM key using HKDF\r\n const deviceKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: hkdfSalt as BufferSource,\r\n info: new TextEncoder().encode(info)\r\n },\r\n keyMaterial,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false,\r\n ['encrypt', 'decrypt']\r\n );\r\n\r\n return { key: deviceKey, salt: hkdfSalt };\r\n}\r\n\r\n/**\r\n * Encrypt data with device-derived key\r\n *\r\n * @param deviceKey - Key derived from device unlock key\r\n * @param hkdfSalt - HKDF salt used to derive the key\r\n * @param data - Data to encrypt\r\n * @returns Encrypted blob with format: [version][salt][IV][ciphertext][auth_tag]\r\n */\r\nexport async function encryptWithDeviceKey<T>(\r\n deviceKey: CryptoKey,\r\n hkdfSalt: Uint8Array,\r\n data: T\r\n): Promise<string> {\r\n // Convert data to bytes\r\n let plaintextBytes: Uint8Array;\r\n\r\n if (data instanceof Uint8Array) {\r\n plaintextBytes = data;\r\n } else if (typeof data === 'string') {\r\n plaintextBytes = new TextEncoder().encode(data);\r\n } else {\r\n const plaintext = JSON.stringify(data);\r\n plaintextBytes = new TextEncoder().encode(plaintext);\r\n }\r\n\r\n // Generate IV\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n\r\n // Encrypt with AES-GCM\r\n const ciphertext = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n deviceKey,\r\n plaintextBytes as BufferSource\r\n );\r\n\r\n // Pack into blob: [version (1 byte)][salt (32 bytes)][IV (12 bytes)][ciphertext + auth tag]\r\n const version = new Uint8Array([1]); // Version 1\r\n const packed = new Uint8Array(1 + 32 + 12 + ciphertext.byteLength);\r\n packed.set(version, 0);\r\n packed.set(hkdfSalt, 1);\r\n packed.set(iv, 33);\r\n packed.set(new Uint8Array(ciphertext), 45);\r\n\r\n return base64Encode(packed);\r\n}\r\n\r\n/**\r\n * Decrypt data with device unlock key\r\n *\r\n * @param deviceUnlockKey - Device unlock key from authentication\r\n * @param encryptedBlob - Encrypted blob from encryptWithDeviceKey\r\n * @param returnRawBytes - If true, returns raw bytes without JSON parsing\r\n * @returns Decrypted data\r\n */\r\nexport async function decryptWithDeviceKey<T = any>(\r\n deviceUnlockKey: Uint8Array,\r\n encryptedBlob: string,\r\n returnRawBytes: boolean = false\r\n): Promise<T> {\r\n const packed = base64Decode(encryptedBlob);\r\n\r\n // Unpack blob\r\n const version = packed[0];\r\n\r\n if (version !== 1) {\r\n throw new Error(`Unsupported encryption version: ${version}`);\r\n }\r\n\r\n // Version 1 format: [version (1 byte)][salt (32 bytes)][IV (12 bytes)][ciphertext + auth tag]\r\n const hkdfSalt = packed.slice(1, 33);\r\n const iv = packed.slice(33, 45);\r\n const ciphertext = packed.slice(45);\r\n\r\n // Derive key using the same salt\r\n const { key: deviceKey } = await deriveDeviceKey(deviceUnlockKey, hkdfSalt);\r\n\r\n // Decrypt with AES-GCM\r\n const plaintextBytes = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n deviceKey,\r\n ciphertext as BufferSource\r\n );\r\n\r\n // Return raw bytes if requested\r\n if (returnRawBytes) {\r\n return new Uint8Array(plaintextBytes) as T;\r\n }\r\n\r\n // Otherwise decode and parse JSON\r\n const plaintext = new TextDecoder().decode(plaintextBytes);\r\n\r\n try {\r\n return JSON.parse(plaintext) as T;\r\n } catch {\r\n return plaintext as unknown as T;\r\n }\r\n}\r\n\r\n/**\r\n * Check if user has a device-bound credential registered\r\n */\r\nexport function hasDeviceBoundCredential(userId: string): boolean {\r\n return localStorage.getItem(`${DEVICE_BOUND_STORAGE_PREFIX}${userId}`) !== null;\r\n}\r\n\r\n/**\r\n * Remove device-bound credential\r\n */\r\nexport function removeDeviceBoundCredential(userId: string): void {\r\n localStorage.removeItem(`${DEVICE_BOUND_STORAGE_PREFIX}${userId}`);\r\n console.log('[DeviceBound] Credential removed');\r\n}\r\n\r\n/**\r\n * Detect if running in Remote Desktop session\r\n * RDP sessions can't access Windows Hello biometrics\r\n */\r\nfunction isRemoteDesktopSession(): boolean {\r\n // Check for Remote Desktop indicators\r\n // Note: Not 100% reliable, but catches most cases\r\n\r\n // Check for RDP-specific screen properties\r\n if (screen.width === 1024 && screen.height === 768) {\r\n // Common RDP default resolution (not definitive)\r\n console.log('[DeviceBound] Possible RDP session detected (screen resolution)');\r\n }\r\n\r\n // Check for Virtual Channel indicators in user agent\r\n const ua = navigator.userAgent.toLowerCase();\r\n if (ua.includes('rdp') || ua.includes('remote desktop')) {\r\n console.log('[DeviceBound] RDP detected in user agent');\r\n return true;\r\n }\r\n\r\n // Check for console/headless indicators\r\n if (!navigator.webdriver && screen.colorDepth === 24 && screen.width < 1280) {\r\n console.log('[DeviceBound] Possible remote session detected (screen properties)');\r\n }\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * Check if platform supports WebAuthn (basic check)\r\n * Also detects and warns about Remote Desktop sessions\r\n */\r\nexport async function detectDeviceBoundSupport(): Promise<boolean> {\r\n try {\r\n if (!window.PublicKeyCredential) {\r\n console.log('[DeviceBound] PublicKeyCredential not available');\r\n return false;\r\n }\r\n\r\n const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();\r\n if (!available) {\r\n console.log('[DeviceBound] Platform authenticator not available');\r\n return false;\r\n }\r\n\r\n console.log('[DeviceBound] Platform authenticator available');\r\n\r\n // Warn if in RDP session\r\n if (isRemoteDesktopSession()) {\r\n console.warn('[DeviceBound] ⚠️ Remote Desktop session detected - Windows Hello biometrics will not work over RDP');\r\n }\r\n\r\n return true;\r\n } catch (error) {\r\n console.error('[DeviceBound] Error detecting support:', error);\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Check if running in a Remote Desktop session\r\n * Exported so UI can show warnings\r\n */\r\nexport function isInRemoteDesktopSession(): boolean {\r\n return isRemoteDesktopSession();\r\n}\r\n","/**\r\n * Configuration and Constants\r\n *\r\n * @module estatehelm/config\r\n */\r\n\r\nimport envPaths from 'env-paths'\r\nimport * as path from 'path'\r\nimport * as fs from 'fs'\r\n\r\n// Platform-specific paths\r\nconst paths = envPaths('estatehelm', { suffix: '' })\r\n\r\n/**\r\n * Application data directory\r\n * - macOS: ~/Library/Application Support/estatehelm/\r\n * - Windows: C:\\Users\\<name>\\AppData\\Roaming\\estatehelm\\\r\n * - Linux: ~/.local/share/estatehelm/\r\n */\r\nexport const DATA_DIR = paths.data\r\n\r\n/**\r\n * SQLite cache database path\r\n */\r\nexport const CACHE_DB_PATH = path.join(DATA_DIR, 'cache.db')\r\n\r\n/**\r\n * Configuration file path\r\n */\r\nexport const CONFIG_PATH = path.join(DATA_DIR, 'config.json')\r\n\r\n/**\r\n * Device ID file path\r\n */\r\nexport const DEVICE_ID_PATH = path.join(DATA_DIR, '.device-id')\r\n\r\n/**\r\n * Keytar service name for credential storage\r\n */\r\nexport const KEYTAR_SERVICE = 'estatehelm'\r\n\r\n/**\r\n * Keytar account names\r\n */\r\nexport const KEYTAR_ACCOUNTS = {\r\n BEARER_TOKEN: 'bearer-token',\r\n REFRESH_TOKEN: 'refresh-token',\r\n DEVICE_CREDENTIALS: 'device-credentials',\r\n} as const\r\n\r\n/**\r\n * API base URL\r\n */\r\nexport const API_BASE_URL = process.env.ESTATEHELM_API_URL || 'https://api.estatehelm.com'\r\n\r\n/**\r\n * App (frontend) URL for browser-based authentication\r\n */\r\nexport const APP_URL = process.env.ESTATEHELM_APP_URL || 'https://app.estatehelm.com'\r\n\r\n/**\r\n * Privacy mode\r\n */\r\nexport type PrivacyMode = 'full' | 'safe'\r\n\r\n/**\r\n * User configuration\r\n */\r\nexport interface UserConfig {\r\n /** Default privacy mode */\r\n defaultMode: PrivacyMode\r\n /** Last used household ID */\r\n lastHouseholdId?: string\r\n}\r\n\r\n/**\r\n * Default user configuration\r\n */\r\nconst DEFAULT_CONFIG: UserConfig = {\r\n defaultMode: 'full',\r\n}\r\n\r\n/**\r\n * Ensure data directory exists\r\n */\r\nexport function ensureDataDir(): void {\r\n if (!fs.existsSync(DATA_DIR)) {\r\n fs.mkdirSync(DATA_DIR, { recursive: true })\r\n }\r\n}\r\n\r\n/**\r\n * Load user configuration\r\n */\r\nexport function loadConfig(): UserConfig {\r\n ensureDataDir()\r\n try {\r\n if (fs.existsSync(CONFIG_PATH)) {\r\n const data = fs.readFileSync(CONFIG_PATH, 'utf-8')\r\n return { ...DEFAULT_CONFIG, ...JSON.parse(data) }\r\n }\r\n } catch (err) {\r\n console.warn('[Config] Failed to load config:', err)\r\n }\r\n return DEFAULT_CONFIG\r\n}\r\n\r\n/**\r\n * Save user configuration\r\n */\r\nexport function saveConfig(config: UserConfig): void {\r\n ensureDataDir()\r\n fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2))\r\n}\r\n\r\n/**\r\n * Get or generate device ID\r\n */\r\nexport function getDeviceId(): string {\r\n ensureDataDir()\r\n try {\r\n if (fs.existsSync(DEVICE_ID_PATH)) {\r\n return fs.readFileSync(DEVICE_ID_PATH, 'utf-8').trim()\r\n }\r\n } catch {\r\n // Generate new ID\r\n }\r\n\r\n // Generate a random device ID\r\n const id = `mcp-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`\r\n fs.writeFileSync(DEVICE_ID_PATH, id)\r\n return id\r\n}\r\n\r\n/**\r\n * Get device platform string\r\n */\r\nexport function getDevicePlatform(): string {\r\n const platform = process.platform\r\n switch (platform) {\r\n case 'darwin':\r\n return 'macOS'\r\n case 'win32':\r\n return 'Windows'\r\n case 'linux':\r\n return 'Linux'\r\n default:\r\n return platform\r\n }\r\n}\r\n\r\n/**\r\n * Get device user agent string\r\n */\r\nexport function getDeviceUserAgent(): string {\r\n return `estatehelm-mcp/1.0 (${getDevicePlatform()})`\r\n}\r\n\r\n/**\r\n * Sanitize token for logging (show first/last 4 chars)\r\n */\r\nexport function sanitizeToken(token: string): string {\r\n if (token.length <= 8) return '***'\r\n return `${token.slice(0, 4)}...${token.slice(-4)}`\r\n}\r\n\r\n/**\r\n * Log a request (safe for production)\r\n */\r\nexport function logRequest(endpoint: string, status: number): void {\r\n console.log(`[${new Date().toISOString()}] ${endpoint} → ${status}`)\r\n}\r\n","/**\r\n * OS Keychain Wrapper\r\n *\r\n * Uses keytar for secure credential storage in the OS keychain.\r\n * - macOS: Keychain Access\r\n * - Windows: Credential Manager\r\n * - Linux: Secret Service (GNOME Keyring / KWallet)\r\n *\r\n * @module estatehelm/keyStore\r\n */\r\n\r\nimport keytar from 'keytar'\r\nimport { KEYTAR_SERVICE, KEYTAR_ACCOUNTS, sanitizeToken } from './config.js'\r\n\r\n/**\r\n * Stored credentials\r\n */\r\nexport interface StoredCredentials {\r\n /** Bearer token for API calls */\r\n bearerToken: string\r\n /** Refresh token for token renewal */\r\n refreshToken: string\r\n /** Device credential data (JSON) */\r\n deviceCredentials: {\r\n credentialId: string\r\n encryptedPayload: string\r\n privateKeyBytes: string // Base64 encoded\r\n }\r\n}\r\n\r\n/**\r\n * Save bearer token to keychain\r\n */\r\nexport async function saveBearerToken(token: string): Promise<void> {\r\n await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.BEARER_TOKEN, token)\r\n console.log(`[KeyStore] Saved bearer token: ${sanitizeToken(token)}`)\r\n}\r\n\r\n/**\r\n * Get bearer token from keychain\r\n */\r\nexport async function getBearerToken(): Promise<string | null> {\r\n return keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.BEARER_TOKEN)\r\n}\r\n\r\n/**\r\n * Save refresh token to keychain\r\n */\r\nexport async function saveRefreshToken(token: string): Promise<void> {\r\n await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.REFRESH_TOKEN, token)\r\n console.log(`[KeyStore] Saved refresh token: ${sanitizeToken(token)}`)\r\n}\r\n\r\n/**\r\n * Get refresh token from keychain\r\n */\r\nexport async function getRefreshToken(): Promise<string | null> {\r\n return keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.REFRESH_TOKEN)\r\n}\r\n\r\n/**\r\n * Save device credentials to keychain\r\n */\r\nexport async function saveDeviceCredentials(credentials: StoredCredentials['deviceCredentials']): Promise<void> {\r\n const json = JSON.stringify(credentials)\r\n await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.DEVICE_CREDENTIALS, json)\r\n console.log(`[KeyStore] Saved device credentials`)\r\n}\r\n\r\n/**\r\n * Get device credentials from keychain\r\n */\r\nexport async function getDeviceCredentials(): Promise<StoredCredentials['deviceCredentials'] | null> {\r\n const json = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.DEVICE_CREDENTIALS)\r\n if (!json) return null\r\n try {\r\n return JSON.parse(json)\r\n } catch {\r\n console.warn('[KeyStore] Failed to parse device credentials')\r\n return null\r\n }\r\n}\r\n\r\n/**\r\n * Check if credentials are stored\r\n */\r\nexport async function hasCredentials(): Promise<boolean> {\r\n const [bearer, device] = await Promise.all([\r\n getBearerToken(),\r\n getDeviceCredentials(),\r\n ])\r\n return !!(bearer && device)\r\n}\r\n\r\n/**\r\n * Get all stored credentials\r\n */\r\nexport async function getCredentials(): Promise<StoredCredentials | null> {\r\n const [bearerToken, refreshToken, deviceCredentials] = await Promise.all([\r\n getBearerToken(),\r\n getRefreshToken(),\r\n getDeviceCredentials(),\r\n ])\r\n\r\n if (!bearerToken || !deviceCredentials) {\r\n return null\r\n }\r\n\r\n return {\r\n bearerToken,\r\n refreshToken: refreshToken || '',\r\n deviceCredentials,\r\n }\r\n}\r\n\r\n/**\r\n * Clear all credentials from keychain\r\n */\r\nexport async function clearCredentials(): Promise<void> {\r\n await Promise.all([\r\n keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.BEARER_TOKEN),\r\n keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.REFRESH_TOKEN),\r\n keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNTS.DEVICE_CREDENTIALS),\r\n ])\r\n console.log('[KeyStore] Cleared all credentials')\r\n}\r\n","/**\r\n * MCP Server\r\n *\r\n * Implements the Model Context Protocol server using @modelcontextprotocol/sdk.\r\n * Exposes EstateHelm data as resources and tools for AI assistants.\r\n *\r\n * @module estatehelm/server\r\n */\r\n\r\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js'\r\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\r\nimport {\r\n CallToolRequestSchema,\r\n ListResourcesRequestSchema,\r\n ListToolsRequestSchema,\r\n ReadResourceRequestSchema,\r\n ListPromptsRequestSchema,\r\n GetPromptRequestSchema,\r\n} from '@modelcontextprotocol/sdk/types.js'\r\nimport type { PrivacyMode } from './config.js'\r\nimport { loadConfig } from './config.js'\r\nimport { getAuthenticatedClient, getPrivateKey } from './login.js'\r\nimport { redactEntity } from './filter.js'\r\nimport { initCache, syncIfNeeded, getDecryptedEntities, getHouseholds } from './cache.js'\r\n\r\n/**\r\n * Start the MCP server\r\n */\r\nexport async function startServer(mode?: PrivacyMode): Promise<void> {\r\n // Load config\r\n const config = loadConfig()\r\n const privacyMode = mode || config.defaultMode\r\n\r\n console.error(`[MCP] Starting server in ${privacyMode} mode`)\r\n\r\n // Verify login\r\n const client = await getAuthenticatedClient()\r\n if (!client) {\r\n console.error('[MCP] Not logged in. Run: estatehelm login')\r\n process.exit(1)\r\n }\r\n\r\n const privateKey = await getPrivateKey()\r\n if (!privateKey) {\r\n console.error('[MCP] Failed to load encryption keys. Run: estatehelm login')\r\n process.exit(1)\r\n }\r\n\r\n // Initialize cache\r\n await initCache()\r\n\r\n // Sync on startup\r\n console.error('[MCP] Checking for updates...')\r\n const synced = await syncIfNeeded(client, privateKey)\r\n if (synced) {\r\n console.error('[MCP] Cache updated')\r\n } else {\r\n console.error('[MCP] Cache is up to date')\r\n }\r\n\r\n // Create MCP server\r\n const server = new Server(\r\n {\r\n name: 'estatehelm',\r\n version: '1.0.0',\r\n },\r\n {\r\n capabilities: {\r\n resources: {},\r\n tools: {},\r\n prompts: {},\r\n },\r\n }\r\n )\r\n\r\n // List available resources\r\n server.setRequestHandler(ListResourcesRequestSchema, async () => {\r\n const households = await getHouseholds()\r\n\r\n const resources = [\r\n {\r\n uri: 'estatehelm://households',\r\n name: 'All Households',\r\n description: 'List of all households you have access to',\r\n mimeType: 'application/json',\r\n },\r\n ]\r\n\r\n // Add household-specific resources\r\n for (const household of households) {\r\n resources.push({\r\n uri: `estatehelm://households/${household.id}`,\r\n name: household.name,\r\n description: `Household: ${household.name}`,\r\n mimeType: 'application/json',\r\n })\r\n\r\n // Add entity type resources for each household\r\n const entityTypes = [\r\n 'pet', 'property', 'vehicle', 'contact', 'insurance', 'bank_account',\r\n 'investment', 'subscription', 'maintenance_task', 'password', 'access_code',\r\n 'document', 'medical', 'prescription', 'credential', 'utility',\r\n ]\r\n\r\n for (const type of entityTypes) {\r\n resources.push({\r\n uri: `estatehelm://households/${household.id}/${type}`,\r\n name: `${household.name} - ${formatEntityType(type)}`,\r\n description: `${formatEntityType(type)} in ${household.name}`,\r\n mimeType: 'application/json',\r\n })\r\n }\r\n }\r\n\r\n return { resources }\r\n })\r\n\r\n // Read resource content\r\n server.setRequestHandler(ReadResourceRequestSchema, async (request) => {\r\n const uri = request.params.uri\r\n const parsed = parseResourceUri(uri)\r\n\r\n if (!parsed) {\r\n throw new Error(`Invalid resource URI: ${uri}`)\r\n }\r\n\r\n let content: any\r\n\r\n if (parsed.type === 'households' && !parsed.householdId) {\r\n // List all households\r\n content = await getHouseholds()\r\n } else if (parsed.type === 'households' && parsed.householdId && !parsed.entityType) {\r\n // Get specific household\r\n const households = await getHouseholds()\r\n content = households.find((h) => h.id === parsed.householdId)\r\n if (!content) {\r\n throw new Error(`Household not found: ${parsed.householdId}`)\r\n }\r\n } else if (parsed.householdId && parsed.entityType) {\r\n // Get entities of a type for a household\r\n const entities = await getDecryptedEntities(parsed.householdId, parsed.entityType, privateKey)\r\n content = entities.map((e) => redactEntity(e, parsed.entityType!, privacyMode))\r\n } else {\r\n throw new Error(`Unsupported resource: ${uri}`)\r\n }\r\n\r\n return {\r\n contents: [\r\n {\r\n uri,\r\n mimeType: 'application/json',\r\n text: JSON.stringify(content, null, 2),\r\n },\r\n ],\r\n }\r\n })\r\n\r\n // List available tools\r\n server.setRequestHandler(ListToolsRequestSchema, async () => {\r\n return {\r\n tools: [\r\n {\r\n name: 'search_entities',\r\n description: 'Search across all entities in EstateHelm',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n query: {\r\n type: 'string',\r\n description: 'Search query',\r\n },\r\n householdId: {\r\n type: 'string',\r\n description: 'Optional: Limit search to a specific household',\r\n },\r\n entityType: {\r\n type: 'string',\r\n description: 'Optional: Limit search to a specific entity type',\r\n },\r\n },\r\n required: ['query'],\r\n },\r\n },\r\n {\r\n name: 'get_household_summary',\r\n description: 'Get a summary of a household including counts and key dates',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n householdId: {\r\n type: 'string',\r\n description: 'The household ID',\r\n },\r\n },\r\n required: ['householdId'],\r\n },\r\n },\r\n {\r\n name: 'get_expiring_items',\r\n description: 'Get items expiring within a given number of days',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n days: {\r\n type: 'number',\r\n description: 'Number of days to look ahead (default: 30)',\r\n },\r\n householdId: {\r\n type: 'string',\r\n description: 'Optional: Limit to a specific household',\r\n },\r\n },\r\n },\r\n },\r\n {\r\n name: 'refresh',\r\n description: 'Force refresh of cached data from the server',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {},\r\n },\r\n },\r\n ],\r\n }\r\n })\r\n\r\n // Handle tool calls\r\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\r\n const { name, arguments: args } = request.params\r\n\r\n switch (name) {\r\n case 'search_entities': {\r\n const { query, householdId, entityType } = args as {\r\n query: string\r\n householdId?: string\r\n entityType?: string\r\n }\r\n\r\n // Get all households to search\r\n const households = await getHouseholds()\r\n const searchHouseholds = householdId\r\n ? households.filter((h) => h.id === householdId)\r\n : households\r\n\r\n const results: any[] = []\r\n\r\n for (const household of searchHouseholds) {\r\n const entityTypes = entityType\r\n ? [entityType]\r\n : ['pet', 'property', 'vehicle', 'contact', 'insurance', 'bank_account',\r\n 'investment', 'subscription', 'maintenance_task', 'password', 'access_code']\r\n\r\n for (const type of entityTypes) {\r\n const entities = await getDecryptedEntities(household.id, type, privateKey)\r\n const matches = entities.filter((e) => searchEntity(e, query))\r\n\r\n for (const match of matches) {\r\n results.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n entityType: type,\r\n entity: redactEntity(match, type, privacyMode),\r\n })\r\n }\r\n }\r\n }\r\n\r\n return {\r\n content: [\r\n {\r\n type: 'text',\r\n text: JSON.stringify(results, null, 2),\r\n },\r\n ],\r\n }\r\n }\r\n\r\n case 'get_household_summary': {\r\n const { householdId } = args as { householdId: string }\r\n\r\n const households = await getHouseholds()\r\n const household = households.find((h) => h.id === householdId)\r\n if (!household) {\r\n throw new Error(`Household not found: ${householdId}`)\r\n }\r\n\r\n const entityTypes = ['pet', 'property', 'vehicle', 'contact', 'insurance', 'bank_account',\r\n 'investment', 'subscription', 'maintenance_task', 'password', 'access_code']\r\n\r\n const counts: Record<string, number> = {}\r\n for (const type of entityTypes) {\r\n const entities = await getDecryptedEntities(householdId, type, privateKey)\r\n counts[type] = entities.length\r\n }\r\n\r\n const summary = {\r\n household: {\r\n id: household.id,\r\n name: household.name,\r\n },\r\n counts,\r\n totalEntities: Object.values(counts).reduce((a, b) => a + b, 0),\r\n }\r\n\r\n return {\r\n content: [\r\n {\r\n type: 'text',\r\n text: JSON.stringify(summary, null, 2),\r\n },\r\n ],\r\n }\r\n }\r\n\r\n case 'get_expiring_items': {\r\n const { days = 30, householdId } = args as { days?: number; householdId?: string }\r\n\r\n const households = await getHouseholds()\r\n const searchHouseholds = householdId\r\n ? households.filter((h) => h.id === householdId)\r\n : households\r\n\r\n const now = new Date()\r\n const cutoff = new Date(now.getTime() + days * 24 * 60 * 60 * 1000)\r\n\r\n const expiring: any[] = []\r\n\r\n for (const household of searchHouseholds) {\r\n // Check insurance expirations\r\n const insurance = await getDecryptedEntities(household.id, 'insurance', privateKey)\r\n for (const policy of insurance) {\r\n if (policy.expirationDate) {\r\n const expires = new Date(policy.expirationDate)\r\n if (expires <= cutoff) {\r\n expiring.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n type: 'insurance',\r\n name: policy.name || policy.policyNumber,\r\n expiresAt: policy.expirationDate,\r\n daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)),\r\n })\r\n }\r\n }\r\n }\r\n\r\n // Check vehicle registrations\r\n const vehicles = await getDecryptedEntities(household.id, 'vehicle', privateKey)\r\n for (const vehicle of vehicles) {\r\n if (vehicle.registrationExpiration) {\r\n const expires = new Date(vehicle.registrationExpiration)\r\n if (expires <= cutoff) {\r\n expiring.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n type: 'vehicle_registration',\r\n name: `${vehicle.year || ''} ${vehicle.make || ''} ${vehicle.model || ''}`.trim(),\r\n expiresAt: vehicle.registrationExpiration,\r\n daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)),\r\n })\r\n }\r\n }\r\n }\r\n\r\n // Check subscriptions\r\n const subscriptions = await getDecryptedEntities(household.id, 'subscription', privateKey)\r\n for (const sub of subscriptions) {\r\n if (sub.renewalDate) {\r\n const renews = new Date(sub.renewalDate)\r\n if (renews <= cutoff) {\r\n expiring.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n type: 'subscription',\r\n name: sub.name || sub.serviceName,\r\n expiresAt: sub.renewalDate,\r\n daysUntil: Math.ceil((renews.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)),\r\n })\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Sort by days until expiration\r\n expiring.sort((a, b) => a.daysUntil - b.daysUntil)\r\n\r\n return {\r\n content: [\r\n {\r\n type: 'text',\r\n text: JSON.stringify(expiring, null, 2),\r\n },\r\n ],\r\n }\r\n }\r\n\r\n case 'refresh': {\r\n const synced = await syncIfNeeded(client!, privateKey!, true)\r\n return {\r\n content: [\r\n {\r\n type: 'text',\r\n text: synced ? 'Cache refreshed with latest data' : 'Cache was already up to date',\r\n },\r\n ],\r\n }\r\n }\r\n\r\n default:\r\n throw new Error(`Unknown tool: ${name}`)\r\n }\r\n })\r\n\r\n // List available prompts\r\n server.setRequestHandler(ListPromptsRequestSchema, async () => {\r\n return {\r\n prompts: [\r\n {\r\n name: 'household_summary',\r\n description: 'Get an overview of a household',\r\n arguments: [\r\n {\r\n name: 'householdId',\r\n description: 'Optional household ID (uses first household if not specified)',\r\n required: false,\r\n },\r\n ],\r\n },\r\n {\r\n name: 'expiring_soon',\r\n description: 'Show items expiring soon',\r\n arguments: [\r\n {\r\n name: 'days',\r\n description: 'Number of days to look ahead (default: 30)',\r\n required: false,\r\n },\r\n ],\r\n },\r\n {\r\n name: 'emergency_contacts',\r\n description: 'Show emergency contacts',\r\n arguments: [],\r\n },\r\n ],\r\n }\r\n })\r\n\r\n // Handle prompt requests\r\n server.setRequestHandler(GetPromptRequestSchema, async (request) => {\r\n const { name, arguments: args } = request.params\r\n\r\n switch (name) {\r\n case 'household_summary': {\r\n const householdId = args?.householdId\r\n const households = await getHouseholds()\r\n const household = householdId\r\n ? households.find((h) => h.id === householdId)\r\n : households[0]\r\n\r\n if (!household) {\r\n throw new Error('No household found')\r\n }\r\n\r\n return {\r\n messages: [\r\n {\r\n role: 'user',\r\n content: {\r\n type: 'text',\r\n text: `Please give me an overview of my household \"${household.name}\". Include counts of different items, any upcoming expirations, and notable information.`,\r\n },\r\n },\r\n ],\r\n }\r\n }\r\n\r\n case 'expiring_soon': {\r\n const days = args?.days || 30\r\n return {\r\n messages: [\r\n {\r\n role: 'user',\r\n content: {\r\n type: 'text',\r\n text: `What items are expiring in the next ${days} days? Include insurance policies, vehicle registrations, subscriptions, and any other items with expiration dates.`,\r\n },\r\n },\r\n ],\r\n }\r\n }\r\n\r\n case 'emergency_contacts': {\r\n return {\r\n messages: [\r\n {\r\n role: 'user',\r\n content: {\r\n type: 'text',\r\n text: 'Show me all emergency contacts across my households. Include their names, phone numbers, and relationship to the household.',\r\n },\r\n },\r\n ],\r\n }\r\n }\r\n\r\n default:\r\n throw new Error(`Unknown prompt: ${name}`)\r\n }\r\n })\r\n\r\n // Start server with stdio transport\r\n const transport = new StdioServerTransport()\r\n await server.connect(transport)\r\n console.error('[MCP] Server started')\r\n}\r\n\r\n/**\r\n * Parse a resource URI\r\n */\r\nfunction parseResourceUri(uri: string): {\r\n type: string\r\n householdId?: string\r\n entityType?: string\r\n entityId?: string\r\n} | null {\r\n const match = uri.match(/^estatehelm:\\/\\/([^/]+)(?:\\/([^/]+))?(?:\\/([^/]+))?(?:\\/([^/]+))?$/)\r\n if (!match) return null\r\n\r\n const [, type, householdId, entityType, entityId] = match\r\n return { type, householdId, entityType, entityId }\r\n}\r\n\r\n/**\r\n * Format entity type for display\r\n */\r\nfunction formatEntityType(type: string): string {\r\n return type\r\n .split('_')\r\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\r\n .join(' ')\r\n}\r\n\r\n/**\r\n * Search an entity for a query string\r\n */\r\nfunction searchEntity(entity: Record<string, any>, query: string): boolean {\r\n const lowerQuery = query.toLowerCase()\r\n\r\n const searchFields = ['name', 'title', 'description', 'notes', 'make', 'model',\r\n 'policyNumber', 'serviceName', 'username', 'email']\r\n\r\n for (const field of searchFields) {\r\n if (entity[field] && String(entity[field]).toLowerCase().includes(lowerQuery)) {\r\n return true\r\n }\r\n }\r\n\r\n return false\r\n}\r\n","/**\r\n * Safe Mode Field Redaction\r\n *\r\n * Implements field-level redaction for privacy when sharing screen\r\n * or when uncertain about who might see the data.\r\n *\r\n * @module estatehelm/filter\r\n */\r\n\r\nimport type { PrivacyMode } from './config.js'\r\n\r\n/**\r\n * Fields to redact by entity type in safe mode\r\n */\r\nconst REDACTION_RULES: Record<string, string[]> = {\r\n // Password entries\r\n password: ['password', 'notes'],\r\n\r\n // Identity documents\r\n identity: ['password', 'recoveryKey', 'securityAnswers'],\r\n\r\n // Financial accounts\r\n bank_account: ['accountNumber', 'routingNumber'],\r\n investment: ['accountNumber'],\r\n\r\n // Access codes\r\n access_code: ['code', 'pin'],\r\n\r\n // Credentials (show last 4 of document number)\r\n credential: ['documentNumber'],\r\n}\r\n\r\n/**\r\n * Fields that should show partial value (last 4 characters)\r\n */\r\nconst PARTIAL_REDACTION_FIELDS = ['documentNumber', 'accountNumber']\r\n\r\n/**\r\n * Redacted value placeholder\r\n */\r\nconst REDACTED = '[REDACTED]'\r\n\r\n/**\r\n * Redact sensitive fields from an entity\r\n *\r\n * @param entity - The entity to redact\r\n * @param entityType - The type of entity\r\n * @param mode - Privacy mode ('full' or 'safe')\r\n * @returns The entity with sensitive fields redacted\r\n */\r\nexport function redactEntity<T extends Record<string, any>>(\r\n entity: T,\r\n entityType: string,\r\n mode: PrivacyMode\r\n): T {\r\n // Full mode returns entity as-is\r\n if (mode === 'full') {\r\n return entity\r\n }\r\n\r\n // Get fields to redact for this entity type\r\n const fieldsToRedact = REDACTION_RULES[entityType] || []\r\n if (fieldsToRedact.length === 0) {\r\n return entity\r\n }\r\n\r\n // Create a copy to avoid mutating original\r\n const redacted: Record<string, any> = { ...entity }\r\n\r\n for (const field of fieldsToRedact) {\r\n if (field in redacted && redacted[field] != null) {\r\n const value = redacted[field]\r\n\r\n // Partial redaction for certain fields (show last 4)\r\n if (PARTIAL_REDACTION_FIELDS.includes(field) && typeof value === 'string' && value.length > 4) {\r\n redacted[field] = `****${value.slice(-4)}`\r\n } else {\r\n redacted[field] = REDACTED\r\n }\r\n }\r\n }\r\n\r\n return redacted as T\r\n}\r\n\r\n/**\r\n * Redact an array of entities\r\n *\r\n * @param entities - The entities to redact\r\n * @param entityType - The type of entities\r\n * @param mode - Privacy mode\r\n * @returns The entities with sensitive fields redacted\r\n */\r\nexport function redactEntities<T extends Record<string, any>>(\r\n entities: T[],\r\n entityType: string,\r\n mode: PrivacyMode\r\n): T[] {\r\n return entities.map((entity) => redactEntity(entity, entityType, mode))\r\n}\r\n\r\n/**\r\n * Check if an entity type has any redaction rules\r\n */\r\nexport function hasRedactionRules(entityType: string): boolean {\r\n return entityType in REDACTION_RULES\r\n}\r\n\r\n/**\r\n * Get the list of sensitive entity types\r\n */\r\nexport function getSensitiveEntityTypes(): string[] {\r\n return Object.keys(REDACTION_RULES)\r\n}\r\n\r\n/**\r\n * Describe what fields are redacted for an entity type\r\n */\r\nexport function describeRedactions(entityType: string): string[] {\r\n return REDACTION_RULES[entityType] || []\r\n}\r\n","/**\r\n * SQLite Cache Store Implementation\r\n *\r\n * Implements CacheStore interface using better-sqlite3 for Node.js environments.\r\n * This is used by the MCP server for local caching.\r\n *\r\n * @module @hearthcoo/cache-sqlite\r\n */\r\n\r\nimport Database from 'better-sqlite3'\r\nimport type {\r\n CacheStore,\r\n CacheMetadata,\r\n CachedEntity,\r\n EntityCacheEntry,\r\n CachedAttachment,\r\n EncryptedKeyCache,\r\n OfflineCredential,\r\n CachedHouseholdMembers,\r\n CacheStats,\r\n} from '@hearthcoo/cache'\r\nimport { DB_VERSION, makeCacheKey, makeMembersCacheKey } from '@hearthcoo/cache'\r\nimport * as fs from 'fs'\r\nimport * as path from 'path'\r\n\r\n/**\r\n * SQLite implementation of CacheStore\r\n */\r\nexport class SqliteCacheStore implements CacheStore {\r\n private db: Database.Database\r\n\r\n constructor(dbPath: string) {\r\n // Ensure directory exists\r\n const dir = path.dirname(dbPath)\r\n if (!fs.existsSync(dir)) {\r\n fs.mkdirSync(dir, { recursive: true })\r\n }\r\n\r\n this.db = new Database(dbPath)\r\n this.db.pragma('journal_mode = WAL')\r\n this.initializeSchema()\r\n }\r\n\r\n private initializeSchema(): void {\r\n // Check current schema version\r\n const versionRow = this.db.prepare(\r\n \"SELECT value FROM metadata WHERE key = 'schema_version'\"\r\n ).get() as { value: string } | undefined\r\n\r\n const currentVersion = versionRow ? parseInt(versionRow.value, 10) : 0\r\n\r\n if (currentVersion < DB_VERSION) {\r\n this.migrate(currentVersion)\r\n }\r\n }\r\n\r\n private migrate(fromVersion: number): void {\r\n console.log(`[SqliteCache] Migrating from version ${fromVersion} to ${DB_VERSION}`)\r\n\r\n // Create tables if they don't exist\r\n this.db.exec(`\r\n -- Metadata table (key-value store)\r\n CREATE TABLE IF NOT EXISTS metadata (\r\n key TEXT PRIMARY KEY,\r\n value TEXT NOT NULL\r\n );\r\n\r\n -- Cache metadata (user/household info)\r\n CREATE TABLE IF NOT EXISTS cache_metadata (\r\n id TEXT PRIMARY KEY DEFAULT 'metadata',\r\n household_id TEXT,\r\n user_id TEXT,\r\n user_identity TEXT, -- JSON\r\n last_full_sync TEXT,\r\n last_changelog_id INTEGER DEFAULT 0,\r\n offline_enabled INTEGER DEFAULT 0,\r\n created_at TEXT\r\n );\r\n\r\n -- Credentials for offline unlock\r\n CREATE TABLE IF NOT EXISTS credentials (\r\n user_id TEXT PRIMARY KEY,\r\n credential_id TEXT NOT NULL,\r\n prf_input TEXT NOT NULL,\r\n cached_at TEXT NOT NULL\r\n );\r\n\r\n -- Encrypted key cache\r\n CREATE TABLE IF NOT EXISTS keys (\r\n user_id TEXT PRIMARY KEY,\r\n household_id TEXT NOT NULL,\r\n iv TEXT NOT NULL,\r\n encrypted_data TEXT NOT NULL,\r\n cached_at TEXT NOT NULL\r\n );\r\n\r\n -- Entity cache\r\n CREATE TABLE IF NOT EXISTS entities (\r\n cache_key TEXT PRIMARY KEY,\r\n household_id TEXT NOT NULL,\r\n entity_type TEXT NOT NULL,\r\n items TEXT NOT NULL, -- JSON array\r\n last_sync TEXT NOT NULL,\r\n expected_count INTEGER,\r\n changelog_id INTEGER\r\n );\r\n\r\n -- Attachment cache\r\n CREATE TABLE IF NOT EXISTS attachments (\r\n file_id TEXT PRIMARY KEY,\r\n encrypted_data BLOB NOT NULL,\r\n entity_id TEXT NOT NULL,\r\n entity_type TEXT NOT NULL,\r\n mime_type TEXT NOT NULL,\r\n key_type TEXT NOT NULL,\r\n version INTEGER NOT NULL,\r\n cached_at TEXT NOT NULL,\r\n crypto_version INTEGER,\r\n key_derivation_id TEXT\r\n );\r\n\r\n CREATE INDEX IF NOT EXISTS idx_attachments_entity_id ON attachments(entity_id);\r\n `)\r\n\r\n // Update schema version\r\n this.db.prepare(\r\n \"INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)\"\r\n ).run(DB_VERSION.toString())\r\n\r\n console.log('[SqliteCache] Migration complete')\r\n }\r\n\r\n // ============================================================================\r\n // Metadata Operations\r\n // ============================================================================\r\n\r\n async getMetadata(): Promise<CacheMetadata | null> {\r\n const row = this.db.prepare(\r\n 'SELECT * FROM cache_metadata WHERE id = ?'\r\n ).get('metadata') as any\r\n\r\n if (!row) return null\r\n\r\n return {\r\n householdId: row.household_id,\r\n userId: row.user_id,\r\n userIdentity: row.user_identity ? JSON.parse(row.user_identity) : undefined,\r\n lastFullSync: row.last_full_sync,\r\n lastChangelogId: row.last_changelog_id,\r\n offlineEnabled: !!row.offline_enabled,\r\n createdAt: row.created_at,\r\n }\r\n }\r\n\r\n async saveMetadata(metadata: CacheMetadata): Promise<void> {\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO cache_metadata\r\n (id, household_id, user_id, user_identity, last_full_sync, last_changelog_id, offline_enabled, created_at)\r\n VALUES (?, ?, ?, ?, ?, ?, ?, ?)\r\n `).run(\r\n 'metadata',\r\n metadata.householdId,\r\n metadata.userId,\r\n metadata.userIdentity ? JSON.stringify(metadata.userIdentity) : null,\r\n metadata.lastFullSync,\r\n metadata.lastChangelogId,\r\n metadata.offlineEnabled ? 1 : 0,\r\n metadata.createdAt\r\n )\r\n }\r\n\r\n async getLastChangelogId(): Promise<number> {\r\n const metadata = await this.getMetadata()\r\n return metadata?.lastChangelogId ?? 0\r\n }\r\n\r\n async updateLastChangelogId(\r\n changelogId: number,\r\n householdId?: string,\r\n userId?: string\r\n ): Promise<void> {\r\n const existing = await this.getMetadata()\r\n\r\n if (existing) {\r\n this.db.prepare(\r\n 'UPDATE cache_metadata SET last_changelog_id = ? WHERE id = ?'\r\n ).run(changelogId, 'metadata')\r\n } else if (householdId && userId) {\r\n await this.saveMetadata({\r\n householdId,\r\n userId,\r\n lastFullSync: null,\r\n lastChangelogId: changelogId,\r\n offlineEnabled: false,\r\n createdAt: new Date().toISOString(),\r\n })\r\n }\r\n }\r\n\r\n // ============================================================================\r\n // Credential Operations\r\n // ============================================================================\r\n\r\n async hasOfflineCredential(userId: string): Promise<boolean> {\r\n const row = this.db.prepare(\r\n 'SELECT 1 FROM credentials WHERE user_id = ?'\r\n ).get(userId)\r\n return !!row\r\n }\r\n\r\n async getOfflineCredential(userId: string): Promise<OfflineCredential | null> {\r\n const row = this.db.prepare(\r\n 'SELECT * FROM credentials WHERE user_id = ?'\r\n ).get(userId) as any\r\n\r\n if (!row) return null\r\n\r\n return {\r\n userId: row.user_id,\r\n credentialId: row.credential_id,\r\n prfInput: row.prf_input,\r\n cachedAt: row.cached_at,\r\n }\r\n }\r\n\r\n async saveOfflineCredential(credential: OfflineCredential): Promise<void> {\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO credentials (user_id, credential_id, prf_input, cached_at)\r\n VALUES (?, ?, ?, ?)\r\n `).run(\r\n credential.userId,\r\n credential.credentialId,\r\n credential.prfInput,\r\n credential.cachedAt\r\n )\r\n console.log('[SqliteCache] Saved offline credential for user:', credential.userId)\r\n }\r\n\r\n async removeOfflineCredential(userId: string): Promise<void> {\r\n this.db.prepare('DELETE FROM credentials WHERE user_id = ?').run(userId)\r\n }\r\n\r\n // ============================================================================\r\n // Key Cache Operations\r\n // ============================================================================\r\n\r\n async getKeyCache(userId: string): Promise<EncryptedKeyCache | null> {\r\n const row = this.db.prepare(\r\n 'SELECT * FROM keys WHERE user_id = ?'\r\n ).get(userId) as any\r\n\r\n if (!row) return null\r\n\r\n return {\r\n userId: row.user_id,\r\n householdId: row.household_id,\r\n iv: row.iv,\r\n encryptedData: row.encrypted_data,\r\n cachedAt: row.cached_at,\r\n }\r\n }\r\n\r\n async saveKeyCache(keyCache: EncryptedKeyCache): Promise<void> {\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO keys (user_id, household_id, iv, encrypted_data, cached_at)\r\n VALUES (?, ?, ?, ?, ?)\r\n `).run(\r\n keyCache.userId,\r\n keyCache.householdId,\r\n keyCache.iv,\r\n keyCache.encryptedData,\r\n keyCache.cachedAt\r\n )\r\n console.log('[SqliteCache] Saved encrypted keys for user:', keyCache.userId)\r\n }\r\n\r\n async removeKeyCache(userId: string): Promise<void> {\r\n this.db.prepare('DELETE FROM keys WHERE user_id = ?').run(userId)\r\n }\r\n\r\n // ============================================================================\r\n // Entity Cache Operations\r\n // ============================================================================\r\n\r\n async getEntityCache(\r\n householdId: string,\r\n entityType: string\r\n ): Promise<EntityCacheEntry | null> {\r\n const cacheKey = makeCacheKey(householdId, entityType)\r\n\r\n const row = this.db.prepare(\r\n 'SELECT * FROM entities WHERE cache_key = ?'\r\n ).get(cacheKey) as any\r\n\r\n if (!row) return null\r\n\r\n const entry: EntityCacheEntry = {\r\n cacheKey: row.cache_key,\r\n householdId: row.household_id,\r\n entityType: row.entity_type,\r\n items: JSON.parse(row.items),\r\n lastSync: row.last_sync,\r\n expectedCount: row.expected_count,\r\n changelogId: row.changelog_id,\r\n }\r\n\r\n // Validate cache integrity\r\n if (entry.expectedCount === undefined || entry.items.length !== entry.expectedCount) {\r\n return null\r\n }\r\n\r\n if (entry.changelogId === undefined) {\r\n return null\r\n }\r\n\r\n return entry\r\n }\r\n\r\n async getAllEntityCaches(householdId?: string): Promise<EntityCacheEntry[]> {\r\n const query = householdId\r\n ? 'SELECT * FROM entities WHERE household_id = ?'\r\n : 'SELECT * FROM entities'\r\n\r\n const rows = householdId\r\n ? this.db.prepare(query).all(householdId) as any[]\r\n : this.db.prepare(query).all() as any[]\r\n\r\n return rows.map(row => ({\r\n cacheKey: row.cache_key,\r\n householdId: row.household_id,\r\n entityType: row.entity_type,\r\n items: JSON.parse(row.items),\r\n lastSync: row.last_sync,\r\n expectedCount: row.expected_count,\r\n changelogId: row.changelog_id,\r\n }))\r\n }\r\n\r\n async saveEntityCache(\r\n householdId: string,\r\n entityType: string,\r\n items: CachedEntity[],\r\n changelogId: number\r\n ): Promise<void> {\r\n const cacheKey = makeCacheKey(householdId, entityType)\r\n\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO entities\r\n (cache_key, household_id, entity_type, items, last_sync, expected_count, changelog_id)\r\n VALUES (?, ?, ?, ?, ?, ?, ?)\r\n `).run(\r\n cacheKey,\r\n householdId,\r\n entityType,\r\n JSON.stringify(items),\r\n new Date().toISOString(),\r\n items.length,\r\n changelogId\r\n )\r\n }\r\n\r\n async updateEntityInCache(\r\n householdId: string,\r\n entity: CachedEntity\r\n ): Promise<boolean> {\r\n const { entityType } = entity\r\n const cacheKey = makeCacheKey(householdId, entityType)\r\n\r\n const existing = await this.getEntityCache(householdId, entityType)\r\n\r\n const items = existing?.items || []\r\n const existingIndex = items.findIndex(e => e.id === entity.id)\r\n const isUpdate = existingIndex >= 0\r\n\r\n if (isUpdate) {\r\n items[existingIndex] = entity\r\n } else {\r\n items.push(entity)\r\n }\r\n\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO entities\r\n (cache_key, household_id, entity_type, items, last_sync, expected_count, changelog_id)\r\n VALUES (?, ?, ?, ?, ?, ?, ?)\r\n `).run(\r\n cacheKey,\r\n householdId,\r\n entityType,\r\n JSON.stringify(items),\r\n new Date().toISOString(),\r\n items.length,\r\n existing?.changelogId ?? null\r\n )\r\n\r\n return isUpdate\r\n }\r\n\r\n async removeEntityFromCache(\r\n householdId: string,\r\n entityType: string,\r\n entityId: string\r\n ): Promise<void> {\r\n const cacheKey = makeCacheKey(householdId, entityType)\r\n\r\n const existing = await this.getEntityCache(householdId, entityType)\r\n if (!existing) return\r\n\r\n const items = existing.items.filter(e => e.id !== entityId)\r\n\r\n this.db.prepare(`\r\n UPDATE entities SET items = ?, expected_count = ?, last_sync = ? WHERE cache_key = ?\r\n `).run(\r\n JSON.stringify(items),\r\n items.length,\r\n new Date().toISOString(),\r\n cacheKey\r\n )\r\n }\r\n\r\n async getEntityVersions(\r\n householdId: string,\r\n entityType: string\r\n ): Promise<Map<string, number>> {\r\n const cache = await this.getEntityCache(householdId, entityType)\r\n const versions = new Map<string, number>()\r\n\r\n if (cache) {\r\n for (const entity of cache.items) {\r\n versions.set(entity.id, entity.version)\r\n }\r\n }\r\n\r\n return versions\r\n }\r\n\r\n // ============================================================================\r\n // Household Members Cache\r\n // ============================================================================\r\n\r\n async getHouseholdMembersCache(\r\n householdId: string\r\n ): Promise<CachedHouseholdMembers | null> {\r\n const cacheKey = makeMembersCacheKey(householdId)\r\n\r\n const row = this.db.prepare(\r\n 'SELECT * FROM entities WHERE cache_key = ?'\r\n ).get(cacheKey) as any\r\n\r\n if (!row) return null\r\n\r\n const items = JSON.parse(row.items)\r\n if (!items.members) return null\r\n\r\n return {\r\n householdId: row.household_id,\r\n members: items.members,\r\n cachedAt: items.cachedAt || row.last_sync,\r\n }\r\n }\r\n\r\n async saveHouseholdMembersCache(\r\n householdId: string,\r\n members: any[]\r\n ): Promise<void> {\r\n const cacheKey = makeMembersCacheKey(householdId)\r\n const cachedAt = new Date().toISOString()\r\n\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO entities\r\n (cache_key, household_id, entity_type, items, last_sync, expected_count)\r\n VALUES (?, ?, ?, ?, ?, ?)\r\n `).run(\r\n cacheKey,\r\n householdId,\r\n '_members',\r\n JSON.stringify({ members, cachedAt }),\r\n cachedAt,\r\n members.length\r\n )\r\n }\r\n\r\n // ============================================================================\r\n // Attachment Cache Operations\r\n // ============================================================================\r\n\r\n async getAttachmentCache(fileId: string): Promise<CachedAttachment | null> {\r\n const row = this.db.prepare(\r\n 'SELECT * FROM attachments WHERE file_id = ?'\r\n ).get(fileId) as any\r\n\r\n if (!row) return null\r\n\r\n return {\r\n fileId: row.file_id,\r\n encryptedData: row.encrypted_data,\r\n entityId: row.entity_id,\r\n entityType: row.entity_type,\r\n mimeType: row.mime_type,\r\n keyType: row.key_type,\r\n version: row.version,\r\n cachedAt: row.cached_at,\r\n cryptoVersion: row.crypto_version,\r\n keyDerivationId: row.key_derivation_id,\r\n }\r\n }\r\n\r\n async saveAttachmentCache(attachment: CachedAttachment): Promise<void> {\r\n // Convert Blob to Buffer if needed\r\n let data: Buffer\r\n if (attachment.encryptedData instanceof Buffer) {\r\n data = attachment.encryptedData\r\n } else if (attachment.encryptedData instanceof Blob) {\r\n const arrayBuffer = await attachment.encryptedData.arrayBuffer()\r\n data = Buffer.from(arrayBuffer)\r\n } else {\r\n data = Buffer.from(attachment.encryptedData as any)\r\n }\r\n\r\n this.db.prepare(`\r\n INSERT OR REPLACE INTO attachments\r\n (file_id, encrypted_data, entity_id, entity_type, mime_type, key_type, version, cached_at, crypto_version, key_derivation_id)\r\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\r\n `).run(\r\n attachment.fileId,\r\n data,\r\n attachment.entityId,\r\n attachment.entityType,\r\n attachment.mimeType,\r\n attachment.keyType,\r\n attachment.version,\r\n attachment.cachedAt,\r\n attachment.cryptoVersion ?? null,\r\n attachment.keyDerivationId ?? null\r\n )\r\n console.log('[SqliteCache] Cached attachment:', attachment.fileId)\r\n }\r\n\r\n async removeAttachmentCache(fileId: string): Promise<void> {\r\n this.db.prepare('DELETE FROM attachments WHERE file_id = ?').run(fileId)\r\n }\r\n\r\n async getAttachmentsForEntity(entityId: string): Promise<CachedAttachment[]> {\r\n const rows = this.db.prepare(\r\n 'SELECT * FROM attachments WHERE entity_id = ?'\r\n ).all(entityId) as any[]\r\n\r\n return rows.map(row => ({\r\n fileId: row.file_id,\r\n encryptedData: row.encrypted_data,\r\n entityId: row.entity_id,\r\n entityType: row.entity_type,\r\n mimeType: row.mime_type,\r\n keyType: row.key_type,\r\n version: row.version,\r\n cachedAt: row.cached_at,\r\n cryptoVersion: row.crypto_version,\r\n keyDerivationId: row.key_derivation_id,\r\n }))\r\n }\r\n\r\n // ============================================================================\r\n // Cache Management\r\n // ============================================================================\r\n\r\n async clearAllCache(): Promise<void> {\r\n this.db.exec(`\r\n DELETE FROM cache_metadata;\r\n DELETE FROM credentials;\r\n DELETE FROM keys;\r\n DELETE FROM entities;\r\n DELETE FROM attachments;\r\n `)\r\n console.log('[SqliteCache] All cache data cleared')\r\n }\r\n\r\n async clearUserCache(userId: string): Promise<void> {\r\n this.db.exec('DELETE FROM cache_metadata')\r\n this.db.prepare('DELETE FROM credentials WHERE user_id = ?').run(userId)\r\n this.db.prepare('DELETE FROM keys WHERE user_id = ?').run(userId)\r\n this.db.exec('DELETE FROM entities')\r\n this.db.exec('DELETE FROM attachments')\r\n console.log('[SqliteCache] User cache cleared:', userId)\r\n }\r\n\r\n async isOfflineCacheAvailable(userId: string): Promise<boolean> {\r\n const [metadata, credential, keys] = await Promise.all([\r\n this.getMetadata(),\r\n this.getOfflineCredential(userId),\r\n this.getKeyCache(userId),\r\n ])\r\n\r\n return !!(\r\n metadata &&\r\n metadata.offlineEnabled &&\r\n metadata.userId === userId &&\r\n credential &&\r\n keys\r\n )\r\n }\r\n\r\n async getCacheStats(): Promise<CacheStats> {\r\n const metadata = await this.getMetadata()\r\n const entityCaches = await this.getAllEntityCaches()\r\n\r\n const attachmentCount = (this.db.prepare(\r\n 'SELECT COUNT(*) as count FROM attachments'\r\n ).get() as any).count\r\n\r\n return {\r\n entityTypes: entityCaches.length,\r\n totalEntities: entityCaches.reduce((sum, cache) => sum + cache.items.length, 0),\r\n attachments: attachmentCount,\r\n lastSync: metadata?.lastFullSync || null,\r\n }\r\n }\r\n\r\n async close(): Promise<void> {\r\n this.db.close()\r\n console.log('[SqliteCache] Database closed')\r\n }\r\n}\r\n","/**\r\n * Cache Schema Constants\r\n *\r\n * Shared schema definitions for offline cache storage.\r\n *\r\n * @module @hearthcoo/cache\r\n */\r\n\r\n/**\r\n * Database/schema version\r\n * Increment when making breaking changes to the cache schema\r\n */\r\nexport const DB_VERSION = 3\r\n\r\n/**\r\n * Database name (for IndexedDB)\r\n */\r\nexport const DB_NAME = 'estatehelm-offline'\r\n\r\n/**\r\n * Object store / table names\r\n */\r\nexport const STORES = {\r\n METADATA: 'metadata',\r\n CREDENTIALS: 'credentials',\r\n KEYS: 'keys',\r\n ENTITIES: 'entities',\r\n ATTACHMENTS: 'attachments',\r\n} as const\r\n\r\n/**\r\n * Generate cache key for entity store\r\n * Format: householdId:entityType\r\n */\r\nexport function makeCacheKey(householdId: string, entityType: string): string {\r\n return `${householdId}:${entityType}`\r\n}\r\n\r\n/**\r\n * Parse cache key back to components\r\n */\r\nexport function parseCacheKey(cacheKey: string): {\r\n householdId: string\r\n entityType: string\r\n} {\r\n const [householdId, entityType] = cacheKey.split(':')\r\n return { householdId, entityType }\r\n}\r\n\r\n/**\r\n * Generate special cache key for household members\r\n * Format: _members_{householdId}\r\n */\r\nexport function makeMembersCacheKey(householdId: string): string {\r\n return `_members_${householdId}`\r\n}\r\n","/**\r\n * Cache Management\r\n *\r\n * Handles SQLite caching and synchronization with the EstateHelm API.\r\n *\r\n * @module estatehelm/cache\r\n */\r\n\r\nimport { SqliteCacheStore } from '@hearthcoo/cache-sqlite'\r\nimport type { CachedEntity } from '@hearthcoo/cache'\r\nimport type { ApiClient } from '@hearthcoo/api-client'\r\n// Use mobile export to avoid frontend-specific dependencies\r\nimport {\r\n unwrapHouseholdKey,\r\n decryptEntity,\r\n unpackEncryptedBlob,\r\n getKeyTypeForEntity,\r\n base64Encode,\r\n} from '@hearthcoo/encryption/mobile'\r\nimport type { EncryptedEntity } from '@hearthcoo/encryption/mobile'\r\nimport { CACHE_DB_PATH } from './config.js'\r\n\r\n// Singleton cache store\r\nlet cacheStore: SqliteCacheStore | null = null\r\n\r\n// In-memory cache for decrypted entities (session only)\r\nconst decryptedCache = new Map<string, any>()\r\n\r\n// In-memory cache for household keys (raw bytes)\r\nconst householdKeysCache = new Map<string, Uint8Array>()\r\n\r\n// Households from API\r\nlet householdsCache: Array<{ id: string; name: string }> = []\r\n\r\n/**\r\n * Initialize the cache store\r\n */\r\nexport async function initCache(): Promise<void> {\r\n if (!cacheStore) {\r\n cacheStore = new SqliteCacheStore(CACHE_DB_PATH)\r\n console.error(`[Cache] Initialized at ${CACHE_DB_PATH}`)\r\n }\r\n}\r\n\r\n/**\r\n * Get the cache store\r\n */\r\nexport function getCache(): SqliteCacheStore {\r\n if (!cacheStore) {\r\n throw new Error('Cache not initialized. Call initCache() first.')\r\n }\r\n return cacheStore\r\n}\r\n\r\n/**\r\n * Close the cache\r\n */\r\nexport async function closeCache(): Promise<void> {\r\n if (cacheStore) {\r\n await cacheStore.close()\r\n cacheStore = null\r\n }\r\n decryptedCache.clear()\r\n householdKeysCache.clear()\r\n}\r\n\r\n/**\r\n * Clear the cache\r\n */\r\nexport async function clearCache(): Promise<void> {\r\n const cache = getCache()\r\n await cache.clearAllCache()\r\n decryptedCache.clear()\r\n householdKeysCache.clear()\r\n householdsCache = []\r\n console.error('[Cache] Cleared')\r\n}\r\n\r\n/**\r\n * Sync cache if server has changes\r\n *\r\n * @param client - API client\r\n * @param privateKey - User's private key for decrypting household keys\r\n * @param force - Force sync even if no changes detected\r\n * @returns true if sync was performed\r\n */\r\nexport async function syncIfNeeded(\r\n client: ApiClient,\r\n privateKey: CryptoKey,\r\n force = false\r\n): Promise<boolean> {\r\n const cache = getCache()\r\n\r\n // Fetch households\r\n const households = await client.getHouseholds()\r\n householdsCache = households.map((h) => ({ id: h.id, name: h.name }))\r\n\r\n let synced = false\r\n\r\n for (const household of households) {\r\n // Load household keys\r\n await loadHouseholdKeys(client, household.id, privateKey)\r\n\r\n // Check changelog\r\n const localChangelogId = await cache.getLastChangelogId()\r\n\r\n if (!force && localChangelogId > 0) {\r\n // Check if server has changes\r\n try {\r\n const response = await client.get<{ latestChangelogId: number }>(\r\n `/households/${household.id}/sync/changes?latestOnly=true`\r\n )\r\n\r\n if (response.latestChangelogId <= localChangelogId) {\r\n console.error(`[Cache] Household ${household.id} up to date (changelog ${localChangelogId})`)\r\n continue\r\n }\r\n\r\n console.error(`[Cache] Household ${household.id} has changes (${localChangelogId} -> ${response.latestChangelogId})`)\r\n } catch (err) {\r\n console.error(`[Cache] Failed to check changelog for ${household.id}:`, err)\r\n continue\r\n }\r\n }\r\n\r\n // Sync all entity types\r\n await syncHousehold(client, household.id, cache)\r\n synced = true\r\n }\r\n\r\n return synced\r\n}\r\n\r\n/**\r\n * Sync all entities for a household\r\n */\r\nasync function syncHousehold(\r\n client: ApiClient,\r\n householdId: string,\r\n cache: SqliteCacheStore\r\n): Promise<void> {\r\n const entityTypes = [\r\n 'pet', 'property', 'vehicle', 'contact', 'insurance', 'bank_account',\r\n 'investment', 'subscription', 'maintenance_task', 'password', 'access_code',\r\n 'document', 'medical', 'prescription', 'credential', 'utility',\r\n ]\r\n\r\n let latestChangelogId = 0\r\n\r\n for (const entityType of entityTypes) {\r\n try {\r\n const response = await client.getEntities(householdId, { entityType, batched: false })\r\n const items = response.items || []\r\n\r\n // Transform to CachedEntity format\r\n const cachedItems: CachedEntity[] = items.map((item: any) => ({\r\n id: item.id,\r\n entityType: item.entityType,\r\n encryptedData: item.encryptedData,\r\n keyType: item.keyType,\r\n householdId: item.householdId,\r\n ownerUserId: item.ownerUserId,\r\n version: item.version,\r\n createdAt: item.createdAt,\r\n updatedAt: item.updatedAt,\r\n cachedAt: new Date().toISOString(),\r\n }))\r\n\r\n // Get current changelog ID\r\n const changelogResponse = await client.get<{ latestChangelogId: number }>(\r\n `/households/${householdId}/sync/changes?latestOnly=true`\r\n )\r\n latestChangelogId = Math.max(latestChangelogId, changelogResponse.latestChangelogId)\r\n\r\n await cache.saveEntityCache(householdId, entityType, cachedItems, latestChangelogId)\r\n console.error(`[Cache] Synced ${items.length} ${entityType}(s) for household ${householdId}`)\r\n } catch (err: any) {\r\n // 404 means no entities of this type - that's fine\r\n if (err.status !== 404) {\r\n console.error(`[Cache] Failed to sync ${entityType} for ${householdId}:`, err.message)\r\n }\r\n }\r\n }\r\n\r\n // Update changelog ID\r\n await cache.updateLastChangelogId(latestChangelogId, householdId)\r\n}\r\n\r\n/**\r\n * Load and cache household keys\r\n */\r\nasync function loadHouseholdKeys(\r\n client: ApiClient,\r\n householdId: string,\r\n privateKey: CryptoKey\r\n): Promise<void> {\r\n // Check if already loaded\r\n if (householdKeysCache.has(`${householdId}:general`)) {\r\n return\r\n }\r\n\r\n try {\r\n const keys = await client.getHouseholdKeys(householdId)\r\n\r\n for (const key of keys) {\r\n const cacheKey = `${householdId}:${key.keyType}`\r\n\r\n // Skip if already loaded\r\n if (householdKeysCache.has(cacheKey)) {\r\n continue\r\n }\r\n\r\n // Unwrap the household key (returns raw bytes)\r\n const householdKeyBytes = await unwrapHouseholdKey(\r\n key.encryptedKey,\r\n privateKey\r\n )\r\n\r\n householdKeysCache.set(cacheKey, householdKeyBytes)\r\n }\r\n\r\n console.error(`[Cache] Loaded ${keys.length} keys for household ${householdId}`)\r\n } catch (err) {\r\n console.error(`[Cache] Failed to load keys for household ${householdId}:`, err)\r\n throw err\r\n }\r\n}\r\n\r\n/**\r\n * Get household key for a specific key type\r\n */\r\nfunction getHouseholdKey(householdId: string, keyType: string): Uint8Array | undefined {\r\n return householdKeysCache.get(`${householdId}:${keyType}`)\r\n}\r\n\r\n/**\r\n * Get decrypted entities from cache\r\n */\r\nexport async function getDecryptedEntities(\r\n householdId: string,\r\n entityType: string,\r\n privateKey: CryptoKey\r\n): Promise<any[]> {\r\n const cache = getCache()\r\n const cacheEntry = await cache.getEntityCache(householdId, entityType)\r\n\r\n if (!cacheEntry || cacheEntry.items.length === 0) {\r\n return []\r\n }\r\n\r\n const results: any[] = []\r\n\r\n for (const item of cacheEntry.items) {\r\n const decryptCacheKey = `${householdId}:${entityType}:${item.id}`\r\n\r\n // Check in-memory cache first\r\n if (decryptedCache.has(decryptCacheKey)) {\r\n results.push(decryptedCache.get(decryptCacheKey))\r\n continue\r\n }\r\n\r\n try {\r\n // Get the appropriate household key\r\n const keyType = getKeyTypeForEntity(entityType)\r\n const householdKeyBytes = getHouseholdKey(householdId, keyType)\r\n\r\n if (!householdKeyBytes) {\r\n console.error(`[Cache] No key for ${householdId}:${keyType}`)\r\n continue\r\n }\r\n\r\n // Unpack the encrypted blob to get IV and ciphertext\r\n const { iv, ciphertext } = unpackEncryptedBlob(item.encryptedData)\r\n\r\n // Construct EncryptedEntity\r\n const encryptedEntity: EncryptedEntity = {\r\n entityId: item.id,\r\n entityType: item.entityType,\r\n keyType: item.keyType,\r\n ciphertext: base64Encode(ciphertext),\r\n iv: base64Encode(iv),\r\n encryptedAt: new Date(item.createdAt),\r\n derivedEntityKey: new Uint8Array(),\r\n }\r\n\r\n // Decrypt entity\r\n const decrypted = await decryptEntity<Record<string, any>>(\r\n householdKeyBytes,\r\n encryptedEntity\r\n )\r\n\r\n // Add metadata\r\n const entity = {\r\n ...decrypted,\r\n id: item.id,\r\n entityType: item.entityType,\r\n householdId: item.householdId,\r\n version: item.version,\r\n createdAt: item.createdAt,\r\n updatedAt: item.updatedAt,\r\n }\r\n\r\n // Cache in memory\r\n decryptedCache.set(decryptCacheKey, entity)\r\n results.push(entity)\r\n } catch (err) {\r\n console.error(`[Cache] Failed to decrypt ${entityType}:${item.id}:`, err)\r\n }\r\n }\r\n\r\n return results\r\n}\r\n\r\n/**\r\n * Get cached households\r\n */\r\nexport async function getHouseholds(): Promise<Array<{ id: string; name: string }>> {\r\n return householdsCache\r\n}\r\n\r\n/**\r\n * Get cache statistics\r\n */\r\nexport async function getCacheStats(): Promise<{\r\n entityTypes: number\r\n totalEntities: number\r\n attachments: number\r\n lastSync: string | null\r\n}> {\r\n const cache = getCache()\r\n return cache.getCacheStats()\r\n}\r\n"],"mappings":"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","names":["path","envPaths","keytar","keytar","open","import_types","fs","path","Database","synced"]}