estatehelm 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -22
- package/dist/index.js +2731 -381
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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, setServerUrls, API_BASE_URL, APP_URL } 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 .option('--staging', 'Use staging environment (previewapi/previewapp.estatehelm.com)')\r\n .option('--api-url <url>', 'API server URL (default: https://api.estatehelm.com)')\r\n .option('--app-url <url>', 'App server URL (default: https://app.estatehelm.com)')\r\n .hook('preAction', (thisCommand) => {\r\n const opts = thisCommand.opts()\r\n if (opts.staging) {\r\n setServerUrls(\r\n 'https://previewapi.estatehelm.com',\r\n 'https://previewapp.estatehelm.com',\r\n 'https://stauth.estatehelm.com'\r\n )\r\n } else if (opts.apiUrl || opts.appUrl) {\r\n setServerUrls(opts.apiUrl, opts.appUrl)\r\n }\r\n })\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 native Kratos login flow with OIDC (Google/Apple).\r\n * Uses local callback server to capture session token from Kratos redirect.\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 (dynamic)\r\n */\r\nasync function findAvailablePort(): Promise<number> {\r\n return new Promise((resolve, reject) => {\r\n const server = http.createServer()\r\n // Port 0 = let OS assign an available port\r\n server.listen(0, '127.0.0.1', () => {\r\n const address = server.address()\r\n const port = typeof address === 'object' && address ? address.port : 0\r\n server.close(() => {\r\n if (port > 0) {\r\n resolve(port)\r\n } else {\r\n reject(new Error('Failed to find available port'))\r\n }\r\n })\r\n })\r\n server.on('error', reject)\r\n })\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 * Wait for callback with session token from CLIAuthPage\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\r\n const sessionToken = url.searchParams.get('session_token')\r\n const error = url.searchParams.get('error')\r\n\r\n if (error) {\r\n res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' })\r\n res.end(`\r\n <!DOCTYPE html>\r\n <html>\r\n <head><meta charset=\"utf-8\"><title>Authentication Failed</title>\r\n <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#fef2f2}.card{background:white;padding:2rem;border-radius:1rem;box-shadow:0 10px 25px rgba(0,0,0,0.1);text-align:center}h1{color:#dc2626}</style></head>\r\n <body><div class=\"card\"><h1>Authentication Failed</h1><p>${url.searchParams.get('error_description') || error}</p></div></body>\r\n </html>\r\n `)\r\n server.close()\r\n reject(new Error(url.searchParams.get('error_description') || error))\r\n return\r\n }\r\n\r\n if (sessionToken) {\r\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })\r\n res.end(`\r\n <!DOCTYPE html>\r\n <html>\r\n <head><meta charset=\"utf-8\"><title>Authentication Successful</title>\r\n <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:linear-gradient(115deg,#fff1be 28%,#ee87cb 70%,#b060ff 100%)}.card{background:white;padding:2rem;border-radius:1rem;box-shadow:0 10px 25px rgba(0,0,0,0.1);text-align:center}h1{color:#059669}</style></head>\r\n <body><div class=\"card\"><h1>✓ Authentication Successful</h1><p>You can close this window and return to your terminal.</p></div></body>\r\n </html>\r\n `)\r\n server.close()\r\n resolve(sessionToken)\r\n return\r\n }\r\n\r\n // Ignore other requests (favicon, etc.)\r\n res.writeHead(404)\r\n res.end()\r\n })\r\n\r\n server.listen(port, '127.0.0.1')\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'))\r\n }, 5 * 60 * 1000)\r\n })\r\n}\r\n\r\n/**\r\n * Login via web app (same pattern as Android Apple Sign-In)\r\n *\r\n * Flow:\r\n * 1. CLI opens browser to web app's /cli-auth page\r\n * 2. If not logged in, user is redirected to sign-in (Google or Apple)\r\n * 3. After auth, /cli-auth gets session token from backend\r\n * 4. /cli-auth redirects to CLI's localhost callback with token\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 login URL - web app handles OAuth, then redirects back with token\r\n const loginUrl = `${APP_URL}/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:\\n${loginUrl}\\n`)\r\n\r\n // Start callback server\r\n const tokenPromise = waitForCallback(port)\r\n\r\n // Open browser to web app\r\n await open(loginUrl)\r\n\r\n // Wait for session token\r\n console.log('Waiting for authentication...')\r\n const sessionToken = await tokenPromise\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 // Kratos sessions don't use refresh tokens - save placeholder to satisfy keychain\r\n await saveRefreshToken('kratos-session-no-refresh')\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('\\nTo use with Claude Code, add to your MCP settings:')\r\n console.log('─────────────────────────────────────────────────')\r\n console.log(JSON.stringify({\r\n mcpServers: {\r\n estatehelm: {\r\n command: 'npx',\r\n args: ['estatehelm', 'mcp']\r\n }\r\n }\r\n }, null, 2))\r\n console.log('─────────────────────────────────────────────────')\r\n console.log('\\nOr run manually: 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 let 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 let APP_URL = process.env.ESTATEHELM_APP_URL || 'https://app.estatehelm.com'\r\n\r\n/**\r\n * Kratos Public API URL for native authentication flows\r\n */\r\nexport let KRATOS_URL = process.env.ESTATEHELM_KRATOS_URL || 'https://pauth.estatehelm.com'\r\n\r\n/**\r\n * Set server URLs at runtime (for CLI flags)\r\n */\r\nexport function setServerUrls(apiUrl?: string, appUrl?: string, kratosUrl?: string): void {\r\n if (apiUrl) {\r\n API_BASE_URL = apiUrl\r\n console.error(`[Config] Using API: ${apiUrl}`)\r\n }\r\n if (appUrl) {\r\n APP_URL = appUrl\r\n console.error(`[Config] Using App: ${appUrl}`)\r\n }\r\n if (kratosUrl) {\r\n KRATOS_URL = kratosUrl\r\n console.error(`[Config] Using Kratos: ${kratosUrl}`)\r\n }\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 console.error('')\r\n console.error('To use with Claude Code, add to ~/.claude/claude_desktop_config.json:')\r\n console.error('─────────────────────────────────────────────────────────────────────')\r\n console.error(JSON.stringify({\r\n mcpServers: {\r\n estatehelm: {\r\n command: 'npx',\r\n args: ['estatehelm', 'mcp']\r\n }\r\n }\r\n }, null, 2))\r\n console.error('─────────────────────────────────────────────────────────────────────')\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 if metadata table exists first\r\n const tableExists = this.db.prepare(\r\n \"SELECT name FROM sqlite_master WHERE type='table' AND name='metadata'\"\r\n ).get()\r\n\r\n let currentVersion = 0\r\n\r\n if (tableExists) {\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 currentVersion = versionRow ? parseInt(versionRow.value, 10) : 0\r\n }\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/contacts.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","../../encryption/src/fileEncryption.ts","../src/config.ts","../src/keyStore.ts","../src/server.ts","../../cache-sqlite/src/sqliteStore.ts","../../cache/src/schema.ts","../src/cache.ts","../src/resources/households.ts","../src/filter.ts","../../list-data/src/computedFields.ts","../../utils/src/dateHelpers.ts","../../utils/src/search/searchEngine.ts","../../utils/src/search/entityIndexer.ts","../../list-data/src/adapters/contactAdapter.ts","../../list-data/src/adapters/insuranceAdapter.ts","../../list-data/src/adapters/petAdapter.ts","../../list-data/src/adapters/vehicleAdapter.ts","../../list-data/src/data/propertyMaintenanceTemplates.json","../../list-data/src/data/vehicleMaintenanceTemplates.json","../../list-data/src/adapters/maintenanceAdapter.ts","../../list-data/src/adapters/subscriptionAdapter.ts","../../list-data/src/adapters/credentialAdapter.ts","../src/enrichment.ts","../src/resources/entities.ts","../src/tools/search.ts","../src/tools/files.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, setServerUrls, API_BASE_URL, APP_URL } 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 .option('--staging', 'Use staging environment (previewapi/previewapp.estatehelm.com)')\r\n .option('--api-url <url>', 'API server URL (default: https://api.estatehelm.com)')\r\n .option('--app-url <url>', 'App server URL (default: https://app.estatehelm.com)')\r\n .hook('preAction', (thisCommand) => {\r\n const opts = thisCommand.opts()\r\n if (opts.staging) {\r\n setServerUrls(\r\n 'https://previewapi.estatehelm.com',\r\n 'https://previewapp.estatehelm.com',\r\n 'https://stauth.estatehelm.com'\r\n )\r\n } else if (opts.apiUrl || opts.appUrl) {\r\n setServerUrls(opts.apiUrl, opts.appUrl)\r\n }\r\n })\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 native Kratos login flow with OIDC (Google/Apple).\r\n * Uses local callback server to capture session token from Kratos redirect.\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 (dynamic)\r\n */\r\nasync function findAvailablePort(): Promise<number> {\r\n return new Promise((resolve, reject) => {\r\n const server = http.createServer()\r\n // Port 0 = let OS assign an available port\r\n server.listen(0, '127.0.0.1', () => {\r\n const address = server.address()\r\n const port = typeof address === 'object' && address ? address.port : 0\r\n server.close(() => {\r\n if (port > 0) {\r\n resolve(port)\r\n } else {\r\n reject(new Error('Failed to find available port'))\r\n }\r\n })\r\n })\r\n server.on('error', reject)\r\n })\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 * Wait for callback with session token from CLIAuthPage\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\r\n const sessionToken = url.searchParams.get('session_token')\r\n const error = url.searchParams.get('error')\r\n\r\n if (error) {\r\n res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' })\r\n res.end(`\r\n <!DOCTYPE html>\r\n <html>\r\n <head><meta charset=\"utf-8\"><title>Authentication Failed</title>\r\n <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#fef2f2}.card{background:white;padding:2rem;border-radius:1rem;box-shadow:0 10px 25px rgba(0,0,0,0.1);text-align:center}h1{color:#dc2626}</style></head>\r\n <body><div class=\"card\"><h1>Authentication Failed</h1><p>${url.searchParams.get('error_description') || error}</p></div></body>\r\n </html>\r\n `)\r\n server.close()\r\n reject(new Error(url.searchParams.get('error_description') || error))\r\n return\r\n }\r\n\r\n if (sessionToken) {\r\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })\r\n res.end(`\r\n <!DOCTYPE html>\r\n <html>\r\n <head><meta charset=\"utf-8\"><title>Authentication Successful</title>\r\n <style>body{font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:linear-gradient(115deg,#fff1be 28%,#ee87cb 70%,#b060ff 100%)}.card{background:white;padding:2rem;border-radius:1rem;box-shadow:0 10px 25px rgba(0,0,0,0.1);text-align:center}h1{color:#059669}</style></head>\r\n <body><div class=\"card\"><h1>✓ Authentication Successful</h1><p>You can close this window and return to your terminal.</p></div></body>\r\n </html>\r\n `)\r\n server.close()\r\n resolve(sessionToken)\r\n return\r\n }\r\n\r\n // Ignore other requests (favicon, etc.)\r\n res.writeHead(404)\r\n res.end()\r\n })\r\n\r\n server.listen(port, '127.0.0.1')\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'))\r\n }, 5 * 60 * 1000)\r\n })\r\n}\r\n\r\n/**\r\n * Login via web app (same pattern as Android Apple Sign-In)\r\n *\r\n * Flow:\r\n * 1. CLI opens browser to web app's /cli-auth page\r\n * 2. If not logged in, user is redirected to sign-in (Google or Apple)\r\n * 3. After auth, /cli-auth gets session token from backend\r\n * 4. /cli-auth redirects to CLI's localhost callback with token\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 login URL - web app handles OAuth, then redirects back with token\r\n const loginUrl = `${APP_URL}/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:\\n${loginUrl}\\n`)\r\n\r\n // Start callback server\r\n const tokenPromise = waitForCallback(port)\r\n\r\n // Open browser to web app\r\n await open(loginUrl)\r\n\r\n // Wait for session token\r\n console.log('Waiting for authentication...')\r\n const sessionToken = await tokenPromise\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 // Kratos sessions don't use refresh tokens - save placeholder to satisfy keychain\r\n await saveRefreshToken('kratos-session-no-refresh')\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('\\nTo use with Claude Code, run:')\r\n console.log('─────────────────────────────────────────────────')\r\n if (process.platform === 'win32') {\r\n console.log(' claude mcp add --transport stdio estatehelm -- cmd /c npx estatehelm mcp')\r\n } else {\r\n console.log(' claude mcp add --transport stdio estatehelm -- npx estatehelm mcp')\r\n }\r\n console.log('─────────────────────────────────────────────────')\r\n console.log('\\nOr run manually: npx 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 * Contact Relationship Types\r\n *\r\n * Used to link contacts to entities (pets, vehicles, residents, etc.) with specific roles.\r\n * This enables flexible many-to-many relationships between contacts and household assets.\r\n *\r\n * Examples:\r\n * - Pet -> groomer, pet_sitter, trainer, boarding, daycare\r\n * - Vehicle -> mechanic, dealership, car_wash, body_shop\r\n * - Resident -> babysitter, nanny, tutor, teacher, coach\r\n */\r\n\r\n/**\r\n * A relationship between an entity and a contact\r\n * Role can be a predefined role or a custom string\r\n */\r\nexport interface ContactRelationship {\r\n contact_id: string\r\n role: string // Can be predefined or custom\r\n}\r\n\r\n/**\r\n * Predefined roles by entity type\r\n * These are suggestions - users can also type custom roles\r\n */\r\n\r\n// Pet-related roles\r\nexport const PET_ROLES = [\r\n 'veterinarian',\r\n 'groomer',\r\n 'pet_sitter',\r\n 'dog_walker',\r\n 'trainer',\r\n 'boarding',\r\n 'daycare',\r\n 'pet_store',\r\n] as const\r\n\r\n// Vehicle-related roles\r\nexport const VEHICLE_ROLES = [\r\n 'mechanic',\r\n 'dealership',\r\n 'car_wash',\r\n 'body_shop',\r\n 'tire_shop',\r\n 'insurance_agent',\r\n 'detailer',\r\n] as const\r\n\r\n// Resident-related roles (especially for children)\r\nexport const RESIDENT_ROLES = [\r\n 'babysitter',\r\n 'nanny',\r\n 'tutor',\r\n 'teacher',\r\n 'coach',\r\n 'doctor',\r\n 'dentist',\r\n 'pediatrician',\r\n 'therapist',\r\n 'chiropractor',\r\n 'physical_therapist',\r\n 'pharmacist',\r\n 'lawyer',\r\n 'financial_advisor',\r\n 'accountant',\r\n 'emergency_contact',\r\n // Trust/estate planning roles\r\n 'executor',\r\n 'beneficiary',\r\n 'trustee',\r\n 'power_of_attorney',\r\n 'healthcare_proxy',\r\n 'guardian',\r\n] as const\r\n\r\n// Property-related roles\r\nexport const PROPERTY_ROLES = [\r\n 'contractor',\r\n 'electrician',\r\n 'plumber',\r\n 'hvac_technician',\r\n 'handyman',\r\n 'landscaper',\r\n 'pool_service',\r\n 'pest_control',\r\n 'cleaning_service',\r\n 'security_guard',\r\n 'property_manager',\r\n 'hoa_contact',\r\n 'locksmith',\r\n 'real_estate_agent',\r\n] as const\r\n\r\n// Service-related roles\r\nexport const SERVICE_ROLES = [\r\n 'provider',\r\n 'backup_provider',\r\n 'supervisor',\r\n 'emergency_contact',\r\n] as const\r\n\r\n// Legal document-related roles\r\nexport const LEGAL_ROLES = [\r\n 'lawyer',\r\n 'notary',\r\n 'executor',\r\n 'trustee',\r\n 'power_of_attorney',\r\n] as const\r\n\r\n// Financial account-related roles (for contacts, not residents)\r\nexport const FINANCIAL_ROLES = [\r\n 'financial_advisor',\r\n 'accountant',\r\n 'manager',\r\n] as const\r\n\r\n// Home improvement-related roles\r\nexport const HOME_IMPROVEMENT_ROLES = [\r\n 'general_contractor',\r\n 'roofer',\r\n 'electrician',\r\n 'plumber',\r\n 'hvac_technician',\r\n 'painter',\r\n 'flooring_installer',\r\n 'window_installer',\r\n 'inspector',\r\n 'handyman',\r\n] as const\r\n\r\n// Vehicle maintenance-related roles (extends vehicle roles)\r\nexport const VEHICLE_MAINTENANCE_ROLES = [\r\n 'mechanic',\r\n 'dealership',\r\n 'body_shop',\r\n 'tire_shop',\r\n 'detailer',\r\n 'inspector',\r\n] as const\r\n\r\n// Health record-related roles (for people)\r\nexport const HEALTH_RECORD_PERSON_ROLES = [\r\n 'primary_care',\r\n 'specialist',\r\n 'dentist',\r\n 'pharmacy',\r\n 'hospital',\r\n 'lab',\r\n 'therapist',\r\n] as const\r\n\r\n// Health record-related roles (for pets)\r\nexport const HEALTH_RECORD_PET_ROLES = [\r\n 'veterinarian',\r\n 'vet_specialist',\r\n 'groomer',\r\n 'pharmacy',\r\n 'emergency_vet',\r\n] as const\r\n\r\n// Type for predefined roles (useful for type hints)\r\nexport type PetRole = typeof PET_ROLES[number]\r\nexport type VehicleRole = typeof VEHICLE_ROLES[number]\r\nexport type ResidentRole = typeof RESIDENT_ROLES[number]\r\nexport type PropertyRole = typeof PROPERTY_ROLES[number]\r\nexport type ServiceRole = typeof SERVICE_ROLES[number]\r\nexport type LegalRole = typeof LEGAL_ROLES[number]\r\nexport type FinancialRole = typeof FINANCIAL_ROLES[number]\r\nexport type HomeImprovementRole = typeof HOME_IMPROVEMENT_ROLES[number]\r\nexport type VehicleMaintenanceRole = typeof VEHICLE_MAINTENANCE_ROLES[number]\r\nexport type HealthRecordPersonRole = typeof HEALTH_RECORD_PERSON_ROLES[number]\r\nexport type HealthRecordPetRole = typeof HEALTH_RECORD_PET_ROLES[number]\r\nexport type PredefinedRole = PetRole | VehicleRole | ResidentRole | PropertyRole | ServiceRole | LegalRole | FinancialRole | HomeImprovementRole | VehicleMaintenanceRole | HealthRecordPersonRole | HealthRecordPetRole\r\n\r\n/**\r\n * Get role suggestions based on entity type\r\n */\r\nexport function getRolesForEntityType(entityType: 'pet' | 'vehicle' | 'resident' | 'property' | 'service' | 'legal' | 'financial_account' | 'home_improvement' | 'vehicle_maintenance' | 'health_record_person' | 'health_record_pet'): readonly string[] {\r\n switch (entityType) {\r\n case 'pet':\r\n return PET_ROLES\r\n case 'vehicle':\r\n return VEHICLE_ROLES\r\n case 'resident':\r\n return RESIDENT_ROLES\r\n case 'property':\r\n return PROPERTY_ROLES\r\n case 'service':\r\n return SERVICE_ROLES\r\n case 'legal':\r\n return LEGAL_ROLES\r\n case 'financial_account':\r\n return FINANCIAL_ROLES\r\n case 'home_improvement':\r\n return HOME_IMPROVEMENT_ROLES\r\n case 'vehicle_maintenance':\r\n return VEHICLE_MAINTENANCE_ROLES\r\n case 'health_record_person':\r\n return HEALTH_RECORD_PERSON_ROLES\r\n case 'health_record_pet':\r\n return HEALTH_RECORD_PET_ROLES\r\n default:\r\n return []\r\n }\r\n}\r\n\r\n/**\r\n * Helper to get contacts for an entity by role\r\n */\r\nexport function getContactsByRole(\r\n relationships: ContactRelationship[] | undefined,\r\n role: string\r\n): string[] {\r\n if (!relationships) return []\r\n return relationships.filter(r => r.role === role).map(r => r.contact_id)\r\n}\r\n\r\n/**\r\n * Helper to check if entity has a contact assigned for a role\r\n */\r\nexport function hasContactForRole(\r\n relationships: ContactRelationship[] | undefined,\r\n role: string\r\n): boolean {\r\n return getContactsByRole(relationships, role).length > 0\r\n}\r\n\r\n/**\r\n * Helper to add a contact relationship\r\n */\r\nexport function addContactRelationship(\r\n existing: ContactRelationship[] | undefined,\r\n contact_id: string,\r\n role: string\r\n): ContactRelationship[] {\r\n const relationships = existing || []\r\n // Check if already exists\r\n if (relationships.some(r => r.contact_id === contact_id && r.role === role)) {\r\n return relationships\r\n }\r\n return [...relationships, { contact_id, role }]\r\n}\r\n\r\n/**\r\n * Helper to remove a contact relationship\r\n */\r\nexport function removeContactRelationship(\r\n existing: ContactRelationship[] | undefined,\r\n contact_id: string,\r\n role: string\r\n): ContactRelationship[] {\r\n if (!existing) return []\r\n return existing.filter(r => !(r.contact_id === contact_id && r.role === role))\r\n}\r\n\r\n/**\r\n * Minimal contact fields needed for display name formatting\r\n */\r\nexport interface ContactNameFields {\r\n first_name?: string | null\r\n last_name?: string | null\r\n company_name?: string | null\r\n}\r\n\r\n/**\r\n * Get the display name for a contact.\r\n * Shows both person name and company name when both are present.\r\n * Format: \"First Last / Company Name\" or just one if only one is available.\r\n * \r\n * @param contact - Contact object with name fields\r\n * @param fallback - Fallback text if no name is available (default: '')\r\n * @returns Formatted display name\r\n */\r\nexport function getContactDisplayName(contact: ContactNameFields, fallback: string = ''): string {\r\n const personName = [contact.first_name, contact.last_name].filter(Boolean).join(' ')\r\n const companyName = contact.company_name || ''\r\n\r\n if (personName && companyName) {\r\n return `${personName} / ${companyName}`\r\n }\r\n\r\n return personName || companyName || fallback\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 * File Encryption Module\r\n *\r\n * Handles encryption and decryption of binary file data using AES-256-GCM.\r\n * Uses the same key derivation as entities (based on entityId + entityType).\r\n *\r\n * Files attached to an entity use that entity's derived key for encryption.\r\n *\r\n * @module encryption/fileEncryption\r\n */\r\n\r\nimport { base64Encode, base64Decode, generateRandomBytes } from './utils';\r\nimport { deriveEntityKey, importEntityKey } from './entityKeys';\r\nimport type { EntityType } from './types';\r\n\r\n/**\r\n * IV size for AES-GCM (96 bits / 12 bytes)\r\n */\r\nexport const FILE_IV_SIZE = 12;\r\n\r\n/**\r\n * Encrypted file data with metadata\r\n */\r\nexport interface EncryptedFileData {\r\n /** Base64-encoded encrypted file bytes (includes IV prefix) */\r\n encryptedBytes: string;\r\n /** Original MIME type of the file */\r\n mimeType: string;\r\n /** Original file size in bytes (before encryption) */\r\n originalSize: number;\r\n}\r\n\r\n/**\r\n * Decrypted file data\r\n */\r\nexport interface DecryptedFileData {\r\n /** Raw file bytes */\r\n bytes: Uint8Array;\r\n /** MIME type of the file */\r\n mimeType: string;\r\n}\r\n\r\n/**\r\n * Options for file encryption\r\n */\r\nexport interface EncryptFileOptions {\r\n /** Additional authenticated data (optional) */\r\n additionalData?: Uint8Array;\r\n /**\r\n * File ID to use for key derivation (crypto_version 2).\r\n * When provided, the key is derived from fileId instead of entityId.\r\n * This gives each file its own unique encryption key.\r\n */\r\n fileId?: string;\r\n}\r\n\r\n/**\r\n * Encrypts a file using a key derived from the parent entity.\r\n *\r\n * The encrypted format is: [12-byte IV][ciphertext]\r\n *\r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - ID of the parent entity (e.g., insurance policy ID)\r\n * @param entityType - Type of the parent entity (e.g., 'insurance')\r\n * @param fileBytes - Raw file bytes to encrypt\r\n * @param mimeType - MIME type of the file\r\n * @param options - Optional encryption settings\r\n * @returns Encrypted file data\r\n *\r\n * @example\r\n * ```ts\r\n * const imageFile = await fetch(imageUrl).then(r => r.arrayBuffer());\r\n * const encrypted = await encryptFile(\r\n * householdKey,\r\n * insuranceId,\r\n * 'insurance',\r\n * new Uint8Array(imageFile),\r\n * 'image/jpeg'\r\n * );\r\n * // Upload encrypted.encryptedBytes to storage\r\n * ```\r\n */\r\nexport async function encryptFile(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType,\r\n fileBytes: Uint8Array,\r\n mimeType: string,\r\n options: EncryptFileOptions = {}\r\n): Promise<EncryptedFileData> {\r\n // crypto_version 2: use fileId for key derivation (per-file keys)\r\n // crypto_version 1 (legacy): use entityId for key derivation (per-entity keys)\r\n const keyDerivationId = options.fileId || entityId;\r\n\r\n try {\r\n // Derive key from fileId (v2) or entityId (v1)\r\n const fileKeyBytes = await deriveEntityKey(householdKey, keyDerivationId, entityType);\r\n const fileKey = await importEntityKey(fileKeyBytes);\r\n\r\n // Generate random IV\r\n const iv = generateRandomBytes(FILE_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 if (options.additionalData) {\r\n encryptParams.additionalData = options.additionalData as BufferSource;\r\n }\r\n\r\n // Encrypt the file bytes\r\n const ciphertext = await crypto.subtle.encrypt(\r\n encryptParams,\r\n fileKey,\r\n fileBytes as BufferSource\r\n );\r\n\r\n // Pack IV + ciphertext together\r\n const packed = new Uint8Array(FILE_IV_SIZE + ciphertext.byteLength);\r\n packed.set(iv, 0);\r\n packed.set(new Uint8Array(ciphertext), FILE_IV_SIZE);\r\n\r\n return {\r\n encryptedBytes: base64Encode(packed),\r\n mimeType,\r\n originalSize: fileBytes.length\r\n };\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to encrypt file for ${entityType} ${keyDerivationId}: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Result of encrypting a file with raw bytes (for large files)\r\n */\r\nexport interface EncryptedFileDataRaw {\r\n /** Encrypted file bytes (IV + ciphertext) - raw Uint8Array */\r\n encryptedBytes: Uint8Array;\r\n /** Original MIME type of the file */\r\n mimeType: string;\r\n /** Original file size in bytes */\r\n originalSize: number;\r\n}\r\n\r\n/**\r\n * Encrypts a file and returns raw bytes (for large files).\r\n * This avoids base64 encoding which can fail for files > ~100MB.\r\n *\r\n * The encrypted format is: [12-byte IV][ciphertext]\r\n *\r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - ID of the parent entity\r\n * @param entityType - Type of the parent entity\r\n * @param fileBytes - Raw file bytes to encrypt\r\n * @param mimeType - MIME type of the file\r\n * @param options - Optional encryption settings\r\n * @returns Encrypted file data with raw bytes\r\n */\r\nexport async function encryptFileToBytes(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType,\r\n fileBytes: Uint8Array,\r\n mimeType: string,\r\n options: EncryptFileOptions = {}\r\n): Promise<EncryptedFileDataRaw> {\r\n // crypto_version 2: use fileId for key derivation (per-file keys)\r\n // crypto_version 1 (legacy): use entityId for key derivation (per-entity keys)\r\n const keyDerivationId = options.fileId || entityId;\r\n\r\n try {\r\n // Derive key from fileId (v2) or entityId (v1)\r\n const fileKeyBytes = await deriveEntityKey(householdKey, keyDerivationId, entityType);\r\n const fileKey = await importEntityKey(fileKeyBytes);\r\n\r\n // Generate random IV\r\n const iv = generateRandomBytes(FILE_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 if (options.additionalData) {\r\n encryptParams.additionalData = options.additionalData as BufferSource;\r\n }\r\n\r\n // Encrypt the file bytes\r\n const ciphertext = await crypto.subtle.encrypt(\r\n encryptParams,\r\n fileKey,\r\n fileBytes as BufferSource\r\n );\r\n\r\n // Pack IV + ciphertext together\r\n const packed = new Uint8Array(FILE_IV_SIZE + ciphertext.byteLength);\r\n packed.set(iv, 0);\r\n packed.set(new Uint8Array(ciphertext), FILE_IV_SIZE);\r\n\r\n return {\r\n encryptedBytes: packed,\r\n mimeType,\r\n originalSize: fileBytes.length\r\n };\r\n } catch (error) {\r\n throw new Error(\r\n `Failed to encrypt file for ${entityType} ${keyDerivationId}: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Decrypts a file using a key derived from the file ID (v2) or entity ID (v1).\r\n *\r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - ID of the parent entity\r\n * @param entityType - Type of the parent entity\r\n * @param encryptedBytes - Base64-encoded encrypted file (IV + ciphertext)\r\n * @param mimeType - MIME type of the file\r\n * @param additionalData - Additional authenticated data (must match encryption)\r\n * @returns Decrypted file data\r\n *\r\n * @example\r\n * ```ts\r\n * const decrypted = await decryptFile(\r\n * householdKey,\r\n * insuranceId,\r\n * 'insurance',\r\n * encryptedBase64,\r\n * 'image/jpeg'\r\n * );\r\n *\r\n * // Create blob URL for display\r\n * const blob = new Blob([decrypted.bytes], { type: decrypted.mimeType });\r\n * const url = URL.createObjectURL(blob);\r\n * ```\r\n */\r\nexport async function decryptFile(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType,\r\n encryptedBytes: string,\r\n mimeType: string,\r\n additionalData?: Uint8Array\r\n): Promise<DecryptedFileData> {\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 // Unpack IV + ciphertext\r\n const packed = base64Decode(encryptedBytes);\r\n\r\n if (packed.length < FILE_IV_SIZE + 16) { // 16 = minimum auth tag size\r\n throw new Error('Encrypted data too short');\r\n }\r\n\r\n const iv = packed.slice(0, FILE_IV_SIZE);\r\n const ciphertext = packed.slice(FILE_IV_SIZE);\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 if (additionalData) {\r\n decryptParams.additionalData = additionalData as BufferSource;\r\n }\r\n\r\n // Decrypt the file bytes\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 return {\r\n bytes: new Uint8Array(plaintext),\r\n mimeType\r\n };\r\n } catch (error) {\r\n if (error instanceof Error && error.name === 'OperationError') {\r\n throw new Error(\r\n `Failed to decrypt file: Authentication failed. Wrong key or corrupted data.`\r\n );\r\n }\r\n throw new Error(\r\n `Failed to decrypt file for ${entityType} ${entityId}: ${error instanceof Error ? error.message : 'Unknown error'}`\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Decrypts file bytes directly (when you already have the raw encrypted bytes).\r\n * Useful when downloading from a pre-signed URL.\r\n *\r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - ID of the parent entity\r\n * @param entityType - Type of the parent entity\r\n * @param encryptedArrayBuffer - Raw encrypted bytes (IV + ciphertext)\r\n * @param mimeType - MIME type of the file\r\n * @returns Decrypted file data\r\n */\r\nexport async function decryptFileFromArrayBuffer(\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType,\r\n encryptedArrayBuffer: ArrayBuffer,\r\n mimeType: string\r\n): Promise<DecryptedFileData> {\r\n const packed = new Uint8Array(encryptedArrayBuffer);\r\n\r\n if (packed.length < FILE_IV_SIZE + 16) {\r\n throw new Error('Encrypted data too short');\r\n }\r\n\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 const iv = packed.slice(0, FILE_IV_SIZE);\r\n const ciphertext = packed.slice(FILE_IV_SIZE);\r\n\r\n try {\r\n const plaintext = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n entityKey,\r\n ciphertext as BufferSource\r\n );\r\n\r\n return {\r\n bytes: new Uint8Array(plaintext),\r\n mimeType\r\n };\r\n } catch (error) {\r\n if (error instanceof Error && error.name === 'OperationError') {\r\n throw new Error('Failed to decrypt file: Authentication failed');\r\n }\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * Decrypts file bytes using a pre-derived key (no key derivation).\r\n * Used for share packages where the key is already derived and embedded in the bundle.\r\n *\r\n * @param derivedKey - Pre-derived decryption key (32 bytes)\r\n * @param encryptedArrayBuffer - Raw encrypted bytes (IV + ciphertext)\r\n * @param mimeType - MIME type of the file\r\n * @returns Decrypted file data\r\n */\r\nexport async function decryptFileWithDerivedKey(\r\n derivedKey: Uint8Array,\r\n encryptedArrayBuffer: ArrayBuffer,\r\n mimeType: string\r\n): Promise<DecryptedFileData> {\r\n const packed = new Uint8Array(encryptedArrayBuffer);\r\n\r\n if (packed.length < FILE_IV_SIZE + 16) {\r\n throw new Error('Encrypted data too short');\r\n }\r\n\r\n // Import the pre-derived key directly (no derivation needed)\r\n const entityKey = await importEntityKey(derivedKey);\r\n\r\n const iv = packed.slice(0, FILE_IV_SIZE);\r\n const ciphertext = packed.slice(FILE_IV_SIZE);\r\n\r\n try {\r\n const plaintext = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv: iv as BufferSource },\r\n entityKey,\r\n ciphertext as BufferSource\r\n );\r\n\r\n return {\r\n bytes: new Uint8Array(plaintext),\r\n mimeType\r\n };\r\n } catch (error) {\r\n if (error instanceof Error && error.name === 'OperationError') {\r\n throw new Error('Failed to decrypt file: Authentication failed');\r\n }\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * Creates a Blob URL from decrypted file data.\r\n * Remember to call URL.revokeObjectURL() when done!\r\n *\r\n * @param decrypted - Decrypted file data\r\n * @returns Blob URL that can be used in img src, etc.\r\n */\r\nexport function createBlobUrl(decrypted: DecryptedFileData): string {\r\n // Create a new ArrayBuffer copy to ensure Blob compatibility\r\n // (Uint8Array.buffer could be SharedArrayBuffer which Blob doesn't accept)\r\n const buffer = new ArrayBuffer(decrypted.bytes.byteLength);\r\n new Uint8Array(buffer).set(decrypted.bytes);\r\n const blob = new Blob([buffer], { type: decrypted.mimeType });\r\n return URL.createObjectURL(blob);\r\n}\r\n\r\n/**\r\n * Downloads a file from a Blob URL with a given filename.\r\n * Works in browser environments.\r\n *\r\n * @param blobUrl - Blob URL to download\r\n * @param filename - Suggested filename for download\r\n */\r\nexport function downloadBlobUrl(blobUrl: string, filename: string): void {\r\n const link = document.createElement('a');\r\n link.href = blobUrl;\r\n link.download = filename;\r\n document.body.appendChild(link);\r\n link.click();\r\n document.body.removeChild(link);\r\n}\r\n\r\n/**\r\n * Fetches an encrypted file from a URL and decrypts it.\r\n * This is a pure function that works on both web and React Native.\r\n *\r\n * @param downloadUrl - Pre-signed URL to download the encrypted file\r\n * @param householdKey - Raw household key bytes\r\n * @param entityId - ID of the parent entity\r\n * @param entityType - Type of the parent entity\r\n * @param mimeType - MIME type of the file\r\n * @returns Decrypted file data\r\n *\r\n * @example\r\n * ```ts\r\n * // Get file metadata (includes downloadUrl)\r\n * const file = await api.get(`/files/${fileId}`);\r\n *\r\n * // Fetch and decrypt\r\n * const decrypted = await fetchAndDecryptFile(\r\n * file.downloadUrl,\r\n * householdKey,\r\n * entityId,\r\n * 'insurance',\r\n * 'image/jpeg'\r\n * );\r\n *\r\n * // Use the decrypted data\r\n * const blobUrl = createBlobUrl(decrypted);\r\n * ```\r\n */\r\nexport async function fetchAndDecryptFile(\r\n downloadUrl: string,\r\n householdKey: Uint8Array,\r\n entityId: string,\r\n entityType: EntityType,\r\n mimeType: string\r\n): Promise<DecryptedFileData> {\r\n // Download encrypted bytes\r\n const response = await fetch(downloadUrl);\r\n if (!response.ok) {\r\n throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);\r\n }\r\n const encryptedBytes = await response.arrayBuffer();\r\n\r\n // Decrypt and return\r\n return decryptFileFromArrayBuffer(\r\n householdKey,\r\n entityId,\r\n entityType,\r\n encryptedBytes,\r\n mimeType\r\n );\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\nimport * as os from 'os'\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 let 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 let APP_URL = process.env.ESTATEHELM_APP_URL || 'https://app.estatehelm.com'\r\n\r\n/**\r\n * Kratos Public API URL for native authentication flows\r\n */\r\nexport let KRATOS_URL = process.env.ESTATEHELM_KRATOS_URL || 'https://pauth.estatehelm.com'\r\n\r\n/**\r\n * Set server URLs at runtime (for CLI flags)\r\n */\r\nexport function setServerUrls(apiUrl?: string, appUrl?: string, kratosUrl?: string): void {\r\n if (apiUrl) {\r\n API_BASE_URL = apiUrl\r\n console.error(`[Config] Using API: ${apiUrl}`)\r\n }\r\n if (appUrl) {\r\n APP_URL = appUrl\r\n console.error(`[Config] Using App: ${appUrl}`)\r\n }\r\n if (kratosUrl) {\r\n KRATOS_URL = kratosUrl\r\n console.error(`[Config] Using Kratos: ${kratosUrl}`)\r\n }\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 (includes hostname for identification)\r\n */\r\nexport function getDevicePlatform(): string {\r\n const platform = process.platform\r\n const hostname = os.hostname()\r\n let osName: string\r\n switch (platform) {\r\n case 'darwin':\r\n osName = 'macOS'\r\n break\r\n case 'win32':\r\n osName = 'Windows'\r\n break\r\n case 'linux':\r\n osName = 'Linux'\r\n break\r\n default:\r\n osName = platform\r\n }\r\n // Return format: \"mcp-Windows-HOSTNAME\" for easy identification in UI\r\n return `mcp-${osName}-${hostname}`\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 (${os.hostname()}, ${process.platform})`\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 { initCache, syncIfNeeded, getDecryptedEntities } from './cache.js'\r\n\r\n// Modular resource and tool handlers\r\nimport {\r\n listHouseholds,\r\n getHousehold,\r\n getHouseholdSummary,\r\n getEntities,\r\n getExpiringItems,\r\n ENTITY_TYPES,\r\n} from './resources/index.js'\r\nimport { executeSearch, executeFileDownload } from './tools/index.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 // ============================================================================\r\n // Resources\r\n // ============================================================================\r\n\r\n server.setRequestHandler(ListResourcesRequestSchema, async () => {\r\n const households = await listHouseholds()\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 for (const type of ENTITY_TYPES) {\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 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 listHouseholds()\r\n } else if (parsed.type === 'households' && parsed.householdId && !parsed.entityType) {\r\n // Get specific household\r\n content = await getHousehold(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 (with enrichment)\r\n content = await getEntities(parsed.householdId, parsed.entityType, privateKey, 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 // ============================================================================\r\n // Tools\r\n // ============================================================================\r\n\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. Returns results with computed fields (age, days until expiry, etc.)',\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 (insurance, vehicle registration, credentials, subscriptions)',\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: 'get_file',\r\n description: 'Download and decrypt a file attachment. Returns base64-encoded file data that can be saved to disk.',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n fileId: {\r\n type: 'string',\r\n description: 'The file ID to download',\r\n },\r\n householdId: {\r\n type: 'string',\r\n description: 'The household ID the file belongs to',\r\n },\r\n entityId: {\r\n type: 'string',\r\n description: 'The entity ID the file is attached to',\r\n },\r\n entityType: {\r\n type: 'string',\r\n description: 'The entity type (e.g., insurance, document)',\r\n },\r\n },\r\n required: ['fileId', 'householdId', 'entityId', 'entityType'],\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 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 results = await executeSearch(\r\n args as { query: string; householdId?: string; entityType?: string },\r\n privateKey,\r\n privacyMode\r\n )\r\n return {\r\n content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],\r\n }\r\n }\r\n\r\n case 'get_household_summary': {\r\n const { householdId } = args as { householdId: string }\r\n const summary = await getHouseholdSummary(householdId, privateKey, getDecryptedEntities)\r\n if (!summary) {\r\n throw new Error(`Household not found: ${householdId}`)\r\n }\r\n return {\r\n content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],\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 const expiring = await getExpiringItems(days, householdId, privateKey)\r\n return {\r\n content: [{ type: 'text', text: JSON.stringify(expiring, null, 2) }],\r\n }\r\n }\r\n\r\n case 'get_file': {\r\n const result = await executeFileDownload(client!, args as any)\r\n return {\r\n content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],\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 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 default:\r\n throw new Error(`Unknown tool: ${name}`)\r\n }\r\n })\r\n\r\n // ============================================================================\r\n // Prompts\r\n // ============================================================================\r\n\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 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 listHouseholds()\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, credentials (passports, licenses), 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// Helpers\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 * 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 if metadata table exists first\r\n const tableExists = this.db.prepare(\r\n \"SELECT name FROM sqlite_master WHERE type='table' AND name='metadata'\"\r\n ).get()\r\n\r\n let currentVersion = 0\r\n\r\n if (tableExists) {\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 currentVersion = versionRow ? parseInt(versionRow.value, 10) : 0\r\n }\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 decryptFileFromArrayBuffer,\r\n} from '@hearthcoo/encryption/mobile'\r\nimport type { EncryptedEntity, EntityType } 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\r\n/**\r\n * File metadata from API\r\n */\r\ninterface FileMetadata {\r\n downloadUrl: string\r\n fileType?: string\r\n fileName?: string\r\n entityId?: string\r\n entityType?: string\r\n size?: number\r\n cryptoVersion?: number\r\n}\r\n\r\n/**\r\n * Result of downloading and decrypting a file\r\n */\r\nexport interface DecryptedFile {\r\n bytes: Uint8Array\r\n dataBase64: string\r\n mimeType: string\r\n fileName?: string\r\n}\r\n\r\n/**\r\n * Download and decrypt a file attachment\r\n */\r\nexport async function downloadAndDecryptFile(\r\n client: ApiClient,\r\n householdId: string,\r\n fileId: string,\r\n entityId: string,\r\n entityType: string\r\n): Promise<DecryptedFile> {\r\n // Get the appropriate household key\r\n const keyType = getKeyTypeForEntity(entityType)\r\n const householdKeyBytes = householdKeysCache.get(`${householdId}:${keyType}`)\r\n\r\n if (!householdKeyBytes) {\r\n throw new Error(`No key available for ${householdId}:${keyType}`)\r\n }\r\n\r\n // Get file metadata from API\r\n const fileInfo = await client.get<FileMetadata>(\r\n `/households/${householdId}/files/${fileId}`\r\n )\r\n\r\n if (!fileInfo.downloadUrl) {\r\n throw new Error('No download URL returned for file')\r\n }\r\n\r\n // Fetch encrypted bytes\r\n const response = await fetch(fileInfo.downloadUrl)\r\n if (!response.ok) {\r\n throw new Error(`Failed to download file: ${response.status}`)\r\n }\r\n const encryptedBytes = await response.arrayBuffer()\r\n\r\n // Determine crypto version and key derivation ID\r\n const cryptoVersion = fileInfo.cryptoVersion ?? 1\r\n const keyDerivationId = cryptoVersion === 2 ? fileId : (fileInfo.entityId || entityId)\r\n const mimeType = fileInfo.fileType || 'application/octet-stream'\r\n\r\n // Decrypt the file\r\n const decrypted = await decryptFileFromArrayBuffer(\r\n householdKeyBytes,\r\n keyDerivationId,\r\n entityType as EntityType,\r\n encryptedBytes,\r\n mimeType\r\n )\r\n\r\n return {\r\n bytes: decrypted.bytes,\r\n dataBase64: base64Encode(decrypted.bytes),\r\n mimeType: decrypted.mimeType,\r\n fileName: fileInfo.fileName,\r\n }\r\n}\r\n","/**\r\n * Households Resource\r\n *\r\n * MCP resource handlers for household data.\r\n *\r\n * @module estatehelm/resources/households\r\n */\r\n\r\nimport { getHouseholds } from '../cache.js'\r\n\r\n/**\r\n * List all households\r\n */\r\nexport async function listHouseholds(): Promise<Array<{ id: string; name: string }>> {\r\n return getHouseholds()\r\n}\r\n\r\n/**\r\n * Get a specific household by ID\r\n */\r\nexport async function getHousehold(\r\n householdId: string\r\n): Promise<{ id: string; name: string } | null> {\r\n const households = await getHouseholds()\r\n return households.find((h) => h.id === householdId) || null\r\n}\r\n\r\n/**\r\n * Get household summary with entity counts\r\n */\r\nexport async function getHouseholdSummary(\r\n householdId: string,\r\n privateKey: CryptoKey,\r\n getDecryptedEntities: (householdId: string, entityType: string, privateKey: CryptoKey) => Promise<any[]>\r\n): Promise<{\r\n household: { id: string; name: string }\r\n counts: Record<string, number>\r\n totalEntities: number\r\n} | null> {\r\n const households = await getHouseholds()\r\n const household = households.find((h) => h.id === householdId)\r\n\r\n if (!household) {\r\n return null\r\n }\r\n\r\n const entityTypes = [\r\n 'pet', 'property', 'vehicle', 'contact', 'insurance', 'bank_account',\r\n 'investment', 'subscription', 'maintenance_task', 'password', 'access_code',\r\n ]\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 return {\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","/**\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', 'recovery_key', 'security_answers'],\r\n\r\n // Financial accounts\r\n bank_account: ['account_number', 'routing_number'],\r\n investment: ['account_number'],\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: ['document_number'],\r\n}\r\n\r\n/**\r\n * Fields that should show partial value (last 4 characters)\r\n */\r\nconst PARTIAL_REDACTION_FIELDS = ['document_number', 'account_number']\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 * Computed Fields\r\n *\r\n * Derived/computed entity fields used by:\r\n * - List-data adapters (for UI rendering)\r\n * - MCP server (for JSON responses)\r\n * - Any consumer needing enriched entity data\r\n *\r\n * @module @hearthcoo/list-data/computedFields\r\n */\r\n\r\n/**\r\n * Computed fields that can be derived from entity data\r\n */\r\nexport interface ComputedFields {\r\n /** Human-readable display name */\r\n displayName?: string\r\n /** Formatted age (e.g., \"5 years\", \"8 months\") */\r\n age?: string\r\n /** Human-equivalent years (for pets) */\r\n humanYears?: number\r\n /** Days until expiration (negative = expired) */\r\n daysUntilExpiry?: number\r\n /** Expiry status */\r\n expiryStatus?: 'expired' | 'expiring_soon' | 'ok'\r\n /** Formatted type label */\r\n formattedType?: string\r\n}\r\n\r\n/**\r\n * Entity with computed fields attached\r\n */\r\nexport interface EnrichedEntity extends Record<string, any> {\r\n _computed?: ComputedFields\r\n}\r\n\r\n/**\r\n * Get expiry status based on days until expiration\r\n */\r\nexport function getExpiryStatus(days: number): 'expired' | 'expiring_soon' | 'ok' {\r\n if (days < 0) return 'expired'\r\n if (days <= 30) return 'expiring_soon'\r\n return 'ok'\r\n}\r\n\r\n/**\r\n * Attach computed fields to an entity\r\n */\r\nexport function attachComputedFields<T extends Record<string, any>>(\r\n entity: T,\r\n computed: ComputedFields\r\n): T & { _computed: ComputedFields } {\r\n return { ...entity, _computed: computed }\r\n}\r\n","/**\r\n * Date Helper Utilities\r\n *\r\n * Common date manipulation and calculation functions.\r\n */\r\n\r\n/**\r\n * Detailed age result with years and months\r\n */\r\nexport interface DetailedAge {\r\n years: number\r\n months: number\r\n /** Fractional years (e.g., 0.75 for 9 months, 1.5 for 1 year 6 months) */\r\n fractionalYears: number\r\n}\r\n\r\n/**\r\n * Calculate age from a date of birth string\r\n * @param dateOfBirth ISO date string (YYYY-MM-DD) or undefined\r\n * @returns Age in years, or null if no date provided\r\n */\r\nexport function calculateAge(dateOfBirth?: string): number | null {\r\n if (!dateOfBirth) return null\r\n\r\n const birth = new Date(dateOfBirth)\r\n const today = new Date()\r\n let years = today.getFullYear() - birth.getFullYear()\r\n const monthDiff = today.getMonth() - birth.getMonth()\r\n\r\n if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {\r\n years--\r\n }\r\n\r\n return years\r\n}\r\n\r\n/**\r\n * Calculate detailed age from a date of birth string\r\n * @param dateOfBirth ISO date string (YYYY-MM-DD) or undefined\r\n * @returns Detailed age with years, months, and fractional years, or null if no date provided\r\n */\r\nexport function calculateDetailedAge(dateOfBirth?: string): DetailedAge | null {\r\n if (!dateOfBirth) return null\r\n\r\n const birth = new Date(dateOfBirth)\r\n const today = new Date()\r\n \r\n let years = today.getFullYear() - birth.getFullYear()\r\n let months = today.getMonth() - birth.getMonth()\r\n \r\n if (today.getDate() < birth.getDate()) {\r\n months--\r\n }\r\n \r\n if (months < 0) {\r\n years--\r\n months += 12\r\n }\r\n\r\n const fractionalYears = years + (months / 12)\r\n\r\n return { years, months, fractionalYears }\r\n}\r\n\r\n/**\r\n * Calculate cat age in human years\r\n * @param catAge Age in cat years (can be fractional, e.g., 0.75 for 9 months)\r\n * @returns Approximate age in human years\r\n */\r\nexport function calculateCatHumanYears(catAge: number): number {\r\n // Cat age to human years conversion\r\n // First year = 15 human years\r\n // Second year = 9 human years (total 24)\r\n // Each year after = 4 human years\r\n if (catAge <= 0) return 0\r\n if (catAge < 1) return Math.round(catAge * 15) // Proportional for first year\r\n if (catAge < 2) return Math.round(15 + (catAge - 1) * 9) // 15 + proportional second year\r\n return Math.round(24 + (catAge - 2) * 4)\r\n}\r\n\r\n/**\r\n * Calculate dog age in human years\r\n * @param dogAge Age in dog years (can be fractional, e.g., 0.75 for 9 months)\r\n * @param size Dog size category\r\n * @returns Approximate age in human years\r\n */\r\nexport function calculateDogHumanYears(\r\n dogAge: number,\r\n size: 'small' | 'medium' | 'large' = 'medium'\r\n): number {\r\n // Dog age varies by size\r\n // Year 1: All sizes = 15 human years\r\n // Year 2: All sizes = 24 human years\r\n // Year 3+: Small +4, Medium +5, Large +6 per year\r\n if (dogAge <= 0) return 0\r\n if (dogAge < 1) return Math.round(dogAge * 15) // Proportional for first year\r\n if (dogAge < 2) return Math.round(15 + (dogAge - 1) * 9) // 15 + proportional second year\r\n\r\n const yearlyRate = size === 'small' ? 4 : size === 'large' ? 6 : 5\r\n return Math.round(24 + (dogAge - 2) * yearlyRate)\r\n}\r\n\r\n/**\r\n * Format a date string for display using locale\r\n * @param dateString ISO date string\r\n * @param locale Optional locale string (defaults to 'en-US')\r\n * @returns Formatted date string\r\n */\r\nexport function formatDate(dateString?: string, locale: string = 'en-US'): string {\r\n if (!dateString) return ''\r\n // For YYYY-MM-DD format, just reformat directly without Date parsing\r\n const match = dateString.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/)\r\n if (match) {\r\n const [, year, month, day] = match\r\n return `${parseInt(month)}/${parseInt(day)}/${year}`\r\n }\r\n // Fallback for other formats (timestamps, etc)\r\n return new Date(dateString).toLocaleDateString(locale)\r\n}\r\n\r\n/**\r\n * Format a date as YYYY-MM-DD (ISO format without time)\r\n */\r\nexport function formatDateISO(date: Date): string {\r\n const year = date.getFullYear()\r\n const month = String(date.getMonth() + 1).padStart(2, '0')\r\n const day = String(date.getDate()).padStart(2, '0')\r\n return `${year}-${month}-${day}`\r\n}\r\n\r\n/**\r\n * Get years between two dates\r\n * @param startDate Start date string\r\n * @param endDate End date string (defaults to today)\r\n * @returns Number of years\r\n */\r\nexport function getYearsBetween(startDate: string, endDate?: string): number {\r\n const start = new Date(startDate)\r\n const end = endDate ? new Date(endDate) : new Date()\r\n\r\n let years = end.getFullYear() - start.getFullYear()\r\n const monthDiff = end.getMonth() - start.getMonth()\r\n\r\n if (monthDiff < 0 || (monthDiff === 0 && end.getDate() < start.getDate())) {\r\n years--\r\n }\r\n\r\n return years\r\n}\r\n\r\n/**\r\n * Add days to a date and return as ISO string\r\n */\r\nexport function addDays(date: Date, days: number): string {\r\n const result = new Date(date)\r\n result.setDate(result.getDate() + days)\r\n return formatDateISO(result)\r\n}\r\n\r\n/**\r\n * Parse a YYYY-MM-DD date string as local time (not UTC).\r\n * This avoids timezone issues where \"2025-12-10\" becomes Dec 9 in US timezones.\r\n */\r\nexport function parseDateLocal(dateStr: string): Date {\r\n const match = dateStr.match(/^(\\d{4})-(\\d{2})-(\\d{2})/)\r\n if (match) {\r\n const [, year, month, day] = match\r\n return new Date(parseInt(year), parseInt(month) - 1, parseInt(day))\r\n }\r\n // Fallback for other formats\r\n return new Date(dateStr)\r\n}\r\n\r\n/**\r\n * Calculate the number of days until a date\r\n */\r\nexport function daysUntil(dateStr: string): number {\r\n const targetDate = parseDateLocal(dateStr)\r\n const today = new Date()\r\n today.setHours(0, 0, 0, 0)\r\n\r\n const diffTime = targetDate.getTime() - today.getTime()\r\n return Math.ceil(diffTime / (1000 * 60 * 60 * 24))\r\n}\r\n","/**\r\n * Search Engine\r\n *\r\n * Platform-agnostic search logic for universal search functionality.\r\n * Shared between web (React) and mobile (React Native).\r\n */\r\n\r\nimport {\r\n SearchableEntity,\r\n SearchableEntityType,\r\n SearchResult,\r\n GroupedSearchResults,\r\n EntityTypeMetadata,\r\n DEFAULT_ENTITY_TYPE_METADATA,\r\n} from './searchableEntity'\r\n\r\n/**\r\n * Options for search behavior\r\n */\r\nexport interface SearchOptions {\r\n /** Minimum query length to trigger search (default: 1) */\r\n minQueryLength?: number\r\n /** Maximum results per entity type group (default: 5) */\r\n maxPerGroup?: number\r\n /** Maximum total results (default: 25) */\r\n maxTotal?: number\r\n /** Only include these entity types */\r\n includeTypes?: SearchableEntityType[]\r\n /** Exclude these entity types */\r\n excludeTypes?: SearchableEntityType[]\r\n}\r\n\r\nconst DEFAULT_OPTIONS: Required<SearchOptions> = {\r\n minQueryLength: 1,\r\n maxPerGroup: 5,\r\n maxTotal: 25,\r\n includeTypes: [],\r\n excludeTypes: [],\r\n}\r\n\r\n/**\r\n * Calculate match score for a query against text\r\n *\r\n * Scoring:\r\n * - Exact match (case-insensitive): 100\r\n * - Starts with query: 75\r\n * - Word starts with query: 50\r\n * - Contains query: 25\r\n * - No match: 0\r\n */\r\nexport function scoreMatch(query: string, text: string): number {\r\n if (!query || !text) return 0\r\n\r\n const queryLower = query.toLowerCase().trim()\r\n const textLower = text.toLowerCase()\r\n\r\n if (queryLower.length === 0) return 0\r\n\r\n // Exact match\r\n if (textLower === queryLower) {\r\n return 100\r\n }\r\n\r\n // Starts with query\r\n if (textLower.startsWith(queryLower)) {\r\n return 75\r\n }\r\n\r\n // Word starts with query (e.g., \"John Smith\" matches \"sm\")\r\n const words = textLower.split(/\\s+/)\r\n for (const word of words) {\r\n if (word.startsWith(queryLower)) {\r\n return 50\r\n }\r\n }\r\n\r\n // Contains query anywhere\r\n if (textLower.includes(queryLower)) {\r\n return 25\r\n }\r\n\r\n return 0\r\n}\r\n\r\n/**\r\n * Score an entity against a search query\r\n * Returns the best score across all searchable fields\r\n */\r\nfunction scoreEntity(\r\n entity: SearchableEntity,\r\n query: string\r\n): { score: number; matchedFields: ('name' | 'subtitle' | 'keywords')[] } {\r\n const matchedFields: ('name' | 'subtitle' | 'keywords')[] = []\r\n let bestScore = 0\r\n\r\n // Score name (primary field, highest weight)\r\n const nameScore = scoreMatch(query, entity.name)\r\n if (nameScore > 0) {\r\n matchedFields.push('name')\r\n bestScore = Math.max(bestScore, nameScore)\r\n }\r\n\r\n // Score subtitle (secondary field)\r\n if (entity.subtitle) {\r\n const subtitleScore = scoreMatch(query, entity.subtitle)\r\n if (subtitleScore > 0) {\r\n matchedFields.push('subtitle')\r\n // Subtitle matches are slightly lower priority\r\n bestScore = Math.max(bestScore, subtitleScore * 0.9)\r\n }\r\n }\r\n\r\n // Score keywords (hidden searchable text)\r\n if (entity.keywords && entity.keywords.length > 0) {\r\n for (const keyword of entity.keywords) {\r\n const keywordScore = scoreMatch(query, keyword)\r\n if (keywordScore > 0) {\r\n if (!matchedFields.includes('keywords')) {\r\n matchedFields.push('keywords')\r\n }\r\n // Keyword matches are lower priority\r\n bestScore = Math.max(bestScore, keywordScore * 0.8)\r\n }\r\n }\r\n }\r\n\r\n return { score: bestScore, matchedFields }\r\n}\r\n\r\n/**\r\n * Search entities and return scored results\r\n */\r\nexport function searchEntities(\r\n entities: SearchableEntity[],\r\n query: string,\r\n options?: SearchOptions\r\n): SearchResult[] {\r\n const opts = { ...DEFAULT_OPTIONS, ...options }\r\n\r\n // Check minimum query length\r\n const trimmedQuery = query.trim()\r\n if (trimmedQuery.length < opts.minQueryLength) {\r\n return []\r\n }\r\n\r\n // Filter by entity type if specified\r\n let filteredEntities = entities\r\n if (opts.includeTypes && opts.includeTypes.length > 0) {\r\n filteredEntities = filteredEntities.filter(e =>\r\n opts.includeTypes!.includes(e.entityType)\r\n )\r\n }\r\n if (opts.excludeTypes && opts.excludeTypes.length > 0) {\r\n filteredEntities = filteredEntities.filter(e =>\r\n !opts.excludeTypes!.includes(e.entityType)\r\n )\r\n }\r\n\r\n // If query is empty, return all entities sorted alphabetically\r\n if (trimmedQuery.length === 0) {\r\n const results: SearchResult[] = filteredEntities.map(entity => ({\r\n entity,\r\n score: 1, // Default score for unfiltered results\r\n matchedFields: [],\r\n }))\r\n // Sort alphabetically by name (with defensive checks)\r\n results.sort((a, b) => (a.entity?.name || '').localeCompare(b.entity?.name || ''))\r\n return results.slice(0, opts.maxTotal)\r\n }\r\n\r\n // Score all entities\r\n const results: SearchResult[] = []\r\n for (const entity of filteredEntities) {\r\n const { score, matchedFields } = scoreEntity(entity, trimmedQuery)\r\n if (score > 0) {\r\n results.push({ entity, score, matchedFields })\r\n }\r\n }\r\n\r\n // Sort by score (highest first), then by name alphabetically\r\n results.sort((a, b) => {\r\n if (b.score !== a.score) {\r\n return b.score - a.score\r\n }\r\n return (a.entity?.name || '').localeCompare(b.entity?.name || '')\r\n })\r\n\r\n // Limit total results\r\n return results.slice(0, opts.maxTotal)\r\n}\r\n\r\n/**\r\n * Group search results by entity type\r\n */\r\nexport function groupSearchResults(\r\n results: SearchResult[],\r\n getMetadata?: (type: SearchableEntityType) => EntityTypeMetadata\r\n): GroupedSearchResults[] {\r\n const metadataFn = getMetadata || ((type) => DEFAULT_ENTITY_TYPE_METADATA[type])\r\n\r\n // Group by entity type\r\n const groups = new Map<SearchableEntityType, SearchResult[]>()\r\n for (const result of results) {\r\n const type = result.entity.entityType\r\n if (!groups.has(type)) {\r\n groups.set(type, [])\r\n }\r\n groups.get(type)!.push(result)\r\n }\r\n\r\n // Convert to array and sort by metadata order\r\n const groupedResults: GroupedSearchResults[] = []\r\n for (const [entityType, typeResults] of groups) {\r\n const metadata = metadataFn(entityType)\r\n groupedResults.push({\r\n entityType,\r\n label: metadata.pluralLabel,\r\n icon: metadata.icon,\r\n results: typeResults,\r\n })\r\n }\r\n\r\n // Sort groups by display order\r\n groupedResults.sort((a, b) => {\r\n const orderA = metadataFn(a.entityType).order\r\n const orderB = metadataFn(b.entityType).order\r\n return orderA - orderB\r\n })\r\n\r\n return groupedResults\r\n}\r\n\r\n/**\r\n * Limit results per group\r\n */\r\nexport function limitResultsPerGroup(\r\n groups: GroupedSearchResults[],\r\n maxPerGroup: number\r\n): GroupedSearchResults[] {\r\n return groups.map(group => ({\r\n ...group,\r\n results: group.results.slice(0, maxPerGroup),\r\n }))\r\n}\r\n\r\n/**\r\n * Get total result count across all groups\r\n */\r\nexport function getTotalResultCount(groups: GroupedSearchResults[]): number {\r\n return groups.reduce((sum, group) => sum + group.results.length, 0)\r\n}\r\n\r\n/**\r\n * Flatten grouped results back to a single array\r\n */\r\nexport function flattenGroupedResults(groups: GroupedSearchResults[]): SearchResult[] {\r\n return groups.flatMap(group => group.results)\r\n}\r\n","/**\r\n * Entity Indexer\r\n *\r\n * Converts raw entities from the database into searchable entities.\r\n * Platform-agnostic - shared between web (React) and mobile (React Native).\r\n */\r\n\r\nimport { SearchableEntity } from './searchableEntity'\r\n\r\n// ============================================================================\r\n// Entity type definitions (minimal, just what we need for indexing)\r\n// These mirror the full types from @hearthcoo/types but are kept minimal\r\n// to avoid circular dependencies\r\n// ============================================================================\r\n\r\ninterface BaseEntity {\r\n id: string\r\n}\r\n\r\ninterface PropertyEntity extends BaseEntity {\r\n name: string\r\n street_address?: string\r\n city?: string\r\n state?: string\r\n contact_relationships?: ContactRelationship[]\r\n}\r\n\r\ninterface VehicleEntity extends BaseEntity {\r\n name?: string\r\n vehicle_type?: string\r\n make?: string\r\n model?: string\r\n year?: number\r\n license_plate?: string\r\n contact_relationships?: ContactRelationship[]\r\n}\r\n\r\ninterface PetEntity extends BaseEntity {\r\n name: string\r\n species?: string\r\n breed?: string\r\n contact_relationships?: ContactRelationship[]\r\n}\r\n\r\ninterface ContactEntity extends BaseEntity {\r\n type?: string\r\n company_name?: string\r\n first_name?: string\r\n last_name?: string\r\n specialty?: string\r\n}\r\n\r\ninterface SubscriptionEntity extends BaseEntity {\r\n custom_name?: string\r\n provider?: string\r\n category?: string\r\n}\r\n\r\ninterface ServiceEntity extends BaseEntity {\r\n service_name?: string\r\n name?: string\r\n service_type?: string\r\n provider?: string\r\n}\r\n\r\ninterface EntityRelationship {\r\n entity_type: string\r\n entity_id: string\r\n}\r\n\r\ninterface ContactRelationship {\r\n contact_id: string\r\n role: string\r\n}\r\n\r\ninterface InsuranceEntity extends BaseEntity {\r\n provider?: string\r\n policy_number?: string\r\n type?: string\r\n relationships?: EntityRelationship[]\r\n // Deprecated fields still supported\r\n property_id?: string\r\n vehicle_id?: string\r\n pet_id?: string\r\n}\r\n\r\ninterface ValuableEntity extends BaseEntity {\r\n name: string\r\n category?: string\r\n other_category?: string\r\n}\r\n\r\ninterface FinancialAccountEntity extends BaseEntity {\r\n nickname?: string\r\n institution?: string\r\n account_type?: string\r\n owner_ids?: string[]\r\n}\r\n\r\ninterface CredentialEntity extends BaseEntity {\r\n name: string\r\n person_id?: string\r\n credential_type?: string\r\n credential_subtype?: string\r\n issuing_authority?: string\r\n}\r\n\r\ninterface PetVetVisitEntity extends BaseEntity {\r\n name?: string\r\n pet_id?: string\r\n date?: string\r\n procedures?: Array<{ name?: string }>\r\n}\r\n\r\ninterface VehicleServiceEntity extends BaseEntity {\r\n name?: string\r\n vehicle_id?: string\r\n date?: string\r\n services?: Array<{ name?: string }>\r\n}\r\n\r\ninterface MaintenanceTaskEntity extends BaseEntity {\r\n name?: string\r\n title?: string\r\n category?: string\r\n property_id?: string\r\n vehicle_id?: string\r\n}\r\n\r\ninterface LegalDocumentEntity extends BaseEntity {\r\n name: string\r\n type?: string\r\n resident_ids?: string[]\r\n}\r\n\r\ninterface AccessCodeEntity extends BaseEntity {\r\n name: string\r\n code_type?: string\r\n property_id?: string\r\n}\r\n\r\ninterface DeviceEntity extends BaseEntity {\r\n name: string\r\n device_type?: string\r\n brand?: string\r\n model?: string\r\n}\r\n\r\ninterface PersonEntity extends BaseEntity {\r\n name: string\r\n person_type?: string\r\n relationship_type?: string\r\n}\r\n\r\ninterface HealthRecordEntity extends BaseEntity {\r\n name: string\r\n record_type?: string\r\n person_id?: string\r\n pet_id?: string\r\n provider?: string\r\n}\r\n\r\ninterface EducationRecordEntity extends BaseEntity {\r\n name: string\r\n record_type?: string\r\n person_id?: string\r\n institution?: string\r\n level?: string\r\n field_of_study?: string\r\n}\r\n\r\ninterface MilitaryRecordEntity extends BaseEntity {\r\n name: string\r\n record_type?: string\r\n person_id?: string\r\n branch?: string\r\n rank?: string\r\n}\r\n\r\ninterface HomeImprovementEntity extends BaseEntity {\r\n name: string\r\n improvement_type?: string\r\n property_id?: string\r\n date_completed?: string\r\n}\r\n\r\ninterface TaxYearEntity extends BaseEntity {\r\n year: number\r\n country?: string\r\n filer_ids?: string[]\r\n}\r\n\r\ninterface MembershipRecordEntity extends BaseEntity {\r\n name: string\r\n person_id?: string\r\n membership_type?: string\r\n membership_number?: string\r\n level?: string\r\n alliance?: string\r\n}\r\n\r\n// ============================================================================\r\n// Helper functions\r\n// ============================================================================\r\n\r\n/**\r\n * Format a label for display (e.g., 'lawn_care' -> 'Lawn Care')\r\n */\r\nexport function formatLabel(value?: string): string {\r\n if (!value) return ''\r\n return value\r\n .replace(/_/g, ' ')\r\n .replace(/\\b\\w/g, (c) => c.toUpperCase())\r\n}\r\n\r\n/**\r\n * Get vehicle display name\r\n */\r\nexport function getVehicleName(vehicle: { name?: string; year?: number; make?: string; model?: string }): string {\r\n if (vehicle.name) return vehicle.name\r\n const parts = [vehicle.year, vehicle.make, vehicle.model].filter(Boolean)\r\n return parts.length > 0 ? parts.join(' ') : 'Vehicle'\r\n}\r\n\r\n/**\r\n * Get contact display name\r\n */\r\nexport function getContactName(contact: { company_name?: string; first_name?: string; last_name?: string }): string {\r\n // Check first/last name first, then company\r\n const fullName = [contact.first_name, contact.last_name].filter(Boolean).join(' ')\r\n if (fullName) return fullName\r\n if (contact.company_name) return contact.company_name\r\n return 'Contact'\r\n}\r\n\r\n/**\r\n * Get subscription display name\r\n */\r\nexport function getSubscriptionName(sub: { custom_name?: string; provider?: string }): string {\r\n return sub.custom_name || formatLabel(sub.provider) || 'Subscription'\r\n}\r\n\r\n/**\r\n * Get service display name\r\n */\r\nexport function getServiceName(service: { service_name?: string; name?: string }): string {\r\n return service.service_name || service.name || 'Service'\r\n}\r\n\r\n/**\r\n * Get property display name\r\n */\r\nexport function getPropertyName(property: { name?: string; street_address?: string; city?: string }): string {\r\n if (property.name) return property.name\r\n if (property.street_address) {\r\n return property.city ? `${property.street_address}, ${property.city}` : property.street_address\r\n }\r\n return 'Property'\r\n}\r\n\r\n/**\r\n * Get pet display name\r\n */\r\nexport function getPetName(pet: { name?: string }): string {\r\n return pet.name || 'Pet'\r\n}\r\n\r\n/**\r\n * Get access code display name\r\n */\r\nexport function getAccessCodeName(code: { name?: string; description?: string }): string {\r\n return code.name || code.description || 'Access Code'\r\n}\r\n\r\n/**\r\n * Get financial account display name\r\n */\r\nexport function getFinancialAccountName(account: { name?: string; account_name?: string; nickname?: string; institution?: string }): string {\r\n return account.name || account.account_name || account.nickname || (account.institution ? `${account.institution} Account` : 'Financial Account')\r\n}\r\n\r\n/**\r\n * Get insurance display name\r\n */\r\nexport function getInsuranceName(policy: { name?: string; policy_name?: string; provider?: string; type?: string }): string {\r\n return policy.name || policy.policy_name || policy.provider || formatLabel(policy.type) || 'Insurance'\r\n}\r\n\r\n/**\r\n * Get valuable display name\r\n */\r\nexport function getValuableName(valuable: { name?: string; item_name?: string; description?: string }): string {\r\n return valuable.name || valuable.item_name || valuable.description || 'Valuable'\r\n}\r\n\r\n/**\r\n * Get maintenance task display name\r\n */\r\nexport function getMaintenanceTaskName(task: { name?: string; title?: string; task_name?: string }): string {\r\n return task.name || task.title || task.task_name || 'Maintenance Task'\r\n}\r\n\r\n/**\r\n * Get device display name\r\n */\r\nexport function getDeviceName(device: { name?: string; device_name?: string }): string {\r\n return device.name || device.device_name || 'Device'\r\n}\r\n\r\n/**\r\n * Get vet visit display name\r\n */\r\nexport function getPetVetVisitName(visit: { name?: string; clinic_name?: string; reason?: string }): string {\r\n return visit.name || visit.clinic_name || visit.reason || 'Vet Visit'\r\n}\r\n\r\n/**\r\n * Get pet health record display name\r\n */\r\nexport function getPetHealthName(record: { name?: string; record_type?: string }): string {\r\n return record.name || (record.record_type ? formatLabel(record.record_type) : 'Health Record')\r\n}\r\n\r\n/**\r\n * Get vehicle maintenance display name\r\n */\r\nexport function getVehicleMaintenanceName(record: { name?: string; service_type?: string; description?: string }): string {\r\n return record.name || record.service_type || record.description || 'Maintenance Record'\r\n}\r\n\r\n/**\r\n * Get vehicle service visit display name\r\n */\r\nexport function getVehicleServiceVisitName(visit: { name?: string; shop_name?: string; description?: string }): string {\r\n return visit.name || visit.shop_name || visit.description || 'Service Visit'\r\n}\r\n\r\n/**\r\n * Get entity display name by type - universal dispatcher\r\n */\r\nexport function getEntityDisplayName(entityType: string, data: any): string {\r\n switch (entityType) {\r\n case 'property': return getPropertyName(data)\r\n case 'vehicle': return getVehicleName(data)\r\n case 'pet': return getPetName(data)\r\n case 'contact': return getContactName(data)\r\n case 'subscription': return getSubscriptionName(data)\r\n case 'service': return getServiceName(data)\r\n case 'insurance': return getInsuranceName(data)\r\n case 'valuable': return getValuableName(data)\r\n case 'financial_account': return getFinancialAccountName(data)\r\n case 'access_code': return getAccessCodeName(data)\r\n case 'maintenance_task': return getMaintenanceTaskName(data)\r\n case 'device': return getDeviceName(data)\r\n case 'pet_vet_visit': return getPetVetVisitName(data)\r\n case 'pet_health': return getPetHealthName(data)\r\n case 'vehicle_maintenance': return getVehicleMaintenanceName(data)\r\n case 'vehicle_service_visit': return getVehicleServiceVisitName(data)\r\n case 'home_improvement': return data.name || data.title || data.project_name || 'Home Improvement'\r\n case 'credential': return data.name || data.title || 'Credential'\r\n case 'password': return data.name || data.site_name || data.title || 'Password'\r\n case 'legal': return data.name || data.title || 'Legal Document'\r\n case 'military_record': return data.name || data.title || 'Military Record'\r\n case 'education_record': return data.name || data.institution || data.title || 'Education Record'\r\n case 'travel': return data.name || data.document_type || 'Travel Document'\r\n case 'identity': return data.name || data.document_type || 'Identity Document'\r\n case 'document': return data.name || data.title || data.file_name || 'Document'\r\n default: return data.name || data.title || entityType\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Individual entity indexers\r\n// ============================================================================\r\n\r\nexport function indexProperty(property: PropertyEntity): SearchableEntity {\r\n const subtitle = property.city && property.state\r\n ? `${property.city}, ${property.state}`\r\n : property.street_address || 'Property'\r\n\r\n return {\r\n id: property.id,\r\n entityType: 'property',\r\n name: property.name,\r\n subtitle,\r\n // Synonyms: house, home for natural language searches\r\n keywords: [property.street_address, property.city, property.state, 'house', 'home'].filter(Boolean) as string[],\r\n icon: 'Home',\r\n route: `/property?id=${property.id}`,\r\n routeParams: { id: property.id },\r\n }\r\n}\r\n\r\nexport function indexVehicle(vehicle: VehicleEntity): SearchableEntity {\r\n const subtitle = formatLabel(vehicle.vehicle_type) || 'Vehicle'\r\n // Synonyms based on vehicle type\r\n const synonyms: string[] = []\r\n if (vehicle.vehicle_type === 'car' || vehicle.vehicle_type === 'sedan' || vehicle.vehicle_type === 'suv' || vehicle.vehicle_type === 'truck') {\r\n synonyms.push('car', 'auto', 'automobile')\r\n }\r\n if (vehicle.vehicle_type === 'motorcycle') {\r\n synonyms.push('bike', 'motorbike')\r\n }\r\n if (vehicle.vehicle_type === 'boat') {\r\n synonyms.push('watercraft')\r\n }\r\n const keywords = [vehicle.make, vehicle.model, vehicle.license_plate, ...synonyms].filter(Boolean) as string[]\r\n\r\n return {\r\n id: vehicle.id,\r\n entityType: 'vehicle',\r\n name: getVehicleName(vehicle),\r\n subtitle,\r\n keywords,\r\n icon: 'Car',\r\n route: `/vehicle?id=${vehicle.id}`,\r\n routeParams: { id: vehicle.id },\r\n }\r\n}\r\n\r\nexport function indexPet(pet: PetEntity): SearchableEntity {\r\n const speciesLabel = formatLabel(pet.species)\r\n const subtitle = pet.breed\r\n ? `${speciesLabel} - ${pet.breed}`\r\n : speciesLabel || 'Pet'\r\n\r\n // Synonyms based on species\r\n const synonyms: string[] = []\r\n if (pet.species === 'dog' || pet.species === 'canine') {\r\n synonyms.push('dog', 'puppy', 'pup')\r\n } else if (pet.species === 'cat' || pet.species === 'feline') {\r\n synonyms.push('cat', 'kitty', 'kitten')\r\n } else if (pet.species === 'bird') {\r\n synonyms.push('bird', 'parrot')\r\n } else if (pet.species === 'fish') {\r\n synonyms.push('fish', 'aquarium')\r\n }\r\n\r\n return {\r\n id: pet.id,\r\n entityType: 'pet',\r\n name: pet.name,\r\n subtitle,\r\n keywords: [pet.breed, pet.species, ...synonyms].filter(Boolean) as string[],\r\n icon: 'PawPrint',\r\n route: `/pet?id=${pet.id}`,\r\n routeParams: { id: pet.id },\r\n }\r\n}\r\n\r\n// Linked entity info for contacts (e.g., pets/vehicles this contact serves)\r\ninterface ContactLinkedEntity {\r\n name: string\r\n role: string\r\n}\r\n\r\nexport function indexContact(\r\n contact: ContactEntity,\r\n linkedEntities?: ContactLinkedEntity[]\r\n): SearchableEntity {\r\n const subtitle = contact.specialty\r\n ? `${formatLabel(contact.type)} - ${contact.specialty}`\r\n : formatLabel(contact.type) || 'Contact'\r\n\r\n // Include entity names for searches like \"Pumpkin's vet\" or \"Tesla mechanic\"\r\n const linkedNames = linkedEntities?.map(e => e.name) || []\r\n // Also include roles for searches like \"find my vet\", \"find the plumber\"\r\n const linkedRoles = linkedEntities?.map(e => formatLabel(e.role)) || []\r\n\r\n return {\r\n id: contact.id,\r\n entityType: 'contact',\r\n name: getContactName(contact),\r\n subtitle,\r\n keywords: [\r\n contact.company_name,\r\n contact.first_name,\r\n contact.last_name,\r\n contact.specialty,\r\n ...linkedNames,\r\n ...linkedRoles,\r\n ].filter(Boolean) as string[],\r\n icon: 'Phone',\r\n route: `/contact?id=${contact.id}`,\r\n routeParams: { id: contact.id },\r\n }\r\n}\r\n\r\nexport function indexSubscription(sub: SubscriptionEntity): SearchableEntity {\r\n const subtitle = formatLabel(sub.category) || 'Subscription'\r\n const displayName = getSubscriptionName(sub)\r\n\r\n // Include provider, custom_name, and formatted versions in keywords for searchability\r\n // When custom_name is the display name, provider should still be searchable and vice versa\r\n const keywords: string[] = []\r\n if (sub.provider) {\r\n keywords.push(sub.provider)\r\n const formatted = formatLabel(sub.provider)\r\n if (formatted !== sub.provider) {\r\n keywords.push(formatted)\r\n }\r\n }\r\n if (sub.custom_name) {\r\n keywords.push(sub.custom_name)\r\n }\r\n if (sub.category) {\r\n keywords.push(sub.category)\r\n }\r\n\r\n // If provider differs from display name, include it in the searchable name\r\n // This ensures provider is searchable at full priority (not just 0.8x keyword priority)\r\n const formattedProvider = formatLabel(sub.provider)\r\n const searchableName = (sub.provider && formattedProvider !== displayName)\r\n ? `${displayName} (${formattedProvider})`\r\n : displayName\r\n\r\n return {\r\n id: sub.id,\r\n entityType: 'subscription',\r\n name: searchableName,\r\n subtitle,\r\n keywords,\r\n icon: 'CreditCard',\r\n route: `/subscription?id=${sub.id}`,\r\n routeParams: { id: sub.id },\r\n }\r\n}\r\n\r\nexport function indexService(service: ServiceEntity): SearchableEntity {\r\n const subtitle = formatLabel(service.service_type) || 'Service'\r\n\r\n return {\r\n id: service.id,\r\n entityType: 'service',\r\n name: getServiceName(service),\r\n subtitle,\r\n keywords: [service.provider, service.service_type].filter(Boolean) as string[],\r\n icon: 'Wrench',\r\n route: `/service?id=${service.id}`,\r\n routeParams: { id: service.id },\r\n }\r\n}\r\n\r\nexport function indexInsurance(\r\n policy: InsuranceEntity,\r\n coveredAssetNames?: string[]\r\n): SearchableEntity {\r\n const subtitle = formatLabel(policy.type) || 'Insurance'\r\n\r\n // Include covered asset names in keywords for searches like \"Tesla insurance\" or \"home insurance\"\r\n const keywords = [policy.policy_number, policy.type, ...(coveredAssetNames || [])].filter(Boolean) as string[]\r\n\r\n return {\r\n id: policy.id,\r\n entityType: 'insurance',\r\n name: policy.provider || 'Insurance Policy',\r\n subtitle,\r\n keywords,\r\n icon: 'Shield',\r\n route: `/insurance?id=${policy.id}`,\r\n routeParams: { id: policy.id },\r\n }\r\n}\r\n\r\nexport function indexValuable(valuable: ValuableEntity): SearchableEntity {\r\n const categoryLabel = valuable.category === 'other' && valuable.other_category\r\n ? valuable.other_category\r\n : formatLabel(valuable.category)\r\n const subtitle = categoryLabel || 'Valuable'\r\n\r\n return {\r\n id: valuable.id,\r\n entityType: 'valuable',\r\n name: valuable.name,\r\n subtitle,\r\n keywords: [valuable.category, valuable.other_category].filter(Boolean) as string[],\r\n icon: 'Gem',\r\n route: `/valuables?id=${valuable.id}`,\r\n routeParams: { id: valuable.id },\r\n }\r\n}\r\n\r\nexport function indexFinancialAccount(\r\n account: FinancialAccountEntity,\r\n ownerNames?: string[]\r\n): SearchableEntity {\r\n const typeLabel = formatLabel(account.account_type) || 'Financial Account'\r\n // Show owner names in subtitle if available\r\n let subtitle = typeLabel\r\n if (ownerNames && ownerNames.length > 0) {\r\n subtitle = `${typeLabel} • ${ownerNames.join(', ')}`\r\n }\r\n\r\n return {\r\n id: account.id,\r\n entityType: 'financial_account',\r\n name: account.nickname || account.institution || 'Financial Account',\r\n subtitle,\r\n keywords: [account.institution, account.account_type, ...(ownerNames || [])].filter(Boolean) as string[],\r\n icon: 'Landmark',\r\n route: `/financial?id=${account.id}`,\r\n routeParams: { id: account.id },\r\n }\r\n}\r\n\r\nexport function indexCredential(\r\n credential: CredentialEntity,\r\n personName?: string\r\n): SearchableEntity {\r\n const subtypeLabel = formatLabel(credential.credential_subtype)\r\n const typeLabel = subtypeLabel || formatLabel(credential.credential_type) || 'Credential'\r\n // Show person name in subtitle if available\r\n const subtitle = personName ? `${typeLabel} • ${personName}` : typeLabel\r\n\r\n // Synonyms for common credential types\r\n const synonyms: string[] = ['id', 'identification']\r\n const subtype = credential.credential_subtype?.toLowerCase() || ''\r\n const type = credential.credential_type?.toLowerCase() || ''\r\n\r\n if (subtype.includes('passport')) {\r\n synonyms.push('passport', 'travel document')\r\n }\r\n if (subtype.includes('driver') || subtype.includes('license')) {\r\n synonyms.push('license', 'drivers license', 'DL', 'driving license')\r\n }\r\n if (subtype.includes('ssn') || subtype.includes('social_security')) {\r\n synonyms.push('social security', 'SSN', 'social security number')\r\n }\r\n if (type.includes('professional') || type.includes('license')) {\r\n synonyms.push('license', 'certification', 'cert')\r\n }\r\n if (subtype.includes('birth') || subtype.includes('certificate')) {\r\n synonyms.push('birth certificate', 'certificate')\r\n }\r\n\r\n return {\r\n id: credential.id,\r\n entityType: 'credential',\r\n name: credential.name,\r\n subtitle,\r\n keywords: [credential.credential_type, credential.credential_subtype, credential.issuing_authority, personName, ...synonyms].filter(Boolean) as string[],\r\n icon: 'CreditCard',\r\n route: `/credentials?id=${credential.id}`,\r\n routeParams: { id: credential.id },\r\n }\r\n}\r\n\r\nexport function indexPetVetVisit(\r\n visit: PetVetVisitEntity,\r\n petName?: string\r\n): SearchableEntity {\r\n const procedureNames = visit.procedures?.map(p => p.name).filter(Boolean).join(', ')\r\n // Show pet name in subtitle, with procedures if available\r\n let subtitle = petName || 'Vet Visit'\r\n if (petName && procedureNames) {\r\n subtitle = `${petName} • ${procedureNames}`\r\n } else if (procedureNames) {\r\n subtitle = procedureNames\r\n }\r\n\r\n return {\r\n id: visit.id,\r\n entityType: 'pet_vet_visit',\r\n name: visit.name || `${petName || 'Pet'} - Vet Visit`,\r\n subtitle,\r\n keywords: [petName, ...(visit.procedures?.map(p => p.name).filter(Boolean) || [])].filter(Boolean) as string[],\r\n icon: 'Stethoscope',\r\n route: `/pet?tab=health&id=${visit.id}`,\r\n routeParams: { tab: 'health', id: visit.id },\r\n }\r\n}\r\n\r\nexport function indexVehicleService(\r\n service: VehicleServiceEntity,\r\n vehicleName?: string\r\n): SearchableEntity {\r\n const serviceNames = service.services?.map(s => s.name).filter(Boolean).join(', ')\r\n // Show vehicle name in subtitle, with services if available\r\n let subtitle = vehicleName || 'Service Record'\r\n if (vehicleName && serviceNames) {\r\n subtitle = `${vehicleName} • ${serviceNames}`\r\n } else if (serviceNames) {\r\n subtitle = serviceNames\r\n }\r\n\r\n return {\r\n id: service.id,\r\n entityType: 'vehicle_service',\r\n name: service.name || `${vehicleName || 'Vehicle'} - Service`,\r\n subtitle,\r\n keywords: [vehicleName, ...(service.services?.map(s => s.name).filter(Boolean) || [])].filter(Boolean) as string[],\r\n icon: 'FileText',\r\n route: `/vehicle-maintenance?id=${service.id}`,\r\n routeParams: { id: service.id },\r\n }\r\n}\r\n\r\nexport function indexMaintenanceTask(\r\n task: MaintenanceTaskEntity,\r\n assetName?: string\r\n): SearchableEntity {\r\n const subtitle = assetName\r\n ? `${formatLabel(task.category)} - ${assetName}`\r\n : formatLabel(task.category) || 'Maintenance'\r\n\r\n return {\r\n id: task.id,\r\n entityType: 'maintenance_task',\r\n name: task.name || task.title || 'Maintenance Task',\r\n subtitle,\r\n keywords: [task.category].filter(Boolean) as string[],\r\n icon: 'ClipboardList',\r\n route: `/maintenance?id=${task.id}`,\r\n routeParams: { id: task.id },\r\n }\r\n}\r\n\r\nexport function indexLegalDocument(\r\n doc: LegalDocumentEntity,\r\n residentNames?: string[]\r\n): SearchableEntity {\r\n const typeLabel = formatLabel(doc.type) || 'Legal Document'\r\n // Show resident names in subtitle if available\r\n let subtitle = typeLabel\r\n if (residentNames && residentNames.length > 0) {\r\n subtitle = `${typeLabel} • ${residentNames.join(', ')}`\r\n }\r\n\r\n // Synonyms for document searches\r\n const synonyms = ['doc', 'document', 'legal', 'paperwork']\r\n\r\n return {\r\n id: doc.id,\r\n entityType: 'legal_document',\r\n name: doc.name,\r\n subtitle,\r\n keywords: [doc.type, ...(residentNames || []), ...synonyms].filter(Boolean) as string[],\r\n icon: 'FileText',\r\n route: `/legal?id=${doc.id}`,\r\n routeParams: { id: doc.id },\r\n }\r\n}\r\n\r\nexport function indexAccessCode(\r\n code: AccessCodeEntity,\r\n propertyName?: string\r\n): SearchableEntity {\r\n const subtitle = propertyName\r\n ? `${formatLabel(code.code_type)} - ${propertyName}`\r\n : formatLabel(code.code_type) || 'Access Code'\r\n\r\n return {\r\n id: code.id,\r\n entityType: 'access_code',\r\n name: code.name,\r\n subtitle,\r\n keywords: [code.code_type].filter(Boolean) as string[],\r\n icon: 'Key',\r\n route: `/property?tab=access&id=${code.id}`,\r\n routeParams: { tab: 'access', id: code.id },\r\n }\r\n}\r\n\r\nexport function indexDevice(device: DeviceEntity): SearchableEntity {\r\n const subtitle = device.brand\r\n ? `${formatLabel(device.device_type)} - ${device.brand}`\r\n : formatLabel(device.device_type) || 'Device'\r\n\r\n return {\r\n id: device.id,\r\n entityType: 'device',\r\n name: device.name,\r\n subtitle,\r\n keywords: [device.device_type, device.brand, device.model].filter(Boolean) as string[],\r\n icon: 'Wifi',\r\n route: `/device?id=${device.id}`,\r\n routeParams: { id: device.id },\r\n }\r\n}\r\n\r\nexport function indexPerson(person: PersonEntity): SearchableEntity {\r\n const subtitle = formatLabel(person.relationship_type) || formatLabel(person.person_type) || 'Person'\r\n\r\n return {\r\n id: person.id,\r\n entityType: 'person',\r\n name: person.name,\r\n subtitle,\r\n keywords: [person.person_type, person.relationship_type].filter(Boolean) as string[],\r\n icon: 'User',\r\n route: `/people?id=${person.id}`,\r\n routeParams: { id: person.id },\r\n }\r\n}\r\n\r\nexport function indexHealthRecord(\r\n record: HealthRecordEntity,\r\n ownerName?: string\r\n): SearchableEntity {\r\n const subtitle = ownerName\r\n ? `${formatLabel(record.record_type)} - ${ownerName}`\r\n : formatLabel(record.record_type) || 'Health Record'\r\n\r\n return {\r\n id: record.id,\r\n entityType: 'health_record',\r\n name: record.name,\r\n subtitle,\r\n keywords: [record.record_type, record.provider].filter(Boolean) as string[],\r\n icon: 'Heart',\r\n route: `/records?type=health_record&id=${record.id}`,\r\n routeParams: { id: record.id },\r\n }\r\n}\r\n\r\nexport function indexEducationRecord(\r\n record: EducationRecordEntity,\r\n personName?: string\r\n): SearchableEntity {\r\n const subtitle = personName\r\n ? `${formatLabel(record.record_type)} - ${personName}`\r\n : record.institution || formatLabel(record.record_type) || 'Education Record'\r\n\r\n return {\r\n id: record.id,\r\n entityType: 'education_record',\r\n name: record.name,\r\n subtitle,\r\n keywords: [record.record_type, record.institution, record.level, record.field_of_study].filter(Boolean) as string[],\r\n icon: 'GraduationCap',\r\n route: `/records?type=education_record&id=${record.id}`,\r\n routeParams: { id: record.id },\r\n }\r\n}\r\n\r\nexport function indexMilitaryRecord(\r\n record: MilitaryRecordEntity,\r\n personName?: string\r\n): SearchableEntity {\r\n const branchLabel = formatLabel(record.branch)\r\n const subtitle = personName\r\n ? `${branchLabel || formatLabel(record.record_type)} - ${personName}`\r\n : branchLabel || formatLabel(record.record_type) || 'Military Record'\r\n\r\n return {\r\n id: record.id,\r\n entityType: 'military_record',\r\n name: record.name,\r\n subtitle,\r\n keywords: [record.record_type, record.branch, record.rank].filter(Boolean) as string[],\r\n icon: 'Medal',\r\n route: `/records?type=military_record&id=${record.id}`,\r\n routeParams: { id: record.id },\r\n }\r\n}\r\n\r\nexport function indexHomeImprovement(\r\n improvement: HomeImprovementEntity,\r\n propertyName?: string\r\n): SearchableEntity {\r\n const subtitle = propertyName\r\n ? `${formatLabel(improvement.improvement_type)} - ${propertyName}`\r\n : formatLabel(improvement.improvement_type) || 'Home Improvement'\r\n\r\n return {\r\n id: improvement.id,\r\n entityType: 'home_improvement',\r\n name: improvement.name,\r\n subtitle,\r\n keywords: [improvement.improvement_type].filter(Boolean) as string[],\r\n icon: 'Hammer',\r\n route: `/home-improvement/${improvement.id}`,\r\n routeParams: { id: improvement.id },\r\n }\r\n}\r\n\r\nexport function indexTaxYear(\r\n taxYear: TaxYearEntity,\r\n filerNames?: string[]\r\n): SearchableEntity {\r\n // Build subtitle with filer names if available\r\n let subtitle = taxYear.country || 'Tax Year'\r\n if (filerNames && filerNames.length > 0) {\r\n subtitle = filerNames.join(', ')\r\n }\r\n\r\n return {\r\n id: taxYear.id,\r\n entityType: 'tax_year',\r\n name: `${taxYear.year} Tax Year`,\r\n subtitle,\r\n keywords: [taxYear.country, String(taxYear.year), ...(filerNames || [])].filter(Boolean) as string[],\r\n icon: 'Receipt',\r\n route: `/taxes?id=${taxYear.id}`,\r\n routeParams: { id: taxYear.id },\r\n }\r\n}\r\n\r\nexport function indexMembershipRecord(\r\n record: MembershipRecordEntity,\r\n personName?: string\r\n): SearchableEntity {\r\n const typeLabel = formatLabel(record.membership_type)\r\n // Show person name in subtitle if available, with level\r\n let subtitle = typeLabel || 'Membership'\r\n if (personName) {\r\n subtitle = record.level\r\n ? `${typeLabel} • ${record.level} • ${personName}`\r\n : `${typeLabel} • ${personName}`\r\n } else if (record.level) {\r\n subtitle = `${typeLabel} • ${record.level}`\r\n }\r\n\r\n return {\r\n id: record.id,\r\n entityType: 'membership_record',\r\n name: record.name,\r\n subtitle,\r\n keywords: [record.membership_type, record.membership_number, record.level, record.alliance, personName].filter(Boolean) as string[],\r\n icon: 'Ticket',\r\n route: `/records?type=membership_record&id=${record.id}`,\r\n routeParams: { id: record.id },\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Master indexer\r\n// ============================================================================\r\n\r\nexport interface AllEntitiesData {\r\n properties?: PropertyEntity[]\r\n vehicles?: VehicleEntity[]\r\n pets?: PetEntity[]\r\n contacts?: ContactEntity[]\r\n subscriptions?: SubscriptionEntity[]\r\n services?: ServiceEntity[]\r\n insurancePolicies?: InsuranceEntity[]\r\n valuables?: ValuableEntity[]\r\n financialAccounts?: FinancialAccountEntity[]\r\n credentials?: CredentialEntity[]\r\n petVetVisits?: PetVetVisitEntity[]\r\n vehicleServices?: VehicleServiceEntity[]\r\n maintenanceTasks?: MaintenanceTaskEntity[]\r\n legalDocuments?: LegalDocumentEntity[]\r\n accessCodes?: AccessCodeEntity[]\r\n devices?: DeviceEntity[]\r\n people?: PersonEntity[]\r\n healthRecords?: HealthRecordEntity[]\r\n educationRecords?: EducationRecordEntity[]\r\n militaryRecords?: MilitaryRecordEntity[]\r\n membershipRecords?: MembershipRecordEntity[]\r\n homeImprovements?: HomeImprovementEntity[]\r\n taxYears?: TaxYearEntity[]\r\n}\r\n\r\n/**\r\n * Index all entities into a unified searchable list\r\n */\r\nexport function indexAllEntities(data: AllEntitiesData): SearchableEntity[] {\r\n const entities: SearchableEntity[] = []\r\n\r\n // Build lookup maps for related entity names\r\n const propertyNames = new Map<string, string>()\r\n const vehicleNames = new Map<string, string>()\r\n const petNames = new Map<string, string>()\r\n const personNames = new Map<string, string>()\r\n\r\n data.properties?.forEach(p => propertyNames.set(p.id, p.name))\r\n data.vehicles?.forEach(v => vehicleNames.set(v.id, getVehicleName(v)))\r\n data.pets?.forEach(p => petNames.set(p.id, p.name))\r\n data.people?.forEach(p => personNames.set(p.id, p.name))\r\n\r\n // Build reverse index: contact_id -> entities that reference them (for \"Pumpkin's vet\")\r\n const contactLinkedEntities = new Map<string, ContactLinkedEntity[]>()\r\n const addContactLink = (contactId: string, name: string, role: string) => {\r\n const existing = contactLinkedEntities.get(contactId) || []\r\n existing.push({ name, role })\r\n contactLinkedEntities.set(contactId, existing)\r\n }\r\n\r\n // Collect contact relationships from pets, vehicles, properties\r\n data.pets?.forEach(pet => {\r\n pet.contact_relationships?.forEach(rel => {\r\n addContactLink(rel.contact_id, pet.name, rel.role)\r\n })\r\n })\r\n data.vehicles?.forEach(vehicle => {\r\n const name = getVehicleName(vehicle)\r\n vehicle.contact_relationships?.forEach(rel => {\r\n addContactLink(rel.contact_id, name, rel.role)\r\n })\r\n })\r\n data.properties?.forEach(property => {\r\n property.contact_relationships?.forEach(rel => {\r\n addContactLink(rel.contact_id, property.name, rel.role)\r\n })\r\n })\r\n\r\n // Index each entity type\r\n data.properties?.forEach(p => entities.push(indexProperty(p)))\r\n data.vehicles?.forEach(v => entities.push(indexVehicle(v)))\r\n data.pets?.forEach(p => entities.push(indexPet(p)))\r\n data.contacts?.forEach(c => {\r\n const linkedEntities = contactLinkedEntities.get(c.id)\r\n entities.push(indexContact(c, linkedEntities))\r\n })\r\n data.subscriptions?.forEach(s => entities.push(indexSubscription(s)))\r\n data.services?.forEach(s => entities.push(indexService(s)))\r\n\r\n // Insurance with covered asset names\r\n data.insurancePolicies?.forEach(policy => {\r\n const coveredAssetNames: string[] = []\r\n // Check relationships array (new format)\r\n policy.relationships?.forEach(rel => {\r\n if (rel.entity_type === 'property') {\r\n const name = propertyNames.get(rel.entity_id)\r\n if (name) coveredAssetNames.push(name)\r\n } else if (rel.entity_type === 'vehicle') {\r\n const name = vehicleNames.get(rel.entity_id)\r\n if (name) coveredAssetNames.push(name)\r\n } else if (rel.entity_type === 'pet') {\r\n const name = petNames.get(rel.entity_id)\r\n if (name) coveredAssetNames.push(name)\r\n }\r\n })\r\n // Also check deprecated fields\r\n if (policy.property_id) {\r\n const name = propertyNames.get(policy.property_id)\r\n if (name && !coveredAssetNames.includes(name)) coveredAssetNames.push(name)\r\n }\r\n if (policy.vehicle_id) {\r\n const name = vehicleNames.get(policy.vehicle_id)\r\n if (name && !coveredAssetNames.includes(name)) coveredAssetNames.push(name)\r\n }\r\n if (policy.pet_id) {\r\n const name = petNames.get(policy.pet_id)\r\n if (name && !coveredAssetNames.includes(name)) coveredAssetNames.push(name)\r\n }\r\n entities.push(indexInsurance(policy, coveredAssetNames.length > 0 ? coveredAssetNames : undefined))\r\n })\r\n data.valuables?.forEach(v => entities.push(indexValuable(v)))\r\n data.financialAccounts?.forEach(a => {\r\n const ownerNames = a.owner_ids\r\n ?.map(id => personNames.get(id))\r\n .filter(Boolean) as string[] | undefined\r\n entities.push(indexFinancialAccount(a, ownerNames))\r\n })\r\n data.credentials?.forEach(c => {\r\n const personName = c.person_id ? personNames.get(c.person_id) : undefined\r\n entities.push(indexCredential(c, personName))\r\n })\r\n\r\n data.petVetVisits?.forEach(v => {\r\n const petName = v.pet_id ? petNames.get(v.pet_id) : undefined\r\n entities.push(indexPetVetVisit(v, petName))\r\n })\r\n\r\n data.vehicleServices?.forEach(s => {\r\n const vehicleName = s.vehicle_id ? vehicleNames.get(s.vehicle_id) : undefined\r\n entities.push(indexVehicleService(s, vehicleName))\r\n })\r\n\r\n data.maintenanceTasks?.forEach(t => {\r\n let assetName: string | undefined\r\n if (t.property_id) assetName = propertyNames.get(t.property_id)\r\n else if (t.vehicle_id) assetName = vehicleNames.get(t.vehicle_id)\r\n entities.push(indexMaintenanceTask(t, assetName))\r\n })\r\n\r\n data.legalDocuments?.forEach(d => {\r\n const residentNames = d.resident_ids\r\n ?.map(id => personNames.get(id))\r\n .filter(Boolean) as string[] | undefined\r\n entities.push(indexLegalDocument(d, residentNames))\r\n })\r\n\r\n data.accessCodes?.forEach(c => {\r\n const propertyName = c.property_id ? propertyNames.get(c.property_id) : undefined\r\n entities.push(indexAccessCode(c, propertyName))\r\n })\r\n\r\n data.devices?.forEach(d => entities.push(indexDevice(d)))\r\n data.people?.forEach(p => entities.push(indexPerson(p)))\r\n\r\n // Health records (linked to person or pet)\r\n data.healthRecords?.forEach(r => {\r\n let ownerName: string | undefined\r\n if (r.person_id) ownerName = personNames.get(r.person_id)\r\n else if (r.pet_id) ownerName = petNames.get(r.pet_id)\r\n entities.push(indexHealthRecord(r, ownerName))\r\n })\r\n\r\n // Education records (linked to person)\r\n data.educationRecords?.forEach(r => {\r\n const personName = r.person_id ? personNames.get(r.person_id) : undefined\r\n entities.push(indexEducationRecord(r, personName))\r\n })\r\n\r\n // Military records (linked to person)\r\n data.militaryRecords?.forEach(r => {\r\n const personName = r.person_id ? personNames.get(r.person_id) : undefined\r\n entities.push(indexMilitaryRecord(r, personName))\r\n })\r\n\r\n // Membership records (linked to person)\r\n data.membershipRecords?.forEach(r => {\r\n const personName = r.person_id ? personNames.get(r.person_id) : undefined\r\n entities.push(indexMembershipRecord(r, personName))\r\n })\r\n\r\n // Home improvements (linked to property)\r\n data.homeImprovements?.forEach(i => {\r\n const propertyName = i.property_id ? propertyNames.get(i.property_id) : undefined\r\n entities.push(indexHomeImprovement(i, propertyName))\r\n })\r\n\r\n // Tax years (linked to filers/people)\r\n data.taxYears?.forEach(t => {\r\n const filerNames = t.filer_ids\r\n ?.map(id => personNames.get(id))\r\n .filter(Boolean) as string[] | undefined\r\n entities.push(indexTaxYear(t, filerNames))\r\n })\r\n\r\n return entities\r\n}\r\n","/**\r\n * Contact Adapter\r\n *\r\n * Converts Contact entities to ListItemData for rendering.\r\n * Platform-agnostic - uses TranslateFunction instead of IntlShape.\r\n */\r\n\r\nimport type {\r\n Contact,\r\n ContactType,\r\n} from '@hearthcoo/types'\r\nimport {\r\n getRolesForEntityType,\r\n CONTACT_TYPE_ICONS,\r\n getContactDisplayName,\r\n} from '@hearthcoo/types'\r\nimport type {\r\n ListItemData,\r\n ListItemSubtitle,\r\n ListItemActionBadge,\r\n BaseAdapterContext,\r\n RenderMode,\r\n} from '../types'\r\nimport type { ComputedFields } from '../computedFields'\r\n\r\nexport interface ContactAdapterContext extends BaseAdapterContext {\r\n renderMode?: RenderMode\r\n role?: string // The role this contact has (e.g., 'groomer', 'mechanic', custom role)\r\n}\r\n\r\n/**\r\n * Color mapping for contact types\r\n */\r\nexport const CONTACT_TYPE_COLORS: Record<ContactType, string> = {\r\n accountant: 'green',\r\n babysitter: 'pink',\r\n chiropractor: 'blue',\r\n contractor: 'orange',\r\n dentist: 'blue',\r\n doctor: 'red',\r\n electrician: 'yellow',\r\n emergency: 'red',\r\n financial_advisor: 'green',\r\n friend_family: 'purple',\r\n handyman: 'orange',\r\n home_organizer: 'teal',\r\n house_cleaner: 'pink',\r\n hvac_technician: 'orange',\r\n insurance_agent: 'blue',\r\n landscaper: 'green',\r\n lawyer: 'purple',\r\n mechanic: 'orange',\r\n notary: 'gray',\r\n pest_control: 'yellow',\r\n pet_service: 'pink',\r\n pharmacist: 'blue',\r\n physical_therapist: 'blue',\r\n plumber: 'blue',\r\n pool_service: 'blue',\r\n professional: 'gray',\r\n real_estate_agent: 'green',\r\n service_provider: 'orange',\r\n therapist: 'purple',\r\n tutor: 'blue',\r\n vendor: 'gray',\r\n veterinarian: 'red',\r\n car_dealership: 'blue',\r\n vet_clinic: 'red',\r\n other: 'gray',\r\n}\r\n\r\nconst getContactIcon = (contactType: ContactType): string => {\r\n return CONTACT_TYPE_ICONS[contactType] || 'UserCircle'\r\n}\r\n\r\nconst getContactColor = (contactType: ContactType): string => {\r\n return CONTACT_TYPE_COLORS[contactType] || 'gray'\r\n}\r\n\r\n/**\r\n * Get role label (translated if predefined, raw if custom)\r\n */\r\nconst getRoleLabel = (role: string, t: BaseAdapterContext['t']): string => {\r\n // Check if it's a predefined role (include all entity types)\r\n const allPredefined = [\r\n ...getRolesForEntityType('pet'),\r\n ...getRolesForEntityType('vehicle'),\r\n ...getRolesForEntityType('resident'),\r\n ...getRolesForEntityType('property'),\r\n ...getRolesForEntityType('service'),\r\n ]\r\n\r\n if (allPredefined.includes(role as any)) {\r\n return t(`contacts.role.${role}`)\r\n }\r\n\r\n // Custom role - return as-is\r\n return role\r\n}\r\n\r\n/**\r\n * Get computed fields for a contact\r\n * Used by contactAdapter internally and can be used directly by MCP, etc.\r\n */\r\nexport function getContactComputedFields(contact: Contact): ComputedFields {\r\n const computed: ComputedFields = {\r\n displayName: getContactDisplayName(contact),\r\n }\r\n\r\n if (contact.type) {\r\n computed.formattedType = contact.type.charAt(0).toUpperCase() + contact.type.slice(1).replace(/_/g, ' ')\r\n }\r\n\r\n return computed\r\n}\r\n\r\nexport function contactAdapter(contact: Contact & { entity_type?: string }, context: ContactAdapterContext): ListItemData {\r\n const { t, navigate, renderMode = 'primary', role } = context\r\n const entityType = contact.entity_type\r\n if (!entityType) {\r\n throw new Error('contactAdapter requires entity_type on the contact object')\r\n }\r\n const isChild = renderMode === 'child'\r\n\r\n const typeLabel = t(`contacts.type_${contact.type}`)\r\n const name = getContactDisplayName(contact) || typeLabel\r\n\r\n // Build subtitle with clickable phone and email links\r\n const subtitleLinks: Array<{ text: string; href: string }> = []\r\n\r\n if (contact.phone_primary) {\r\n subtitleLinks.push({\r\n text: contact.phone_primary,\r\n href: `tel:${contact.phone_primary}`\r\n })\r\n }\r\n\r\n if (contact.email) {\r\n subtitleLinks.push({\r\n text: contact.email,\r\n href: `mailto:${contact.email}`\r\n })\r\n }\r\n\r\n // Build subtitle - use links if available, otherwise use type/specialty\r\n const subtitle: ListItemSubtitle = subtitleLinks.length > 0\r\n ? {\r\n type: 'link-text',\r\n text: subtitleLinks.map(l => l.text).join(' • '),\r\n links: subtitleLinks\r\n }\r\n : {\r\n type: 'text',\r\n text: [typeLabel, contact.specialty].filter(Boolean).join(' • ')\r\n }\r\n\r\n // Build badges\r\n const badges: ListItemData['badges'] = []\r\n\r\n // Role badge (shown first in child mode when role is provided)\r\n if (role) {\r\n badges.push({\r\n id: 'role',\r\n text: getRoleLabel(role, t),\r\n variant: 'blue',\r\n })\r\n }\r\n\r\n // Type badge (always show in primary mode, optionally in child mode)\r\n if (!isChild || !role) {\r\n badges.push({\r\n id: 'type',\r\n text: typeLabel,\r\n variant: 'secondary',\r\n })\r\n }\r\n\r\n // Rating badge\r\n if (contact.rating) {\r\n badges.push({\r\n id: 'rating',\r\n text: `${contact.rating}/5`,\r\n icon: 'Star',\r\n variant: 'outline',\r\n })\r\n }\r\n\r\n // Favorite badge\r\n if (contact.is_favorite) {\r\n badges.push({\r\n id: 'favorite',\r\n text: t('contacts.favorite_badge'),\r\n icon: 'Heart',\r\n variant: 'default',\r\n })\r\n }\r\n\r\n // Build sections (only for primary mode)\r\n const sections: ListItemData['sections'] = []\r\n\r\n if (renderMode === 'primary') {\r\n // Contact info section\r\n const contactFields: any[] = []\r\n\r\n if (contact.email) {\r\n contactFields.push({\r\n label: t('contacts.email'),\r\n value: contact.email,\r\n icon: 'Mail',\r\n format: 'email',\r\n })\r\n }\r\n\r\n if (contact.phone_primary) {\r\n contactFields.push({\r\n label: t('contacts.phone_primary'),\r\n value: contact.phone_primary,\r\n icon: 'Phone',\r\n format: 'phone',\r\n })\r\n }\r\n\r\n if (contact.phone_secondary) {\r\n contactFields.push({\r\n label: t('contacts.phone_secondary'),\r\n value: contact.phone_secondary,\r\n icon: 'Phone',\r\n format: 'phone',\r\n })\r\n }\r\n\r\n if (contact.website) {\r\n contactFields.push({\r\n label: t('contacts.website'),\r\n value: contact.website,\r\n icon: 'Globe',\r\n format: 'url',\r\n })\r\n }\r\n\r\n if (contactFields.length > 0) {\r\n sections.push({\r\n id: 'contact-info',\r\n type: 'key-value-list',\r\n title: {\r\n text: t('contacts.contact_info'),\r\n },\r\n fields: contactFields,\r\n })\r\n }\r\n\r\n // Address section\r\n if (contact.street_address || contact.city || contact.state || contact.postal_code) {\r\n const addressParts = [contact.street_address, contact.city, contact.state, contact.postal_code]\r\n .filter(Boolean)\r\n .join(', ')\r\n\r\n sections.push({\r\n id: 'address',\r\n type: 'key-value-list',\r\n border: 'top',\r\n title: {\r\n text: t('contacts.address_info'),\r\n icon: 'MapPin',\r\n },\r\n fields: [{\r\n label: t('contacts.address'),\r\n value: addressParts,\r\n format: 'address',\r\n }],\r\n })\r\n }\r\n\r\n // Professional info section\r\n const professionalFields: any[] = []\r\n\r\n if (contact.specialty) {\r\n professionalFields.push({\r\n label: t('contacts.specialty'),\r\n value: contact.specialty,\r\n })\r\n }\r\n\r\n if (contact.license_number) {\r\n professionalFields.push({\r\n label: t('contacts.license_number'),\r\n value: contact.license_number,\r\n })\r\n }\r\n\r\n if (professionalFields.length > 0) {\r\n sections.push({\r\n id: 'professional',\r\n type: 'key-value-list',\r\n border: 'top',\r\n title: {\r\n text: t('contacts.professional_info'),\r\n },\r\n fields: professionalFields,\r\n })\r\n }\r\n\r\n // Notes section\r\n if (contact.notes) {\r\n sections.push({\r\n id: 'notes',\r\n type: 'text-block',\r\n border: 'top',\r\n title: {\r\n text: t('contacts.notes'),\r\n },\r\n text: contact.notes,\r\n })\r\n }\r\n }\r\n\r\n // Build action badges for child mode (View action in dropdown)\r\n const actionBadges: ListItemActionBadge[] | undefined = isChild ? [{\r\n id: 'actions',\r\n text: '',\r\n icon: 'MoreHorizontal',\r\n variant: 'outline',\r\n actions: [{\r\n id: 'view',\r\n label: t('common.view'),\r\n icon: 'Eye',\r\n onClick: () => navigate(`/contact/${contact.id}`),\r\n }],\r\n }] : undefined\r\n\r\n return {\r\n // Use composite id when role is provided (for child items with same contact in multiple roles)\r\n id: role ? `${contact.id}-${role}` : contact.id,\r\n renderMode,\r\n icon: {\r\n type: 'lucide-icon',\r\n value: getContactIcon(contact.type),\r\n bgColor: getContactColor(contact.type) as any,\r\n shape: 'square',\r\n size: isChild ? 'sm' : 'md',\r\n },\r\n title: name,\r\n subtitle,\r\n badges,\r\n actionBadges,\r\n sections: sections.length > 0 ? sections : undefined,\r\n }\r\n}\r\n","/**\r\n * Insurance Policy Adapter\r\n *\r\n * Converts InsurancePolicy entities to ListItemData for rendering.\r\n * Platform-agnostic - uses TranslateFunction instead of IntlShape.\r\n */\r\n\r\nimport type { InsurancePolicy, InsuranceType } from '@hearthcoo/types'\r\nimport { INSURANCE_TYPE_ICONS } from '@hearthcoo/types'\r\nimport type {\r\n ListItemData,\r\n ListItemAction,\r\n BaseAdapterContext,\r\n RenderMode,\r\n} from '../types'\r\nimport { defaultFormatDate } from '../types'\r\nimport { daysUntil } from '@hearthcoo/utils'\r\nimport type { ComputedFields } from '../computedFields'\r\nimport { getExpiryStatus } from '../computedFields'\r\n\r\n/**\r\n * Color mapping for insurance types\r\n */\r\nexport const INSURANCE_TYPE_COLORS: Record<InsuranceType, string> = {\r\n home: 'blue',\r\n auto: 'green',\r\n umbrella: 'purple',\r\n life: 'red',\r\n health: 'pink',\r\n pet: 'orange',\r\n collection: 'yellow',\r\n other: 'gray',\r\n}\r\n\r\nexport interface CoveredItem {\r\n id: string\r\n name: string\r\n type: string\r\n}\r\n\r\nexport interface InsuranceAdapterContext extends BaseAdapterContext {\r\n renderMode?: RenderMode\r\n coveredItems?: CoveredItem[]\r\n onRemoveCoverage?: (itemId: string) => void\r\n /** Callback to attach/replace card image (button click - opens dialog) */\r\n onAttachCardImage?: () => void\r\n /** Callback to attach/replace policy document (button click - opens dialog) */\r\n onAttachDocument?: () => void\r\n /** Callback when card image is uploaded via drag/drop */\r\n onCardImageUploaded?: (fileId: string) => void\r\n /** Callback when policy document is uploaded via drag/drop */\r\n onDocumentUploaded?: (fileId: string) => void\r\n /** Original filename for policy document (for display) */\r\n policyDocumentFileName?: string\r\n}\r\n\r\nconst getIconForType = (type: string): string => {\r\n switch (type) {\r\n case 'property': return 'Building2'\r\n case 'vehicle': return 'Car'\r\n case 'pet': return 'PawPrint'\r\n case 'resident': return 'User'\r\n case 'valuable': return 'Gem'\r\n default: return 'Shield'\r\n }\r\n}\r\n\r\nconst getBgColorForType = (type: string): 'blue' | 'green' | 'orange' | 'purple' | 'gray' | 'yellow' => {\r\n switch (type) {\r\n case 'property': return 'blue'\r\n case 'vehicle': return 'green'\r\n case 'pet': return 'orange'\r\n case 'resident': return 'purple'\r\n case 'valuable': return 'yellow'\r\n default: return 'gray'\r\n }\r\n}\r\n\r\nconst getRouteForType = (type: string, id: string): string | null => {\r\n switch (type) {\r\n case 'property': return `/property/${id}`\r\n case 'vehicle': return `/vehicle/${id}`\r\n case 'pet': return `/pet/${id}`\r\n case 'resident': return `/people?id=${id}` // People page uses query param\r\n case 'valuable': return `/valuables?id=${id}`\r\n default: return null // Unknown type - can't navigate\r\n }\r\n}\r\n\r\n/**\r\n * Get computed fields for an insurance policy\r\n * Used by insuranceAdapter internally and can be used directly by MCP, etc.\r\n */\r\nexport function getInsuranceComputedFields(policy: InsurancePolicy): ComputedFields {\r\n const computed: ComputedFields = {\r\n displayName: policy.provider || 'Insurance Policy',\r\n }\r\n\r\n if (policy.expiration_date) {\r\n computed.daysUntilExpiry = daysUntil(policy.expiration_date)\r\n computed.expiryStatus = getExpiryStatus(computed.daysUntilExpiry)\r\n }\r\n\r\n if (policy.type) {\r\n computed.formattedType = policy.type.charAt(0).toUpperCase() + policy.type.slice(1)\r\n }\r\n\r\n return computed\r\n}\r\n\r\nexport function insuranceAdapter(\r\n policy: InsurancePolicy & { entity_type?: string },\r\n context: InsuranceAdapterContext\r\n): ListItemData {\r\n const { t, navigate, renderMode = 'primary', coveredItems = [], onRemoveCoverage, onAttachCardImage, onAttachDocument, onCardImageUploaded, onDocumentUploaded, policyDocumentFileName } = context\r\n const entityType = policy.entity_type\r\n if (!entityType) {\r\n throw new Error('insuranceAdapter requires entity_type on the policy object')\r\n }\r\n const formatDate = context.formatDate || defaultFormatDate\r\n const isChild = renderMode === 'child'\r\n\r\n // Check if policy is expired\r\n const isExpired = policy.expiration_date\r\n ? new Date(policy.expiration_date) < new Date()\r\n : false\r\n\r\n // Build badges\r\n const badges: ListItemData['badges'] = isChild ? [\r\n // Minimal badges for child mode\r\n {\r\n id: 'type',\r\n text: t(`insurance.type_${policy.type}`),\r\n variant: 'outline',\r\n }\r\n ] : [\r\n // Full badges for primary mode\r\n {\r\n id: 'type',\r\n text: t(`insurance.type_${policy.type}`),\r\n variant: 'secondary',\r\n },\r\n ...(policy.expiration_date ? [{\r\n id: 'expires',\r\n text: isExpired\r\n ? t('insurance.expired')\r\n : `${t('insurance.expires')} ${formatDate(policy.expiration_date)}`,\r\n variant: (isExpired ? 'destructive' : 'outline') as 'destructive' | 'outline',\r\n }] : []),\r\n ]\r\n\r\n // Build sections\r\n const sections: ListItemData['sections'] = []\r\n\r\n // Get card images from documents array (new pattern) or legacy card_image_id\r\n const cardDocs = (policy.documents || []).filter(d => d.type === 'card' || d.type === 'scan')\r\n const hasCardImages = cardDocs.length > 0 || policy.card_image_id\r\n\r\n // Insurance card images - show all from documents array, plus legacy card_image_id for backwards compat\r\n if (hasCardImages) {\r\n // First show legacy card_image_id if it exists and not already in documents\r\n if (policy.card_image_id && !cardDocs.some(d => d.file_id === policy.card_image_id)) {\r\n sections.push({\r\n id: 'card-image-legacy',\r\n type: 'image',\r\n title: { text: t('insurance.card_image'), icon: 'CreditCard' },\r\n image: {\r\n fileId: policy.card_image_id,\r\n entityId: policy.id,\r\n entityType,\r\n alt: t('insurance.card_image_alt', { provider: policy.provider }),\r\n downloadLabel: t('insurance.download_card'),\r\n },\r\n })\r\n }\r\n\r\n // Show all documents from the documents array\r\n cardDocs.forEach((doc, index) => {\r\n if (doc.file_id) {\r\n const docTitle = cardDocs.length > 1\r\n ? `${t('insurance.card_image')} ${index + 1}`\r\n : t('insurance.card_image')\r\n sections.push({\r\n id: `card-image-${index}`,\r\n type: 'image',\r\n title: { text: docTitle, icon: 'CreditCard' },\r\n image: {\r\n fileId: doc.file_id,\r\n entityId: policy.id,\r\n entityType,\r\n alt: `${t('insurance.card_image_alt', { provider: policy.provider })} - ${index + 1}`,\r\n downloadLabel: t('insurance.download_card'),\r\n },\r\n })\r\n }\r\n })\r\n } else if (!isChild && (onAttachCardImage || onCardImageUploaded)) {\r\n // Show upload placeholder only in primary mode when no images exist\r\n sections.push({\r\n id: 'card-upload',\r\n type: 'upload-placeholder',\r\n title: { text: t('insurance.card_image'), icon: 'CreditCard' },\r\n placeholder: {\r\n icon: 'ImagePlus',\r\n text: t('insurance.no_card_image'),\r\n buttonLabel: t('insurance.upload_card'),\r\n onUpload: onAttachCardImage,\r\n entityType,\r\n entityId: policy.id,\r\n accept: 'image/jpeg,image/png,image/webp,image/heic,image/heif,application/pdf',\r\n onFileUploaded: onCardImageUploaded,\r\n },\r\n })\r\n }\r\n\r\n // Policy document is added as a child card below (not in sections)\r\n\r\n // Details section (only in primary mode - child mode keeps it compact)\r\n if (!isChild) {\r\n const detailFields: any[] = []\r\n if (policy.effective_date) {\r\n detailFields.push({\r\n label: t('insurance.effective_date'),\r\n value: policy.effective_date,\r\n format: 'date',\r\n })\r\n }\r\n if (policy.expiration_date) {\r\n detailFields.push({\r\n label: t('insurance.expiration_date'),\r\n value: policy.expiration_date,\r\n format: 'date',\r\n })\r\n }\r\n if (detailFields.length > 0) {\r\n sections.push({\r\n id: 'details',\r\n type: 'key-value-grid',\r\n fields: detailFields,\r\n })\r\n }\r\n }\r\n\r\n // Build children for covered items (only in primary mode)\r\n const childrenItems: ListItemData[] = []\r\n\r\n if (!isChild && coveredItems.length > 0) {\r\n coveredItems.forEach(item => {\r\n const route = getRouteForType(item.type, item.id)\r\n const actions: ListItemAction[] = []\r\n\r\n // Only add View action if we have a valid route\r\n if (route) {\r\n actions.push({\r\n id: 'view',\r\n label: t('common.view'),\r\n icon: 'Eye',\r\n onClick: () => navigate(route),\r\n })\r\n }\r\n\r\n if (onRemoveCoverage) {\r\n actions.push({\r\n id: 'remove',\r\n label: t('insurance.remove_coverage'),\r\n icon: 'Trash2',\r\n variant: 'destructive',\r\n onClick: () => onRemoveCoverage(item.id),\r\n })\r\n }\r\n\r\n childrenItems.push({\r\n id: `covered-${item.id}`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: getIconForType(item.type),\r\n bgColor: getBgColorForType(item.type),\r\n shape: 'square',\r\n size: 'sm',\r\n },\r\n title: item.name,\r\n subtitle: {\r\n type: 'text',\r\n text: t(`insurance.covered_${item.type}`),\r\n },\r\n actionBadges: actions.length > 0 ? [{\r\n id: 'actions',\r\n text: '',\r\n icon: 'MoreHorizontal',\r\n variant: 'outline',\r\n actions,\r\n }] : undefined,\r\n })\r\n })\r\n }\r\n\r\n // Add policy document as last child (only in primary mode)\r\n if (!isChild) {\r\n if (policy.policy_document_id) {\r\n childrenItems.push({\r\n id: `document-${policy.id}`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'FileText',\r\n bgColor: 'gray',\r\n size: 'sm',\r\n },\r\n title: t('insurance.policy_document'),\r\n sections: [{\r\n id: 'policy-document',\r\n type: 'document',\r\n document: {\r\n fileId: policy.policy_document_id,\r\n entityId: policy.id,\r\n entityType,\r\n fileName: policyDocumentFileName || t('insurance.policy_document_file'),\r\n downloadLabel: t('insurance.download_document'),\r\n },\r\n }],\r\n })\r\n } else if (onAttachDocument || onDocumentUploaded) {\r\n childrenItems.push({\r\n id: `document-upload-${policy.id}`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'FilePlus',\r\n bgColor: 'gray',\r\n size: 'sm',\r\n },\r\n title: t('insurance.policy_document'),\r\n sections: [{\r\n id: 'document-upload',\r\n type: 'upload-placeholder',\r\n placeholder: {\r\n icon: 'FilePlus',\r\n text: t('insurance.no_policy_document'),\r\n buttonLabel: t('insurance.upload_document'),\r\n onUpload: onAttachDocument,\r\n entityType,\r\n entityId: policy.id,\r\n accept: 'application/pdf',\r\n onFileUploaded: onDocumentUploaded,\r\n },\r\n }],\r\n })\r\n }\r\n }\r\n\r\n const children: ListItemData['children'] = childrenItems.length > 0 ? {\r\n title: t('insurance.covers'),\r\n items: childrenItems,\r\n } : undefined\r\n\r\n // Build action badges (only for child mode - view action)\r\n let actionBadges: ListItemData['actionBadges'] = undefined\r\n\r\n if (isChild) {\r\n // Child mode: View action only\r\n actionBadges = [{\r\n id: 'actions',\r\n text: '',\r\n icon: 'MoreHorizontal',\r\n variant: 'outline' as const,\r\n actions: [{\r\n id: 'view',\r\n label: t('common.view'),\r\n icon: 'Eye',\r\n onClick: () => navigate(`/insurance/${policy.id}`),\r\n }],\r\n }]\r\n }\r\n // Note: Attach actions (attach card, attach document) are added via\r\n // getAdditionalActions in the InsurancePage component, so they appear\r\n // in the main \"...\" dropdown alongside Edit, Delete, etc.\r\n\r\n // Get type-specific icon and color\r\n const insuranceIcon = INSURANCE_TYPE_ICONS[policy.type] || 'Shield'\r\n const insuranceColor = INSURANCE_TYPE_COLORS[policy.type] || 'gray'\r\n\r\n return {\r\n id: policy.id,\r\n renderMode,\r\n icon: {\r\n type: 'lucide-icon',\r\n value: insuranceIcon,\r\n bgColor: insuranceColor as any,\r\n shape: 'square',\r\n size: isChild ? 'sm' : 'md',\r\n },\r\n title: policy.provider,\r\n subtitle: {\r\n type: 'text',\r\n text: policy.policy_number\r\n ? `${t('insurance.policy')} #${policy.policy_number}`\r\n : t(`insurance.type_${policy.type}`),\r\n },\r\n badges,\r\n actionBadges,\r\n sections: sections.length > 0 ? sections : undefined,\r\n children,\r\n }\r\n}\r\n","/**\r\n * Pet Adapter\r\n *\r\n * Converts Pet entities to ListItemData for rendering.\r\n * Platform-agnostic - uses TranslateFunction instead of IntlShape.\r\n */\r\n\r\nimport type {\r\n Pet,\r\n Contact,\r\n InsurancePolicy,\r\n Property,\r\n PetSpecies,\r\n HealthRecord,\r\n} from '@hearthcoo/types'\r\nimport { PET_SPECIES_ICONS } from '@hearthcoo/types'\r\nimport { calculateDetailedAge, calculateCatHumanYears, calculateDogHumanYears } from '@hearthcoo/utils'\r\nimport type {\r\n ListItemData,\r\n ListItemAction,\r\n BaseAdapterContext,\r\n RenderMode,\r\n IconBgColor,\r\n} from '../types'\r\nimport { defaultFormatDate, createCustomKeyTypeBadge } from '../types'\r\nimport { contactAdapter } from './contactAdapter'\r\nimport { importantInfoAdapter } from './importantInfoAdapter'\r\nimport { insuranceAdapter } from './insuranceAdapter'\r\nimport type { ComputedFields } from '../computedFields'\r\n\r\n// Default key type for pets\r\nconst DEFAULT_KEY_TYPE = 'general'\r\n\r\n/**\r\n * Color mapping for pet species\r\n */\r\nexport const PET_SPECIES_COLORS: Record<PetSpecies, IconBgColor> = {\r\n dog: 'orange',\r\n cat: 'purple',\r\n bird: 'blue',\r\n fish: 'blue',\r\n rabbit: 'pink',\r\n hamster: 'orange',\r\n guinea_pig: 'pink',\r\n reptile: 'green',\r\n small_mammal: 'orange',\r\n other: 'gray',\r\n}\r\n\r\n/**\r\n * Helper to check if a policy covers an entity\r\n */\r\nconst policyCoversEntity = (policy: InsurancePolicy, entityId: string): boolean => {\r\n // Check new relationships system\r\n if (policy.relationships?.some(r => r.type === 'covers' && r.to === entityId)) {\r\n return true\r\n }\r\n // Fallback to legacy direct ID fields\r\n return policy.pet_id === entityId ||\r\n policy.property_id === entityId ||\r\n policy.vehicle_id === entityId\r\n}\r\n\r\nexport interface PetAdapterContext extends BaseAdapterContext {\r\n renderMode?: RenderMode\r\n insurancePolicies?: InsurancePolicy[]\r\n contacts?: Contact[]\r\n properties?: Property[]\r\n healthRecords?: HealthRecord[]\r\n // Relationship action callbacks\r\n onRemoveProperty?: (petId: string) => void\r\n // Generic callback for opening important info dialog\r\n onOpenImportantInfoDialog?: (pet: Pet, editInfoId?: string) => void\r\n // Callback when weight chart datapoint is clicked (for showing closest photo by date)\r\n onWeightChartClick?: (pet: Pet, date: string) => void\r\n // Callback when gallery thumbnail is clicked (for direct navigation by index)\r\n onGalleryPhotoClick?: (entityId: string, index: number) => void\r\n // Override photo to display (from chart/gallery click) - map of petId -> { fileId, date, index }\r\n chartPhotoOverrides?: Record<string, { fileId: string; date: string; index: number } | null>\r\n // Navigation callbacks for photo browsing\r\n onPhotoNavigate?: (petId: string, direction: 'prev' | 'next') => void\r\n // Callback to open photo history with add photo triggered\r\n onAddPhoto?: (petId: string) => void\r\n // Share mode - disables progressive loading since thumbnails may not be available\r\n shareMode?: boolean\r\n // Callback to manage pet tag (open mode switcher)\r\n onManageTag?: (pet: Pet) => void\r\n // Callback to create pet tag (open tag setup)\r\n onCreateTag?: (pet: Pet) => void\r\n // Callback to copy pet tag URL\r\n onCopyTag?: (pet: Pet) => void\r\n // Callback to delete pet tag\r\n onDeleteTag?: (pet: Pet) => void\r\n}\r\n\r\nconst getSpeciesIcon = (species: PetSpecies): string => {\r\n return PET_SPECIES_ICONS[species] || 'PawPrint'\r\n}\r\n\r\nconst getSpeciesColor = (species: PetSpecies): IconBgColor => {\r\n return PET_SPECIES_COLORS[species] || 'gray'\r\n}\r\n\r\n/**\r\n * Get computed fields for a pet entity\r\n * Used by petAdapter internally and can be used directly by MCP, etc.\r\n */\r\nexport function getPetComputedFields(pet: Pet): ComputedFields {\r\n const computed: ComputedFields = {\r\n displayName: pet.name,\r\n }\r\n\r\n // Calculate age\r\n const detailedAge = calculateDetailedAge(pet.date_of_birth)\r\n if (detailedAge !== null && !pet.memorialized_at) {\r\n // Format age string\r\n if (detailedAge.years < 1) {\r\n const months = detailedAge.months || 1\r\n computed.age = `${months} month${months !== 1 ? 's' : ''}`\r\n } else {\r\n computed.age = `${detailedAge.years} year${detailedAge.years !== 1 ? 's' : ''}`\r\n }\r\n\r\n // Human years for cats and dogs\r\n if (pet.species === 'cat') {\r\n computed.humanYears = calculateCatHumanYears(detailedAge.fractionalYears)\r\n } else if (pet.species === 'dog') {\r\n // Determine dog size from weight\r\n let size: 'small' | 'medium' | 'large' = 'medium'\r\n const weightHistory = pet.weight_history || []\r\n const latestWeight = weightHistory.length > 0\r\n ? [...weightHistory].sort((a, b) => b.date.localeCompare(a.date))[0]\r\n : null\r\n const weightInLbs = latestWeight\r\n ? (latestWeight.unit === 'kg' ? latestWeight.weight * 2.20462 : latestWeight.weight)\r\n : pet.weight_lbs\r\n if (weightInLbs) {\r\n if (weightInLbs < 20) size = 'small'\r\n else if (weightInLbs > 50) size = 'large'\r\n }\r\n computed.humanYears = calculateDogHumanYears(detailedAge.fractionalYears, size)\r\n }\r\n }\r\n\r\n // Formatted species\r\n if (pet.species) {\r\n computed.formattedType = pet.species.charAt(0).toUpperCase() + pet.species.slice(1).replace(/_/g, ' ')\r\n }\r\n\r\n return computed\r\n}\r\n\r\nexport function petAdapter(pet: Pet & { entity_type?: string }, context: PetAdapterContext): ListItemData {\r\n const {\r\n t,\r\n navigate,\r\n renderMode = 'primary',\r\n insurancePolicies = [],\r\n contacts = [],\r\n properties = [],\r\n healthRecords = [],\r\n onRemoveProperty,\r\n onOpenImportantInfoDialog,\r\n onWeightChartClick,\r\n onGalleryPhotoClick,\r\n chartPhotoOverrides = {},\r\n onPhotoNavigate,\r\n onAddPhoto,\r\n shareMode = false,\r\n onManageTag,\r\n onCreateTag,\r\n onCopyTag,\r\n onDeleteTag,\r\n } = context\r\n const entityType = pet.entity_type\r\n if (!entityType) {\r\n throw new Error('petAdapter requires entity_type on the pet object')\r\n }\r\n const formatDate = context.formatDate || defaultFormatDate\r\n\r\n const isChild = renderMode === 'child'\r\n const detailedAge = calculateDetailedAge(pet.date_of_birth)\r\n\r\n // Find associated property\r\n const petProperty = pet.property_id ? properties.find(p => p.id === pet.property_id) : null\r\n\r\n // Find associated insurance (using new relationships system)\r\n const petInsurance = insurancePolicies.filter(policy => policyCoversEntity(policy, pet.id))\r\n\r\n // Filter health records for this pet\r\n const petHealthRecords = healthRecords.filter(hr => hr.pet_id === pet.id)\r\n\r\n // Find associated contacts from contact_relationships\r\n const petContactRelationships = pet.contact_relationships || []\r\n const petContacts = petContactRelationships.map(rel => {\r\n const contact = contacts.find(c => c.id === rel.contact_id)\r\n return contact ? { contact, role: rel.role } : null\r\n }).filter((item): item is { contact: Contact; role: string } => item !== null)\r\n\r\n // Build badges\r\n const badges: ListItemData['badges'] = []\r\n\r\n // Add custom key type badge if different from default\r\n const customKeyBadge = createCustomKeyTypeBadge(pet.key_type, DEFAULT_KEY_TYPE, t)\r\n if (customKeyBadge) {\r\n badges.push(customKeyBadge)\r\n }\r\n\r\n // Species badge\r\n badges.push({\r\n id: 'species',\r\n text: t(`pets.species_${pet.species}`),\r\n variant: 'secondary',\r\n })\r\n\r\n // Breed badge\r\n if (pet.breed) {\r\n badges.push({\r\n id: 'breed',\r\n text: pet.breed,\r\n variant: 'outline',\r\n })\r\n }\r\n\r\n // Sex badge\r\n if (pet.sex) {\r\n badges.push({\r\n id: 'sex',\r\n text: t(`pets.sex_${pet.sex}`),\r\n variant: 'outline',\r\n })\r\n }\r\n\r\n // Age badge (skip for memorialized pets)\r\n if (detailedAge !== null && !pet.memorialized_at) {\r\n // Show months if under 1 year, otherwise show years\r\n if (detailedAge.years < 1) {\r\n badges.push({\r\n id: 'age',\r\n text: t('pets.months_old', { months: detailedAge.months || 1 }), // At least 1 month\r\n variant: 'outline',\r\n })\r\n } else {\r\n badges.push({\r\n id: 'age',\r\n text: t('pets.years_old', { years: detailedAge.years }),\r\n variant: 'outline',\r\n })\r\n }\r\n\r\n // Human years for cats and dogs (using fractional years for accuracy)\r\n if (pet.species === 'cat') {\r\n const humanYears = calculateCatHumanYears(detailedAge.fractionalYears)\r\n badges.push({\r\n id: 'human-years',\r\n text: t('pets.human_years', { years: humanYears }),\r\n variant: 'secondary',\r\n })\r\n } else if (pet.species === 'dog') {\r\n // Determine dog size from weight if available, otherwise default to medium\r\n // Prefer latest weight from weight_history, fallback to deprecated weight_lbs\r\n let size: 'small' | 'medium' | 'large' = 'medium'\r\n const weightHistory = pet.weight_history || []\r\n const latestWeight = weightHistory.length > 0\r\n ? weightHistory.sort((a, b) => b.date.localeCompare(a.date))[0]\r\n : null\r\n // Convert to lbs for size calculation\r\n const weightInLbs = latestWeight\r\n ? (latestWeight.unit === 'kg' ? latestWeight.weight * 2.20462 : latestWeight.weight)\r\n : pet.weight_lbs\r\n if (weightInLbs) {\r\n if (weightInLbs < 20) size = 'small'\r\n else if (weightInLbs > 50) size = 'large'\r\n }\r\n const humanYears = calculateDogHumanYears(detailedAge.fractionalYears, size)\r\n badges.push({\r\n id: 'human-years',\r\n text: t('pets.human_years', { years: humanYears }),\r\n variant: 'secondary',\r\n })\r\n }\r\n }\r\n\r\n // Note: Property and Insurance are shown as child cards below, not as action badges\r\n\r\n // Photo count badge\r\n const photoCount = (pet.photo_history || []).length\r\n if (photoCount > 0) {\r\n badges.push({\r\n id: 'photos',\r\n text: t('pets.photo_count', { count: photoCount }),\r\n variant: 'outline',\r\n icon: 'Camera',\r\n })\r\n }\r\n\r\n // Build sections (only for primary mode)\r\n const sections: ListItemData['sections'] = []\r\n\r\n if (renderMode === 'primary') {\r\n // Photo section (at the top of expanded content)\r\n // Priority: 1) Chart override photo, 2) Default photo (photo_file_id), 3) Latest from photo_history\r\n const chartOverride = chartPhotoOverrides[pet.id]\r\n const photoHistory = pet.photo_history || []\r\n // Sort chronologically for consistent navigation\r\n const sortedPhotoHistory = [...photoHistory].sort((a, b) => a.date.localeCompare(b.date))\r\n const latestPhoto = sortedPhotoHistory.length > 0\r\n ? sortedPhotoHistory[sortedPhotoHistory.length - 1]\r\n : null\r\n\r\n // Determine which photo to show and navigation state\r\n // isHistoryPhoto = true when we're showing a photo from photo_history (not the profile photo)\r\n let photoToShow: { fileId: string; date?: string; isHistoryPhoto?: boolean; index: number; aspectRatio?: number } | null = null\r\n const historyLength = sortedPhotoHistory.length\r\n\r\n if (chartOverride) {\r\n // Showing a specific photo from chart click\r\n const photo = sortedPhotoHistory[chartOverride.index]\r\n photoToShow = { fileId: chartOverride.fileId, date: chartOverride.date, isHistoryPhoto: true, index: chartOverride.index, aspectRatio: photo?.aspect_ratio }\r\n } else if (pet.photo_file_id) {\r\n // Showing the default profile photo (not from history, no navigation)\r\n photoToShow = { fileId: pet.photo_file_id, index: -1 }\r\n } else if (latestPhoto) {\r\n // Fallback to latest from photo_history - enable navigation\r\n const latestIndex = historyLength - 1\r\n photoToShow = { fileId: latestPhoto.file_id, date: latestPhoto.date, isHistoryPhoto: true, index: latestIndex, aspectRatio: latestPhoto.aspect_ratio }\r\n }\r\n\r\n if (photoToShow) {\r\n // Navigation is available when viewing a history photo and there's more than 1 photo\r\n const canNavigate = photoToShow.isHistoryPhoto && historyLength > 1\r\n const hasPrev = canNavigate && photoToShow.index > 0\r\n const hasNext = canNavigate && photoToShow.index < historyLength - 1\r\n\r\n // Format date for display if showing a dated photo (full date)\r\n let photoTitle: { text: string; icon?: string } | undefined\r\n if (photoToShow.isHistoryPhoto && photoToShow.date) {\r\n const formattedDate = formatDate(photoToShow.date)\r\n photoTitle = { text: t('pets.photo_from_date', { date: formattedDate }), icon: 'Calendar' }\r\n }\r\n\r\n // Collect neighbor file IDs for preloading\r\n const preloadNeighborIds: string[] = []\r\n if (photoToShow.isHistoryPhoto && photoToShow.index >= 0) {\r\n if (photoToShow.index > 0) {\r\n preloadNeighborIds.push(sortedPhotoHistory[photoToShow.index - 1].file_id)\r\n }\r\n if (photoToShow.index < historyLength - 1) {\r\n preloadNeighborIds.push(sortedPhotoHistory[photoToShow.index + 1].file_id)\r\n }\r\n }\r\n\r\n sections.push({\r\n id: 'photo',\r\n type: 'image',\r\n title: photoTitle,\r\n image: {\r\n fileId: photoToShow.fileId,\r\n entityId: pet.id,\r\n entityType,\r\n keyType: pet.key_type || 'general',\r\n alt: pet.name,\r\n showDownload: false,\r\n // Progressive loading: show thumbnail first for instant display\r\n // Disabled in share mode since thumbnails may not be available\r\n progressiveLoad: !shareMode,\r\n // Known aspect ratio for stable layout before image loads\r\n aspectRatio: photoToShow.aspectRatio,\r\n // Preload neighbor thumbnails for instant navigation\r\n preloadNeighborIds: preloadNeighborIds.length > 0 ? preloadNeighborIds : undefined,\r\n // Navigation when viewing history photos with multiple entries\r\n hasPrev,\r\n hasNext,\r\n onPrev: hasPrev && onPhotoNavigate\r\n ? () => onPhotoNavigate(pet.id, 'prev')\r\n : undefined,\r\n onNext: hasNext && onPhotoNavigate\r\n ? () => onPhotoNavigate(pet.id, 'next')\r\n : undefined,\r\n prevLabel: t('pets.previous_photo'),\r\n nextLabel: t('pets.next_photo'),\r\n },\r\n })\r\n }\r\n\r\n // Photo gallery thumbnails (load thumbnails for fast display)\r\n // Show gallery when there are photos, or when onAddPhoto is provided (so user can add first photo)\r\n const MAX_PHOTOS = 12\r\n if (sortedPhotoHistory.length > 0 || onAddPhoto) {\r\n sections.push({\r\n id: 'photo-gallery',\r\n type: 'image-gallery',\r\n title: {\r\n text: t('pets.photo_count', { count: sortedPhotoHistory.length }),\r\n icon: 'Camera',\r\n },\r\n gallery: {\r\n items: sortedPhotoHistory.map((photo) => {\r\n const [year, month] = photo.date.split('-')\r\n const formattedDate = `${parseInt(month)}/${year.slice(-2)}`\r\n return {\r\n id: photo.file_id,\r\n fileId: photo.file_id,\r\n entityId: pet.id,\r\n entityType,\r\n keyType: pet.key_type || 'general',\r\n alt: pet.name,\r\n label: formattedDate,\r\n }\r\n }),\r\n onItemClick: onGalleryPhotoClick\r\n ? (_item, index) => {\r\n // Direct navigation by index - no date matching needed\r\n onGalleryPhotoClick(pet.id, index)\r\n }\r\n : undefined,\r\n onAddPhoto: onAddPhoto ? () => onAddPhoto(pet.id) : undefined,\r\n maxItems: MAX_PHOTOS,\r\n },\r\n })\r\n }\r\n\r\n // Details section\r\n const detailFields: any[] = []\r\n\r\n if (pet.color) {\r\n detailFields.push({\r\n label: t('pets.color'),\r\n value: pet.color,\r\n })\r\n }\r\n\r\n // Weight history chart (show even with 1 entry to educate users about tracking)\r\n const weightHistory = pet.weight_history || []\r\n if (weightHistory.length >= 1) {\r\n // Determine preferred unit based on majority of records\r\n const kgCount = weightHistory.filter(e => e.unit === 'kg').length\r\n const lbsCount = weightHistory.length - kgCount\r\n const preferKg = kgCount > lbsCount\r\n const displayUnit = preferKg ? 'kg' : 'lbs'\r\n\r\n // Only enable click if there are photos to show\r\n const photoHistory = pet.photo_history || []\r\n const hasPhotos = photoHistory.length > 0\r\n\r\n // Create photo markers for the chart\r\n const photoMarkers = photoHistory.map(photo => {\r\n const [year, month, day] = photo.date.split('-')\r\n const formattedDate = `${parseInt(month)}/${parseInt(day)}/${year.slice(-2)}`\r\n return {\r\n date: photo.date,\r\n label: `📷 ${formattedDate}`,\r\n }\r\n })\r\n\r\n sections.push({\r\n id: 'weight-chart',\r\n type: 'mini-line-chart',\r\n title: {\r\n text: t('pets.weight_history'),\r\n icon: 'TrendingUp',\r\n },\r\n chart: {\r\n dataPoints: weightHistory\r\n .map(entry => {\r\n // Format date as MM/DD/YY\r\n const [year, month, day] = entry.date.split('-')\r\n const formattedDate = `${parseInt(month)}/${parseInt(day)}/${year.slice(-2)}`\r\n // Convert to preferred unit\r\n let displayWeight: number\r\n if (preferKg) {\r\n displayWeight = entry.unit === 'lbs' ? entry.weight / 2.20462 : entry.weight\r\n } else {\r\n displayWeight = entry.unit === 'kg' ? entry.weight * 2.20462 : entry.weight\r\n }\r\n return {\r\n x: entry.date,\r\n y: displayWeight,\r\n label: `${formattedDate}: ${displayWeight.toFixed(1)} ${displayUnit}`,\r\n }\r\n })\r\n .sort((a, b) => a.x.localeCompare(b.x)),\r\n unit: displayUnit,\r\n color: 'pink',\r\n // Click handler to show closest photo (only if photos exist)\r\n onDataPointClick: hasPhotos && onWeightChartClick\r\n ? (dataPoint) => onWeightChartClick(pet, dataPoint.x)\r\n : undefined,\r\n // Photo markers on the timeline\r\n photoMarkers: hasPhotos ? photoMarkers : undefined,\r\n onPhotoMarkerClick: hasPhotos && onWeightChartClick\r\n ? (marker) => onWeightChartClick(pet, marker.date)\r\n : undefined,\r\n },\r\n })\r\n }\r\n\r\n if (pet.microchip_number) {\r\n detailFields.push({\r\n label: t('pets.microchip_number'),\r\n value: pet.microchip_number,\r\n format: 'monospace' as const,\r\n })\r\n }\r\n\r\n if (pet.license_number) {\r\n detailFields.push({\r\n label: t('pets.license_number'),\r\n value: pet.license_number,\r\n })\r\n }\r\n\r\n if (detailFields.length > 0) {\r\n sections.push({\r\n id: 'details',\r\n type: 'key-value-grid',\r\n fields: detailFields,\r\n })\r\n }\r\n\r\n // Notes section\r\n if (pet.notes) {\r\n sections.push({\r\n id: 'notes',\r\n type: 'text-block',\r\n border: 'top',\r\n title: {\r\n text: t('pets.notes'),\r\n },\r\n text: pet.notes,\r\n })\r\n }\r\n\r\n // Pet Tag QR Code - moved to children section for collapsed-by-default behavior\r\n }\r\n\r\n // Build children (important info, property, insurance, contacts)\r\n const childrenItems: ListItemData[] = []\r\n\r\n // Add important info as a grouped section (if any exist)\r\n const importantInfoItems = pet.important_information || []\r\n if (importantInfoItems.length > 0) {\r\n // Build children for the important info group\r\n const importantInfoChildren: ListItemData[] = importantInfoItems.map(info =>\r\n importantInfoAdapter(info, {\r\n t,\r\n navigate,\r\n entityType: entityType as any,\r\n parentEntityId: pet.id,\r\n // Edit/Delete open the important info dialog with this specific item\r\n onEdit: onOpenImportantInfoDialog ? () => onOpenImportantInfoDialog(pet, info.id) : undefined,\r\n onDelete: onOpenImportantInfoDialog ? () => onOpenImportantInfoDialog(pet, info.id) : undefined,\r\n })\r\n )\r\n\r\n // Create the grouped \"Important Information\" item\r\n childrenItems.push({\r\n id: `${pet.id}-important-info`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'AlertTriangle',\r\n bgColor: 'red',\r\n },\r\n title: t('importantInfo.title'),\r\n // Action badge with just \"Edit\" to open the dialog\r\n actionBadges: onOpenImportantInfoDialog ? [{\r\n id: 'edit-important-info',\r\n text: '',\r\n icon: 'MoreHorizontal',\r\n variant: 'outline',\r\n actions: [{\r\n id: 'edit',\r\n label: t('common.edit'),\r\n icon: 'Edit',\r\n onClick: () => onOpenImportantInfoDialog(pet),\r\n }],\r\n }] : undefined,\r\n children: {\r\n items: importantInfoChildren,\r\n },\r\n })\r\n }\r\n\r\n // Add property as a child card (where pet lives) - only in primary mode\r\n if (!isChild && petProperty) {\r\n const propertyActions: ListItemAction[] = [\r\n {\r\n id: 'view',\r\n label: t('common.view'),\r\n icon: 'Eye',\r\n onClick: () => navigate(`/property/${petProperty.id}`),\r\n },\r\n ]\r\n if (onRemoveProperty) {\r\n propertyActions.push({\r\n id: 'remove',\r\n label: t('common.remove'),\r\n icon: 'X',\r\n variant: 'destructive',\r\n onClick: () => onRemoveProperty(pet.id),\r\n })\r\n }\r\n childrenItems.push({\r\n id: `property-${petProperty.id}`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'Home',\r\n bgColor: 'blue',\r\n shape: 'square',\r\n size: 'sm',\r\n },\r\n title: petProperty.name,\r\n subtitle: {\r\n type: 'text',\r\n text: t('pets.lives_at'),\r\n },\r\n actionBadges: propertyActions.length > 0 ? [{\r\n id: 'actions',\r\n text: '',\r\n icon: 'MoreHorizontal',\r\n variant: 'outline',\r\n actions: propertyActions,\r\n }] : undefined,\r\n })\r\n }\r\n\r\n // Add insurance policies as child cards - only in primary mode\r\n if (!isChild) {\r\n petInsurance.forEach(policy => {\r\n childrenItems.push(insuranceAdapter(policy as InsurancePolicy & { entity_type?: string }, { ...context, renderMode: 'child' }))\r\n })\r\n }\r\n\r\n // Add contacts as child cards (they have rich data like phone/email)\r\n petContacts.forEach(({ contact, role }) => {\r\n childrenItems.push(contactAdapter(contact as Contact & { entity_type?: string }, { ...context, renderMode: 'child', role }))\r\n })\r\n\r\n // Add health records link - navigates to Health Records page filtered to this pet\r\n if (!isChild && petHealthRecords.length > 0) {\r\n childrenItems.push({\r\n id: `${pet.id}-health-records`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'HeartPulse',\r\n bgColor: 'pink',\r\n },\r\n title: t('healthRecords.title'),\r\n subtitle: {\r\n type: 'text',\r\n text: t('common.count_items', { count: petHealthRecords.length }),\r\n },\r\n onClick: () => navigate(`/pet?tab=health&pet=${pet.id}`),\r\n })\r\n }\r\n\r\n // Add Pet Tag child item\r\n if (!isChild) {\r\n if (pet.profile_url) {\r\n // Tag exists - show QR code in collapsible child (collapsed by default)\r\n const qrActions: ListItemAction[] = []\r\n\r\n if (onManageTag) {\r\n qrActions.push({\r\n id: 'manage',\r\n label: t('pets.manage_mode'),\r\n icon: 'Settings',\r\n onClick: () => onManageTag(pet),\r\n })\r\n }\r\n\r\n if (onCopyTag) {\r\n qrActions.push({\r\n id: 'copy',\r\n label: t('pets.copy_tag_url'),\r\n icon: 'Copy',\r\n onClick: () => onCopyTag(pet),\r\n })\r\n }\r\n\r\n if (onDeleteTag) {\r\n qrActions.push({\r\n id: 'delete',\r\n label: t('pets.delete_tag'),\r\n icon: 'Trash2',\r\n variant: 'destructive',\r\n onClick: () => onDeleteTag(pet),\r\n })\r\n }\r\n\r\n childrenItems.push({\r\n id: `${pet.id}-pet-tag`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'QrCode',\r\n bgColor: 'cyan',\r\n },\r\n title: t('pets.pet_tag'),\r\n subtitle: {\r\n type: 'text',\r\n text: t('pets.tag_created'),\r\n },\r\n // Collapsed by default - user can expand to see QR code\r\n isExpanded: false,\r\n actionBadges: qrActions.length > 0 ? [{\r\n id: 'actions',\r\n text: '',\r\n icon: 'MoreHorizontal',\r\n variant: 'outline',\r\n actions: qrActions,\r\n }] : undefined,\r\n // QR code shown when expanded\r\n sections: [{\r\n id: 'qr-code',\r\n type: 'qr-code',\r\n qrCode: {\r\n value: pet.profile_url,\r\n size: 160,\r\n showUrl: true,\r\n description: t('pets.tag_scan_description'),\r\n downloadFilename: `${pet.name.toLowerCase().replace(/\\s+/g, '-')}-tag-qr`,\r\n },\r\n }],\r\n })\r\n } else if (onCreateTag) {\r\n // No tag - show create button\r\n childrenItems.push({\r\n id: `${pet.id}-pet-tag`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'QrCode',\r\n bgColor: 'gray',\r\n },\r\n title: t('pets.pet_tag'),\r\n subtitle: {\r\n type: 'text',\r\n text: t('pets.tag_not_created'),\r\n },\r\n actionBadges: [{\r\n id: 'create-tag',\r\n text: t('pets.create_tag'),\r\n icon: 'Plus',\r\n variant: 'outline',\r\n onClick: () => onCreateTag(pet),\r\n }],\r\n })\r\n }\r\n }\r\n\r\n // Only show children section if there are items\r\n const children: ListItemData['children'] = childrenItems.length > 0 ? {\r\n title: t('common.related'),\r\n items: childrenItems,\r\n } : undefined\r\n\r\n // Use the most recent photo thumbnail as icon if available, otherwise use species icon\r\n const photoHistoryForIcon = pet.photo_history || []\r\n const sortedPhotosForIcon = [...photoHistoryForIcon].sort((a, b) => a.date.localeCompare(b.date))\r\n const iconPhoto = sortedPhotosForIcon.length > 0 ? sortedPhotosForIcon[sortedPhotosForIcon.length - 1] : null\r\n\r\n return {\r\n id: pet.id,\r\n renderMode,\r\n icon: iconPhoto ? {\r\n type: 'image',\r\n value: '', // Not used for encrypted images\r\n fileId: iconPhoto.file_id,\r\n entityId: pet.id,\r\n entityType,\r\n keyType: pet.key_type || 'general',\r\n shape: 'circle',\r\n } : {\r\n type: 'lucide-icon',\r\n value: getSpeciesIcon(pet.species),\r\n bgColor: getSpeciesColor(pet.species),\r\n shape: 'circle',\r\n },\r\n title: pet.memorialized_at ? `${pet.name} 🌈` : pet.name,\r\n subtitle: pet.date_of_birth ? {\r\n type: 'text',\r\n text: `Born ${formatDate(pet.date_of_birth)}`,\r\n } : undefined,\r\n badges,\r\n sections: sections.length > 0 ? sections : undefined,\r\n children,\r\n }\r\n}\r\n","/**\r\n * Vehicle Adapter\r\n *\r\n * Converts Vehicle entities to ListItemData for rendering.\r\n * Platform-agnostic - uses TranslateFunction instead of IntlShape.\r\n */\r\n\r\nimport type {\r\n Vehicle,\r\n Contact,\r\n InsurancePolicy,\r\n AccessCode,\r\n VehicleType,\r\n VehicleMaintenance,\r\n MaintenanceTask,\r\n FileReference,\r\n} from '@hearthcoo/types'\r\nimport { VEHICLE_TYPE_ICONS, VEHICLE_DOCUMENT_TYPE_ICONS } from '@hearthcoo/types'\r\nimport type {\r\n ListItemData,\r\n BaseAdapterContext,\r\n RenderMode,\r\n IconBgColor,\r\n} from '../types'\r\nimport { defaultFormatDate } from '../types'\r\nimport { contactAdapter } from './contactAdapter'\r\nimport { accessCodeAdapter } from './accessCodeAdapter'\r\nimport { insuranceAdapter } from './insuranceAdapter'\r\nimport { importantInfoAdapter } from './importantInfoAdapter'\r\nimport type { ComputedFields } from '../computedFields'\r\n\r\n/**\r\n * Color mapping for vehicle types\r\n */\r\nexport const VEHICLE_TYPE_COLORS: Record<VehicleType, IconBgColor> = {\r\n car: 'blue',\r\n truck: 'orange',\r\n suv: 'green',\r\n motorcycle: 'red',\r\n rv: 'purple',\r\n boat: 'blue',\r\n bicycle: 'green',\r\n other: 'gray',\r\n}\r\n\r\n/**\r\n * Helper to check if a policy covers an entity\r\n */\r\nconst policyCoversEntity = (policy: InsurancePolicy, entityId: string): boolean => {\r\n // Check new relationships system\r\n if (policy.relationships?.some(r => r.type === 'covers' && r.to === entityId)) {\r\n return true\r\n }\r\n // Fallback to legacy direct ID fields\r\n return policy.pet_id === entityId ||\r\n policy.property_id === entityId ||\r\n policy.vehicle_id === entityId\r\n}\r\n\r\nexport interface VehicleAdapterContext extends BaseAdapterContext {\r\n renderMode?: RenderMode\r\n insurancePolicies?: InsurancePolicy[]\r\n contacts?: Contact[]\r\n accessCodes?: AccessCode[]\r\n vehicleMaintenanceHistory?: VehicleMaintenance[]\r\n maintenanceTasks?: MaintenanceTask[]\r\n serviceInvoices?: { id: string; vehicle_id?: string; services?: any[] }[]\r\n // Generic callback for opening important info dialog\r\n onOpenImportantInfoDialog?: (vehicle: Vehicle, editInfoId?: string) => void\r\n // Generic callback for opening documents dialog\r\n onOpenDocumentsDialog?: (vehicle: Vehicle, editDocId?: string) => void\r\n // Callback to download a document file\r\n onDownloadDocument?: (vehicle: Vehicle, doc: FileReference) => void\r\n}\r\n\r\nconst getVehicleIcon = (vehicleType: VehicleType): string => {\r\n return VEHICLE_TYPE_ICONS[vehicleType] || 'Car'\r\n}\r\n\r\n/**\r\n * Get computed fields for a vehicle entity\r\n * Used by vehicleAdapter internally and can be used directly by MCP, etc.\r\n */\r\nexport function getVehicleComputedFields(vehicle: Vehicle): ComputedFields {\r\n // Build display name: \"2022 Toyota RAV4\" or \"My Car\"\r\n const parts = [vehicle.year, vehicle.make, vehicle.model].filter(Boolean)\r\n const displayName = parts.length > 0 ? parts.join(' ') : vehicle.name || 'Vehicle'\r\n\r\n const computed: ComputedFields = {\r\n displayName,\r\n }\r\n\r\n // registration_expiration is a month number (1-12), not a date\r\n // Calculate days until that month in the current/next year\r\n if (vehicle.registration_expiration) {\r\n const monthNum = parseInt(vehicle.registration_expiration, 10)\r\n if (monthNum >= 1 && monthNum <= 12) {\r\n const now = new Date()\r\n const currentYear = now.getFullYear()\r\n const currentMonth = now.getMonth() + 1 // 1-based\r\n\r\n // Determine target year\r\n let targetYear = currentYear\r\n if (monthNum < currentMonth) {\r\n // Month already passed this year, use next year\r\n targetYear = currentYear + 1\r\n }\r\n\r\n // End of the expiration month\r\n const expirationDate = new Date(targetYear, monthNum, 0) // Last day of month\r\n const daysUntil = Math.ceil((expirationDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))\r\n\r\n computed.daysUntilExpiry = daysUntil\r\n if (daysUntil < 0) {\r\n computed.expiryStatus = 'expired'\r\n } else if (daysUntil <= 30) {\r\n computed.expiryStatus = 'expiring_soon'\r\n } else {\r\n computed.expiryStatus = 'ok'\r\n }\r\n }\r\n }\r\n\r\n // Format vehicle type\r\n const vehicleType = vehicle.vehicle_type || vehicle.type\r\n if (vehicleType) {\r\n computed.formattedType = vehicleType.charAt(0).toUpperCase() + vehicleType.slice(1)\r\n }\r\n\r\n return computed\r\n}\r\n\r\nexport function vehicleAdapter(vehicle: Vehicle & { entity_type?: string }, context: VehicleAdapterContext): ListItemData {\r\n const {\r\n t,\r\n navigate,\r\n renderMode = 'primary',\r\n insurancePolicies = [],\r\n contacts = [],\r\n accessCodes = [],\r\n vehicleMaintenanceHistory = [],\r\n maintenanceTasks = [],\r\n serviceInvoices = [],\r\n onOpenImportantInfoDialog,\r\n } = context\r\n const entityType = vehicle.entity_type\r\n if (!entityType) {\r\n throw new Error('vehicleAdapter requires entity_type on the vehicle object')\r\n }\r\n const formatDate = context.formatDate || defaultFormatDate\r\n\r\n // Get the vehicle type (support both field names)\r\n const vehicleType = vehicle.vehicle_type || vehicle.type || 'car'\r\n\r\n // Find associated insurance (using new relationships system)\r\n const vehicleInsurance = insurancePolicies.filter(policy => policyCoversEntity(policy, vehicle.id))\r\n\r\n // Find associated access codes (e.g., bike lock codes)\r\n const vehicleAccessCodes = accessCodes.filter(ac =>\r\n (ac as any).linked_assets?.some((la: any) => la.asset_type === 'vehicle' && la.asset_id === vehicle.id)\r\n )\r\n\r\n // Filter maintenance history for this vehicle (legacy records not linked to invoices)\r\n const vehicleMaintenance = vehicleMaintenanceHistory.filter(m => m.vehicle_id === vehicle.id && !m.service_invoice_id)\r\n \r\n // Filter service invoices for this vehicle\r\n const vehicleInvoices = serviceInvoices.filter(inv => inv.vehicle_id === vehicle.id)\r\n \r\n // Total records count (legacy records + services from invoices)\r\n const totalServicesFromInvoices = vehicleInvoices.reduce((sum, inv) => sum + (inv.services?.length || 0), 0)\r\n const totalRecordsCount = vehicleMaintenance.length + totalServicesFromInvoices\r\n \r\n // Filter maintenance tasks for this vehicle\r\n const vehicleMaintenanceTasksList = maintenanceTasks.filter(t => t.vehicle_id === vehicle.id)\r\n\r\n // Find associated contacts from contact_relationships\r\n const vehicleContactRelationships = vehicle.contact_relationships || []\r\n const vehicleContacts = vehicleContactRelationships.map(rel => {\r\n const contact = contacts.find(c => c.id === rel.contact_id)\r\n return contact ? { contact, role: rel.role } : null\r\n }).filter((item): item is { contact: Contact; role: string } => item !== null)\r\n\r\n // Build badges\r\n const badges: ListItemData['badges'] = []\r\n\r\n // Type badge\r\n badges.push({\r\n id: 'type',\r\n text: t(`vehicles.type_${vehicleType}`),\r\n variant: 'outline',\r\n })\r\n\r\n // Make/Year badge\r\n if (vehicle.year && vehicle.make) {\r\n badges.push({\r\n id: 'make-year',\r\n text: `${vehicle.year} ${vehicle.make}`,\r\n variant: 'secondary',\r\n })\r\n }\r\n\r\n // Status badge\r\n if (vehicle.status) {\r\n badges.push({\r\n id: 'status',\r\n text: t(`vehicles.status_${vehicle.status}`),\r\n variant: vehicle.status === 'owned' ? 'default' : 'outline',\r\n })\r\n }\r\n\r\n // Documents badge - show count if there are attachments\r\n const documentCount = vehicle.documents?.length || 0\r\n if (documentCount > 0) {\r\n badges.push({\r\n id: 'documents',\r\n text: documentCount.toString(),\r\n icon: 'Paperclip',\r\n variant: 'outline',\r\n })\r\n }\r\n\r\n // Note: Insurance is shown as child cards below, not as a badge\r\n\r\n // Build sections (only for primary mode)\r\n const sections: ListItemData['sections'] = []\r\n\r\n if (renderMode === 'primary') {\r\n // Basic details section\r\n const detailFields: any[] = []\r\n\r\n if (vehicle.make) {\r\n detailFields.push({\r\n label: t('vehicles.make'),\r\n value: vehicle.make,\r\n })\r\n }\r\n\r\n if (vehicle.model) {\r\n detailFields.push({\r\n label: t('vehicles.model'),\r\n value: vehicle.model,\r\n })\r\n }\r\n\r\n if (vehicle.year) {\r\n detailFields.push({\r\n label: t('vehicles.year'),\r\n value: vehicle.year,\r\n })\r\n }\r\n\r\n if (vehicle.color) {\r\n detailFields.push({\r\n label: t('vehicles.color'),\r\n value: vehicle.color,\r\n })\r\n }\r\n\r\n if (vehicle.vin) {\r\n detailFields.push({\r\n label: t('vehicles.vin'),\r\n value: vehicle.vin,\r\n format: 'monospace' as const,\r\n })\r\n }\r\n\r\n if (vehicle.license_plate) {\r\n detailFields.push({\r\n label: t('vehicles.license_plate'),\r\n value: vehicle.license_plate,\r\n })\r\n }\r\n\r\n if (vehicle.registration_state) {\r\n detailFields.push({\r\n label: t('vehicles.registration_state'),\r\n value: vehicle.registration_state,\r\n })\r\n }\r\n\r\n if (vehicle.registration_expiration) {\r\n // registration_expiration is stored as month number (1-12)\r\n const monthNum = parseInt(vehicle.registration_expiration, 10)\r\n const monthNames = [\r\n t('common.month_january'),\r\n t('common.month_february'),\r\n t('common.month_march'),\r\n t('common.month_april'),\r\n t('common.month_may'),\r\n t('common.month_june'),\r\n t('common.month_july'),\r\n t('common.month_august'),\r\n t('common.month_september'),\r\n t('common.month_october'),\r\n t('common.month_november'),\r\n t('common.month_december'),\r\n ]\r\n const monthName = monthNum >= 1 && monthNum <= 12 ? monthNames[monthNum - 1] : vehicle.registration_expiration\r\n detailFields.push({\r\n label: t('vehicles.registration_expiration'),\r\n value: monthName,\r\n })\r\n }\r\n\r\n const mileage = vehicle.current_mileage || vehicle.mileage\r\n if (mileage) {\r\n detailFields.push({\r\n label: t('vehicles.mileage'),\r\n value: new Intl.NumberFormat().format(mileage),\r\n })\r\n }\r\n\r\n // Odometer history chart (show if 2+ entries)\r\n const odometerHistory = vehicle.odometer_history || []\r\n if (odometerHistory.length >= 2) {\r\n sections.push({\r\n id: 'odometer-chart',\r\n type: 'mini-line-chart',\r\n title: {\r\n text: t('vehicles.odometer_history'),\r\n icon: 'TrendingUp',\r\n },\r\n chart: {\r\n dataPoints: odometerHistory\r\n .map(entry => {\r\n // Format date as MM/DD/YY\r\n const [year, month, day] = entry.date.split('-')\r\n const formattedDate = `${parseInt(month)}/${parseInt(day)}/${year.slice(-2)}`\r\n // Format miles with commas\r\n const formattedMiles = entry.miles.toLocaleString()\r\n return {\r\n x: entry.date,\r\n y: entry.miles,\r\n label: `${formattedDate}: ${formattedMiles} mi`,\r\n }\r\n })\r\n .sort((a, b) => a.x.localeCompare(b.x)),\r\n unit: 'mi',\r\n color: 'blue',\r\n },\r\n })\r\n }\r\n\r\n if (detailFields.length > 0) {\r\n sections.push({\r\n id: 'details',\r\n type: 'key-value-grid',\r\n fields: detailFields,\r\n })\r\n }\r\n\r\n // Financial section\r\n const financialFields: any[] = []\r\n\r\n if (vehicle.purchase_date) {\r\n financialFields.push({\r\n label: t('vehicles.purchase_date'),\r\n value: formatDate(vehicle.purchase_date),\r\n })\r\n }\r\n\r\n if (vehicle.purchase_price) {\r\n financialFields.push({\r\n label: t('vehicles.purchase_price'),\r\n value: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(vehicle.purchase_price),\r\n format: 'currency' as const,\r\n })\r\n }\r\n\r\n if (vehicle.current_value) {\r\n financialFields.push({\r\n label: t('vehicles.current_value'),\r\n value: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(vehicle.current_value),\r\n format: 'currency' as const,\r\n })\r\n }\r\n\r\n if (financialFields.length > 0) {\r\n sections.push({\r\n id: 'financial',\r\n type: 'key-value-grid',\r\n border: 'top',\r\n fields: financialFields,\r\n })\r\n }\r\n\r\n // Notes section\r\n if (vehicle.notes) {\r\n sections.push({\r\n id: 'notes',\r\n type: 'text-block',\r\n border: 'top',\r\n title: {\r\n text: t('vehicles.notes'),\r\n },\r\n text: vehicle.notes,\r\n })\r\n }\r\n }\r\n\r\n // Build children (important info, access codes, insurance policies, contacts)\r\n const childrenItems: ListItemData[] = []\r\n\r\n // Add important info as a grouped section (if any exist)\r\n const importantInfoItems = vehicle.important_information || []\r\n if (importantInfoItems.length > 0) {\r\n // Build children for the important info group\r\n const importantInfoChildren: ListItemData[] = importantInfoItems.map(info =>\r\n importantInfoAdapter(info, {\r\n t,\r\n navigate,\r\n entityType: entityType as any,\r\n parentEntityId: vehicle.id,\r\n // Edit/Delete open the important info dialog with this specific item\r\n onEdit: onOpenImportantInfoDialog ? () => onOpenImportantInfoDialog(vehicle, info.id) : undefined,\r\n onDelete: onOpenImportantInfoDialog ? () => onOpenImportantInfoDialog(vehicle, info.id) : undefined,\r\n })\r\n )\r\n\r\n // Create the grouped \"Important Information\" item\r\n childrenItems.push({\r\n id: `${vehicle.id}-important-info`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'AlertTriangle',\r\n bgColor: 'red',\r\n },\r\n title: t('importantInfo.title'),\r\n // Action badge with just \"Edit\" to open the dialog\r\n actionBadges: onOpenImportantInfoDialog ? [{\r\n id: 'edit-important-info',\r\n text: '',\r\n icon: 'MoreHorizontal',\r\n variant: 'outline',\r\n actions: [{\r\n id: 'edit',\r\n label: t('common.edit'),\r\n icon: 'Edit',\r\n onClick: () => onOpenImportantInfoDialog(vehicle),\r\n }],\r\n }] : undefined,\r\n children: {\r\n items: importantInfoChildren,\r\n },\r\n })\r\n }\r\n\r\n // Add each document directly as a child item (with file rendering when expanded)\r\n const { onOpenDocumentsDialog, onDownloadDocument } = context\r\n const documents = vehicle.documents || []\r\n documents.forEach(doc => {\r\n if (!doc.file_id) return // Skip documents without files\r\n\r\n const docType = doc.type || 'other'\r\n const iconName = VEHICLE_DOCUMENT_TYPE_ICONS[docType as keyof typeof VEHICLE_DOCUMENT_TYPE_ICONS] || 'FileText'\r\n\r\n childrenItems.push({\r\n id: doc.id,\r\n renderMode: 'child' as RenderMode,\r\n icon: {\r\n type: 'lucide-icon' as const,\r\n value: iconName,\r\n bgColor: 'gray' as IconBgColor,\r\n shape: 'square' as const,\r\n size: 'sm' as const,\r\n },\r\n title: t(`vehicles.document_type_${docType}`),\r\n subtitle: doc.description ? {\r\n type: 'text' as const,\r\n text: doc.description,\r\n } : undefined,\r\n sections: [{\r\n id: 'document',\r\n type: 'document' as const,\r\n document: {\r\n fileId: doc.file_id,\r\n entityId: vehicle.id,\r\n entityType,\r\n fileName: doc.file_name || `${vehicle.name} - ${t(`vehicles.document_type_${docType}`)}.pdf`,\r\n downloadLabel: t('common.download'),\r\n },\r\n }],\r\n actionBadges: (onOpenDocumentsDialog || onDownloadDocument) ? [{\r\n id: 'actions',\r\n text: '',\r\n icon: 'MoreHorizontal',\r\n variant: 'outline' as const,\r\n actions: [\r\n ...(onDownloadDocument && doc.file_id ? [{\r\n id: 'download',\r\n label: t('common.download'),\r\n icon: 'Download',\r\n onClick: () => onDownloadDocument(vehicle, doc),\r\n }] : []),\r\n ...(onOpenDocumentsDialog ? [{\r\n id: 'edit',\r\n label: t('common.edit'),\r\n icon: 'Edit',\r\n onClick: () => onOpenDocumentsDialog(vehicle, doc.id),\r\n }] : []),\r\n ],\r\n }] : undefined,\r\n })\r\n })\r\n\r\n // Add access codes (e.g., bike lock codes)\r\n vehicleAccessCodes.forEach(accessCode => {\r\n childrenItems.push(accessCodeAdapter(accessCode as AccessCode & { entity_type?: string }, { t, navigate }))\r\n })\r\n\r\n // Add insurance policies\r\n vehicleInsurance.forEach(policy => {\r\n childrenItems.push(insuranceAdapter(policy as InsurancePolicy & { entity_type?: string }, { ...context, renderMode: 'child' }))\r\n })\r\n\r\n // Add contacts\r\n vehicleContacts.forEach(({ contact, role }) => {\r\n childrenItems.push(contactAdapter(contact as Contact & { entity_type?: string }, { ...context, renderMode: 'child', role }))\r\n })\r\n\r\n // Add maintenance tasks link - navigates to Maintenance tab filtered to this vehicle\r\n if (vehicleMaintenanceTasksList.length > 0) {\r\n childrenItems.push({\r\n id: `${vehicle.id}-maintenance-tasks`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'CalendarClock',\r\n bgColor: 'orange',\r\n },\r\n title: t('maintenance.tasks'),\r\n subtitle: {\r\n type: 'text',\r\n text: t('common.count_items', { count: vehicleMaintenanceTasksList.length }),\r\n },\r\n onClick: () => navigate(`/vehicle?tab=maintenance&vehicle=${vehicle.id}`),\r\n })\r\n }\r\n\r\n // Add records link - navigates to Records tab filtered to this vehicle\r\n if (totalRecordsCount > 0) {\r\n childrenItems.push({\r\n id: `${vehicle.id}-records`,\r\n renderMode: 'child',\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'FileText',\r\n bgColor: 'blue',\r\n },\r\n title: t('records.title'),\r\n subtitle: {\r\n type: 'text',\r\n text: t('common.count_items', { count: totalRecordsCount }),\r\n },\r\n onClick: () => navigate(`/vehicle?tab=records&vehicle=${vehicle.id}`),\r\n })\r\n }\r\n\r\n // Only show children section if there are items\r\n const children: ListItemData['children'] = childrenItems.length > 0 ? {\r\n title: t('common.related'),\r\n items: childrenItems,\r\n } : undefined\r\n\r\n return {\r\n id: vehicle.id,\r\n renderMode,\r\n icon: {\r\n type: 'lucide-icon',\r\n value: getVehicleIcon(vehicleType),\r\n bgColor: VEHICLE_TYPE_COLORS[vehicleType] || 'blue',\r\n shape: 'square',\r\n },\r\n title: vehicle.name,\r\n subtitle: vehicle.model ? {\r\n type: 'text',\r\n text: `${vehicle.make || ''} ${vehicle.model}`,\r\n } : vehicle.make ? {\r\n type: 'text',\r\n text: vehicle.make,\r\n } : undefined,\r\n badges,\r\n sections: sections.length > 0 ? sections : undefined,\r\n children,\r\n }\r\n}\r\n","[\n {\n \"id\": \"smoke-detector-maintenance\",\n \"name\": \"Smoke & CO Detector Maintenance\",\n \"description\": \"Monthly: Test all detectors. Annually: Replace batteries\",\n \"category\": \"safety\",\n \"applicableTo\": \"property\",\n \"conditions\": {},\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"monthly\"\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 15,\n \"whyImportant\": \"Life-saving devices must be functional - test monthly, replace batteries yearly\"\n },\n {\n \"id\": \"hvac-filter-replacement\",\n \"name\": \"Replace HVAC Air Filter\",\n \"description\": \"Change air filter in heating/cooling system to maintain efficiency\",\n \"category\": \"hvac\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasAC\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"monthly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 10,\n \"whyImportant\": \"Dirty filters reduce efficiency by up to 15% and worsen air quality\"\n },\n {\n \"id\": \"furnace-inspection\",\n \"name\": \"Furnace Inspection & Service\",\n \"description\": \"Professional inspection and cleaning of gas furnace\",\n \"category\": \"hvac\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasGasFurnace\": true\n },\n \"defaultFrequency\": {\n \"type\": \"seasonal\",\n \"season\": \"fall\",\n \"monthsNorthern\": [9],\n \"monthsSouthern\": [3]\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 90,\n \"whyImportant\": \"Ensures safe operation and prevents carbon monoxide leaks\"\n },\n {\n \"id\": \"ac-service\",\n \"name\": \"Air Conditioning Service\",\n \"description\": \"Professional AC inspection and cleaning\",\n \"category\": \"hvac\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasAC\": true\n },\n \"defaultFrequency\": {\n \"type\": \"seasonal\",\n \"season\": \"spring\",\n \"monthsNorthern\": [4, 5],\n \"monthsSouthern\": [10, 11]\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 90,\n \"whyImportant\": \"Maintains efficiency and prevents breakdowns in summer heat\"\n },\n {\n \"id\": \"gutter-cleaning-fall\",\n \"name\": \"Clean Gutters (Fall)\",\n \"description\": \"Remove leaves and debris from gutters and downspouts\",\n \"category\": \"exterior\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasGutters\": true,\n \"propertyType\": [\"single_family\", \"townhouse\"]\n },\n \"defaultFrequency\": {\n \"type\": \"seasonal\",\n \"season\": \"fall\",\n \"monthsNorthern\": [10, 11],\n \"monthsSouthern\": [4, 5]\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 120,\n \"whyImportant\": \"Clogged gutters can cause water damage to foundation and roof\"\n },\n {\n \"id\": \"gutter-cleaning-spring\",\n \"name\": \"Clean Gutters (Spring)\",\n \"description\": \"Remove debris accumulated over winter\",\n \"category\": \"exterior\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasGutters\": true,\n \"propertyType\": [\"single_family\", \"townhouse\"]\n },\n \"defaultFrequency\": {\n \"type\": \"seasonal\",\n \"season\": \"spring\",\n \"monthsNorthern\": [4, 5],\n \"monthsSouthern\": [10, 11]\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 120,\n \"whyImportant\": \"Spring runoff needs clear gutters to prevent water damage\"\n },\n {\n \"id\": \"roof-moss-removal\",\n \"name\": \"Remove Roof Moss\",\n \"description\": \"Clean moss and debris from roof shingles\",\n \"category\": \"roof\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"propertyType\": [\"single_family\", \"townhouse\"],\n \"climate\": [\"marine\", \"humid_subtropical\", \"humid_continental\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 240,\n \"whyImportant\": \"Moss damages shingles and reduces roof lifespan significantly\"\n },\n {\n \"id\": \"roof-inspection\",\n \"name\": \"Inspect Roof\",\n \"description\": \"Check for damaged shingles, leaks, and wear\",\n \"category\": \"roof\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"propertyType\": [\"single_family\", \"townhouse\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 60,\n \"whyImportant\": \"Early detection of roof damage prevents expensive interior water damage\"\n },\n {\n \"id\": \"pool-chemistry-check\",\n \"name\": \"Check Pool Chemistry\",\n \"description\": \"Test and balance pH, chlorine, and alkalinity levels\",\n \"category\": \"pool\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasPool\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"weekly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 30,\n \"whyImportant\": \"Prevents algae growth and keeps water safe for swimming\"\n },\n {\n \"id\": \"pool-filter-cleaning\",\n \"name\": \"Clean Pool Filter\",\n \"description\": \"Backwash or clean pool filter system\",\n \"category\": \"pool\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasPool\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"monthly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 45,\n \"whyImportant\": \"Keeps water circulation efficient and clear\"\n },\n {\n \"id\": \"pool-winterization\",\n \"name\": \"Winterize Pool\",\n \"description\": \"Close pool for winter: drain, add antifreeze, install cover\",\n \"category\": \"pool\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasPool\": true,\n \"winterSeverity\": [\"moderate\", \"severe\"]\n },\n \"defaultFrequency\": {\n \"type\": \"seasonal\",\n \"season\": \"fall\",\n \"monthsNorthern\": [10],\n \"monthsSouthern\": [4]\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 180,\n \"whyImportant\": \"Prevents freeze damage to pool equipment and plumbing\"\n },\n {\n \"id\": \"pool-opening\",\n \"name\": \"Open Pool for Summer\",\n \"description\": \"Remove cover, refill, balance chemicals, start equipment\",\n \"category\": \"pool\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasPool\": true,\n \"winterSeverity\": [\"moderate\", \"severe\"]\n },\n \"defaultFrequency\": {\n \"type\": \"seasonal\",\n \"season\": \"spring\",\n \"monthsNorthern\": [4, 5],\n \"monthsSouthern\": [10, 11]\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 180,\n \"whyImportant\": \"Proper opening prevents damage and ensures safe swimming\"\n },\n {\n \"id\": \"sprinkler-system-blowout\",\n \"name\": \"Blow Out Sprinkler System\",\n \"description\": \"Clear water from sprinkler lines to prevent freeze damage\",\n \"category\": \"landscaping\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasSprinklerSystem\": true,\n \"winterSeverity\": [\"moderate\", \"severe\"]\n },\n \"defaultFrequency\": {\n \"type\": \"seasonal\",\n \"season\": \"fall\",\n \"monthsNorthern\": [10],\n \"monthsSouthern\": [4]\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 60,\n \"whyImportant\": \"Frozen water in lines can cause $1000+ in repairs\"\n },\n {\n \"id\": \"sprinkler-system-startup\",\n \"name\": \"Start Up Sprinkler System\",\n \"description\": \"Turn on system, check for leaks, adjust spray patterns\",\n \"category\": \"landscaping\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasSprinklerSystem\": true,\n \"winterSeverity\": [\"moderate\", \"severe\"]\n },\n \"defaultFrequency\": {\n \"type\": \"seasonal\",\n \"season\": \"spring\",\n \"monthsNorthern\": [4, 5],\n \"monthsSouthern\": [10, 11]\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 90,\n \"whyImportant\": \"Catch leaks early before they waste water and money\"\n },\n {\n \"id\": \"chimney-cleaning-inspection\",\n \"name\": \"Chimney Cleaning & Inspection\",\n \"description\": \"Remove creosote buildup and inspect for damage\",\n \"category\": \"safety\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasFireplace\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 120,\n \"whyImportant\": \"Prevents chimney fires caused by creosote buildup\"\n },\n {\n \"id\": \"septic-tank-pumping\",\n \"name\": \"Pump Septic Tank\",\n \"description\": \"Professional septic tank pumping and inspection\",\n \"category\": \"plumbing\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"propertyType\": [\"single_family\"],\n \"hasSepticSystem\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"customMonths\": 36\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 120,\n \"whyImportant\": \"Prevents backup and expensive drain field replacement ($10,000+)\"\n },\n {\n \"id\": \"water-heater-flush\",\n \"name\": \"Flush Water Heater\",\n \"description\": \"Drain sediment from water heater tank\",\n \"category\": \"plumbing\",\n \"applicableTo\": \"property\",\n \"conditions\": {},\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 60,\n \"whyImportant\": \"Removes sediment buildup that reduces efficiency and lifespan\"\n },\n {\n \"id\": \"deck-staining\",\n \"name\": \"Stain/Seal Deck\",\n \"description\": \"Apply protective stain or sealant to wood deck\",\n \"category\": \"exterior\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasDeck\": true,\n \"deckMaterial\": [\"wood\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"customMonths\": 24\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 480,\n \"whyImportant\": \"Protects wood from rot and extends deck life by years\"\n },\n {\n \"id\": \"driveway-sealing\",\n \"name\": \"Seal Asphalt Driveway\",\n \"description\": \"Apply sealcoat to protect asphalt surface\",\n \"category\": \"exterior\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasDriveway\": true,\n \"drivewayMaterial\": [\"asphalt\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"customMonths\": 24\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 360,\n \"whyImportant\": \"Prevents cracks and extends driveway life significantly\"\n },\n {\n \"id\": \"power-wash-exterior\",\n \"name\": \"Power Wash Home Exterior\",\n \"description\": \"Clean siding, walkways, and deck to remove dirt and mildew\",\n \"category\": \"exterior\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"propertyType\": [\"single_family\", \"townhouse\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 240,\n \"whyImportant\": \"Prevents mildew damage and maintains curb appeal\"\n },\n {\n \"id\": \"window-caulking\",\n \"name\": \"Inspect & Recaulk Windows\",\n \"description\": \"Check window caulking and reapply where needed\",\n \"category\": \"exterior\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"propertyType\": [\"single_family\", \"townhouse\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"customMonths\": 24\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 180,\n \"whyImportant\": \"Prevents water infiltration and improves energy efficiency\"\n },\n {\n \"id\": \"dryer-vent-cleaning\",\n \"name\": \"Clean Dryer Vent\",\n \"description\": \"Remove lint buildup from dryer vent ductwork\",\n \"category\": \"appliances\",\n \"applicableTo\": \"property\",\n \"conditions\": {},\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 45,\n \"whyImportant\": \"Prevents fire hazard and improves dryer efficiency\"\n },\n {\n \"id\": \"refrigerator-coil-cleaning\",\n \"name\": \"Clean Refrigerator Coils\",\n \"description\": \"Vacuum dust from refrigerator condenser coils\",\n \"category\": \"appliances\",\n \"applicableTo\": \"property\",\n \"conditions\": {},\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 30,\n \"whyImportant\": \"Improves efficiency and extends refrigerator lifespan\"\n },\n {\n \"id\": \"lawn-mowing\",\n \"name\": \"Mow Lawn\",\n \"description\": \"Cut grass to maintain healthy lawn appearance\",\n \"category\": \"landscaping\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"propertyType\": [\"single_family\", \"townhouse\"],\n \"lawnSize\": [\"small\", \"medium\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"weekly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 60,\n \"whyImportant\": \"Keeps lawn healthy and property looking well-maintained\"\n },\n {\n \"id\": \"lawn-fertilizing-spring\",\n \"name\": \"Fertilize Lawn (Spring)\",\n \"description\": \"Apply spring fertilizer to promote growth\",\n \"category\": \"landscaping\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"propertyType\": [\"single_family\", \"townhouse\"],\n \"lawnSize\": [\"small\", \"medium\"]\n },\n \"defaultFrequency\": {\n \"type\": \"seasonal\",\n \"season\": \"spring\",\n \"monthsNorthern\": [4, 5],\n \"monthsSouthern\": [10, 11]\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 90,\n \"whyImportant\": \"Promotes healthy growth and green color\"\n },\n {\n \"id\": \"lawn-fertilizing-fall\",\n \"name\": \"Fertilize Lawn (Fall)\",\n \"description\": \"Apply fall fertilizer to strengthen roots\",\n \"category\": \"landscaping\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"propertyType\": [\"single_family\", \"townhouse\"],\n \"lawnSize\": [\"small\", \"medium\"]\n },\n \"defaultFrequency\": {\n \"type\": \"seasonal\",\n \"season\": \"fall\",\n \"monthsNorthern\": [9, 10],\n \"monthsSouthern\": [3, 4]\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 90,\n \"whyImportant\": \"Strengthens roots for winter and early spring growth\"\n },\n {\n \"id\": \"lawn-aeration\",\n \"name\": \"Aerate Lawn\",\n \"description\": \"Core aerate lawn to improve water and nutrient absorption\",\n \"category\": \"landscaping\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"propertyType\": [\"single_family\", \"townhouse\"],\n \"lawnSize\": [\"medium\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 120,\n \"whyImportant\": \"Reduces soil compaction and promotes healthier grass\"\n },\n {\n \"id\": \"tree-trimming\",\n \"name\": \"Trim Trees & Shrubs\",\n \"description\": \"Prune dead branches and shape trees/shrubs\",\n \"category\": \"landscaping\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"propertyType\": [\"single_family\", \"townhouse\"],\n \"treeCount\": [\"few\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 180,\n \"whyImportant\": \"Prevents damage from falling branches and promotes healthy growth\"\n },\n {\n \"id\": \"well-pump-testing\",\n \"name\": \"Test Well Pump\",\n \"description\": \"Check well pump operation and water quality\",\n \"category\": \"plumbing\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasWell\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 90,\n \"whyImportant\": \"Catches pump issues early before complete failure\"\n },\n {\n \"id\": \"basement-sump-pump-test\",\n \"name\": \"Test Sump Pump\",\n \"description\": \"Pour water into sump pit to ensure pump activates properly\",\n \"category\": \"plumbing\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasBasement\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"quarterly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 15,\n \"whyImportant\": \"Prevents basement flooding during heavy rain\"\n },\n {\n \"id\": \"solar-panel-cleaning\",\n \"name\": \"Clean Solar Panels\",\n \"description\": \"Remove dirt, pollen, bird droppings, and debris from solar panels\",\n \"category\": \"solar\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasSolarPanels\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 60,\n \"whyImportant\": \"Dirty panels can reduce energy output by 20-25%\"\n },\n {\n \"id\": \"solar-panel-visual-inspection\",\n \"name\": \"Inspect Solar Panels\",\n \"description\": \"Check panels for cracks, hotspots, discoloration, loose connections, and debris buildup\",\n \"category\": \"solar\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasSolarPanels\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 30,\n \"whyImportant\": \"Early detection of damage prevents costly repairs and output loss\"\n },\n {\n \"id\": \"solar-inverter-inspection\",\n \"name\": \"Check Solar Inverter\",\n \"description\": \"Verify inverter is functioning properly, check status lights and error codes\",\n \"category\": \"solar\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasSolarPanels\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"monthly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 10,\n \"whyImportant\": \"Inverter failure stops all energy production - catch issues early\"\n },\n {\n \"id\": \"solar-system-professional-inspection\",\n \"name\": \"Professional Solar System Inspection\",\n \"description\": \"Full system inspection including wiring, connections, mounting, and performance analysis\",\n \"category\": \"solar\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasSolarPanels\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"customMonths\": 24\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 120,\n \"whyImportant\": \"Ensures system operates at peak efficiency and maintains warranty\"\n },\n {\n \"id\": \"home-battery-visual-inspection\",\n \"name\": \"Inspect Home Battery System\",\n \"description\": \"Check battery unit for damage, leaks, unusual sounds, and ensure ventilation is clear\",\n \"category\": \"solar\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasHomeBattery\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"quarterly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 15,\n \"whyImportant\": \"Early detection of battery issues prevents safety hazards and costly damage\"\n },\n {\n \"id\": \"home-battery-firmware-update\",\n \"name\": \"Check Battery Firmware Updates\",\n \"description\": \"Check manufacturer app for firmware updates and apply if available\",\n \"category\": \"solar\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasHomeBattery\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"quarterly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 15,\n \"whyImportant\": \"Updates improve performance, fix bugs, and enhance safety features\"\n },\n {\n \"id\": \"home-battery-professional-inspection\",\n \"name\": \"Professional Battery System Inspection\",\n \"description\": \"Full inspection of battery connections, performance testing, and state of health analysis\",\n \"category\": \"solar\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasHomeBattery\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 90,\n \"whyImportant\": \"Ensures safe operation and optimal battery health for longevity\"\n },\n {\n \"id\": \"window-cleaning\",\n \"name\": \"Clean Windows\",\n \"description\": \"Clean interior and exterior windows for improved visibility and appearance\",\n \"category\": \"interior\",\n \"applicableTo\": \"property\",\n \"conditions\": {},\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 180,\n \"whyImportant\": \"Clean windows improve natural light and curb appeal\"\n },\n {\n \"id\": \"carpet-cleaning\",\n \"name\": \"Carpet Cleaning\",\n \"description\": \"Deep clean carpets to remove dirt, stains, and allergens\",\n \"category\": \"interior\",\n \"applicableTo\": \"property\",\n \"conditions\": {\n \"hasCarpets\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 240,\n \"whyImportant\": \"Extends carpet life, improves air quality, and removes allergens\"\n }\n]\n","[\n {\n \"id\": \"oil-change\",\n \"name\": \"Oil Change\",\n \"description\": \"Change engine oil and oil filter\",\n \"category\": \"engine\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\", \"rv\"],\n \"engineType\": [\"gasoline\", \"diesel\", \"hybrid\", \"plugin_hybrid\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"quarterly\"\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 45,\n \"whyImportant\": \"Regular oil changes prevent engine wear and extend vehicle life. Most critical maintenance task.\"\n },\n {\n \"id\": \"tire-rotation\",\n \"name\": \"Tire Rotation\",\n \"description\": \"Rotate tires to ensure even wear\",\n \"category\": \"tires\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\", \"rv\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 60,\n \"whyImportant\": \"Even tire wear extends tire life by up to 20% and improves safety and fuel efficiency.\"\n },\n {\n \"id\": \"brake-inspection\",\n \"name\": \"Brake Inspection\",\n \"description\": \"Inspect brake pads, rotors, and fluid\",\n \"category\": \"brakes\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\", \"motorcycle\", \"rv\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 45,\n \"whyImportant\": \"Brake failure is life-threatening. Regular inspection prevents accidents and costly repairs.\"\n },\n {\n \"id\": \"air-filter-replacement\",\n \"name\": \"Replace Engine Air Filter\",\n \"description\": \"Replace engine air filter for optimal performance\",\n \"category\": \"engine\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\", \"rv\"],\n \"engineType\": [\"gasoline\", \"diesel\", \"hybrid\", \"plugin_hybrid\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 15,\n \"whyImportant\": \"Clean air filter improves fuel economy by up to 10% and engine performance.\"\n },\n {\n \"id\": \"cabin-air-filter\",\n \"name\": \"Replace Cabin Air Filter\",\n \"description\": \"Replace cabin air filter for clean interior air\",\n \"category\": \"interior\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\", \"rv\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 10,\n \"whyImportant\": \"Filters out pollen, dust, and pollutants for healthier cabin air.\"\n },\n {\n \"id\": \"battery-check\",\n \"name\": \"Battery Test\",\n \"description\": \"Test battery voltage and terminals, clean corrosion\",\n \"category\": \"electrical\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\", \"rv\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 20,\n \"whyImportant\": \"Prevents unexpected breakdowns. Most batteries last 3-5 years.\"\n },\n {\n \"id\": \"coolant-flush\",\n \"name\": \"Coolant Flush\",\n \"description\": \"Drain and replace engine coolant/antifreeze\",\n \"category\": \"engine\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\", \"rv\"],\n \"engineType\": [\"gasoline\", \"diesel\", \"hybrid\", \"plugin_hybrid\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 90,\n \"whyImportant\": \"Prevents engine overheating and corrosion. Critical for engine longevity.\"\n },\n {\n \"id\": \"transmission-service\",\n \"name\": \"Transmission Service\",\n \"description\": \"Change transmission fluid and filter\",\n \"category\": \"drivetrain\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\", \"rv\"],\n \"transmissionType\": [\"automatic\", \"cvt\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 120,\n \"whyImportant\": \"Extends transmission life. Transmission replacement can cost $3,000-$8,000.\"\n },\n {\n \"id\": \"spark-plugs\",\n \"name\": \"Replace Spark Plugs\",\n \"description\": \"Replace spark plugs for optimal ignition\",\n \"category\": \"engine\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\", \"rv\"],\n \"engineType\": [\"gasoline\", \"hybrid\", \"plugin_hybrid\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 60,\n \"whyImportant\": \"Improves fuel efficiency, reduces emissions, and ensures smooth engine operation.\"\n },\n {\n \"id\": \"wiper-blades\",\n \"name\": \"Replace Wiper Blades\",\n \"description\": \"Replace windshield wiper blades\",\n \"category\": \"exterior\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\", \"rv\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 10,\n \"whyImportant\": \"Essential for visibility and safety in rain and snow.\"\n },\n {\n \"id\": \"wheel-alignment\",\n \"name\": \"Wheel Alignment\",\n \"description\": \"Check and adjust wheel alignment\",\n \"category\": \"tires\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 60,\n \"whyImportant\": \"Prevents uneven tire wear and improves handling and fuel economy.\"\n },\n {\n \"id\": \"timing-belt\",\n \"name\": \"Timing Belt Replacement\",\n \"description\": \"Replace timing belt (if applicable)\",\n \"category\": \"engine\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\"],\n \"engineType\": [\"gasoline\", \"diesel\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 240,\n \"whyImportant\": \"Timing belt failure can cause catastrophic engine damage. Check your owner's manual for mileage interval (typically 60k-100k miles).\"\n },\n {\n \"id\": \"differential-service\",\n \"name\": \"Differential Service\",\n \"description\": \"Change differential fluid\",\n \"category\": \"drivetrain\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\"],\n \"driveType\": [\"awd\", \"4wd\", \"rwd\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"routine\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 90,\n \"whyImportant\": \"Maintains drivetrain efficiency and prevents costly differential failure.\"\n },\n {\n \"id\": \"motorcycle-chain-maintenance\",\n \"name\": \"Chain Cleaning & Lubrication\",\n \"description\": \"Clean and lubricate motorcycle chain\",\n \"category\": \"drivetrain\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"motorcycle\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"monthly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 30,\n \"whyImportant\": \"Prevents chain wear and ensures safe operation. Chain failure can be dangerous.\"\n },\n {\n \"id\": \"bicycle-tune-up\",\n \"name\": \"Bicycle Tune-Up\",\n \"description\": \"Check brakes, gears, tire pressure, chain lubrication\",\n \"category\": \"general\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"bicycle\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"quarterly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 45,\n \"whyImportant\": \"Ensures safe and efficient riding. Prevents accidents and costly repairs.\"\n },\n {\n \"id\": \"ev-battery-check\",\n \"name\": \"EV Battery Health Check\",\n \"description\": \"Professional battery health diagnostic\",\n \"category\": \"electrical\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\"],\n \"engineType\": [\"electric\", \"plugin_hybrid\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 45,\n \"whyImportant\": \"Monitors battery degradation and ensures warranty compliance.\"\n },\n {\n \"id\": \"winter-tire-swap\",\n \"name\": \"Winter Tire Installation\",\n \"description\": \"Swap to winter tires for cold weather\",\n \"category\": \"tires\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"car\"],\n \"hasWinterTires\": true\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 90,\n \"whyImportant\": \"Winter tires dramatically improve traction and safety in snow and ice.\"\n },\n {\n \"id\": \"boat-engine-oil\",\n \"name\": \"Boat Engine Oil Change\",\n \"description\": \"Change engine oil and filter for boat motor\",\n \"category\": \"engine\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"boat\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 60,\n \"whyImportant\": \"Marine engines operate in harsh conditions. Regular oil changes prevent corrosion and extend engine life.\"\n },\n {\n \"id\": \"boat-hull-inspection\",\n \"name\": \"Hull Inspection & Cleaning\",\n \"description\": \"Inspect hull for damage, clean barnacles and growth\",\n \"category\": \"exterior\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"boat\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 180,\n \"whyImportant\": \"Hull growth reduces fuel efficiency and speed. Damage can lead to costly repairs or sinking.\"\n },\n {\n \"id\": \"boat-winterization\",\n \"name\": \"Winterization\",\n \"description\": \"Prepare boat for winter storage: drain water, stabilize fuel, protect engine\",\n \"category\": \"general\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"boat\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"professional\",\n \"estimatedTimeMinutes\": 240,\n \"whyImportant\": \"Prevents freeze damage to engine and systems. Skipping winterization can cause thousands in repairs.\"\n },\n {\n \"id\": \"boat-battery-maintenance\",\n \"name\": \"Battery Check & Charging\",\n \"description\": \"Test battery, check terminals, maintain charge\",\n \"category\": \"electrical\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"boat\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"quarterly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 30,\n \"whyImportant\": \"Dead battery leaves you stranded on water. Marine batteries need regular charging and maintenance.\"\n },\n {\n \"id\": \"boat-safety-equipment\",\n \"name\": \"Safety Equipment Inspection\",\n \"description\": \"Check life jackets, fire extinguishers, flares, first aid kit\",\n \"category\": \"safety\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"boat\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 45,\n \"whyImportant\": \"Required by law and essential for safety. Expired flares and damaged life jackets can be life-threatening.\"\n },\n {\n \"id\": \"boat-bilge-pump\",\n \"name\": \"Bilge Pump Test\",\n \"description\": \"Test bilge pump operation and clean intake\",\n \"category\": \"safety\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"boat\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"quarterly\"\n },\n \"priority\": \"critical\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 20,\n \"whyImportant\": \"Bilge pump failure can lead to sinking. Regular testing ensures it works when needed.\"\n },\n {\n \"id\": \"boat-propeller-inspection\",\n \"name\": \"Propeller Inspection\",\n \"description\": \"Check propeller for damage, dings, fishing line\",\n \"category\": \"drivetrain\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"boat\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"biannually\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_easy\",\n \"estimatedTimeMinutes\": 30,\n \"whyImportant\": \"Damaged props reduce performance and fuel efficiency. Can damage transmission if left unchecked.\"\n },\n {\n \"id\": \"boat-fuel-system\",\n \"name\": \"Fuel System Maintenance\",\n \"description\": \"Add fuel stabilizer, check fuel lines and water separator\",\n \"category\": \"engine\",\n \"applicableTo\": \"vehicle\",\n \"conditions\": {\n \"vehicleType\": [\"boat\"]\n },\n \"defaultFrequency\": {\n \"type\": \"recurring\",\n \"interval\": \"yearly\"\n },\n \"priority\": \"important\",\n \"skillLevel\": \"diy_moderate\",\n \"estimatedTimeMinutes\": 60,\n \"whyImportant\": \"Marine fuel degrades quickly. Prevents engine damage from bad fuel and water contamination.\"\n }\n]\n","/**\r\n * Maintenance Task Adapter\r\n *\r\n * Converts MaintenanceTask entities to ListItemData for rendering.\r\n * Platform-agnostic - uses TranslateFunction instead of IntlShape.\r\n */\r\n\r\nimport type {\r\n MaintenanceTask,\r\n Contact,\r\n MaintenanceCategory,\r\n RecurrenceFrequency,\r\n} from '@hearthcoo/types'\r\nimport { MAINTENANCE_CATEGORY_ICONS, getFrequencyLabel, getContactDisplayName } from '@hearthcoo/types'\r\nimport type {\r\n ListItemData,\r\n ListItemActionBadge,\r\n BaseAdapterContext,\r\n IconBgColor,\r\n} from '../types'\r\nimport { defaultFormatDate } from '../types'\r\nimport { contactAdapter } from './contactAdapter'\r\nimport propertyTemplates from '../data/propertyMaintenanceTemplates.json'\r\nimport vehicleTemplates from '../data/vehicleMaintenanceTemplates.json'\r\n\r\n// Combined templates for internal lookup\r\nconst ALL_MAINTENANCE_TEMPLATES = [\r\n ...propertyTemplates,\r\n ...vehicleTemplates,\r\n] as MaintenanceTemplate[]\r\n\r\ninterface Resident {\r\n id: string\r\n name: string\r\n [key: string]: any\r\n}\r\n\r\ninterface MaintenanceTemplate {\r\n id: string\r\n name: string\r\n description?: string\r\n category?: string\r\n priority?: string\r\n}\r\n\r\nexport interface MaintenanceAdapterContext extends BaseAdapterContext {\r\n propertyName?: string\r\n assignedContact?: Contact\r\n assignedResident?: Resident\r\n linkedServiceName?: string\r\n completedByName?: string\r\n templates?: MaintenanceTemplate[]\r\n /** Callback when user clicks \"Complete Today\" on overdue/due-soon badge */\r\n onCompleteToday?: () => void\r\n /** Callback when user selects a date from the date picker */\r\n onCompleteOn?: (date: string) => void\r\n}\r\n\r\nconst getCategoryIcon = (category: string): string => {\r\n return MAINTENANCE_CATEGORY_ICONS[category as MaintenanceCategory] || 'Wrench'\r\n}\r\n\r\nconst getPriorityColor = (priority: string): IconBgColor => {\r\n switch (priority) {\r\n case 'critical': return 'red'\r\n case 'important': return 'orange'\r\n case 'routine': return 'blue'\r\n default: return 'gray'\r\n }\r\n}\r\n\r\nexport function maintenanceAdapter(\r\n task: MaintenanceTask,\r\n context: MaintenanceAdapterContext\r\n): ListItemData {\r\n const {\r\n t,\r\n navigate,\r\n assignedContact,\r\n assignedResident,\r\n linkedServiceName,\r\n completedByName,\r\n templates = [],\r\n onCompleteToday,\r\n onCompleteOn,\r\n } = context\r\n const formatDate = context.formatDate || defaultFormatDate\r\n\r\n // Get the assignee name (either contact or resident)\r\n const assignedToContactName = assignedContact\r\n ? getContactDisplayName(assignedContact, 'Unknown Contact')\r\n : undefined\r\n\r\n const assignedToResidentName = assignedResident?.name\r\n const assigneeName = assignedToContactName || assignedToResidentName\r\n\r\n // Look up template if task has template_id (use internal templates as fallback)\r\n const taskAny = task as any\r\n const templateId = taskAny.template_id\r\n const templateSource = templates.length > 0 ? templates : ALL_MAINTENANCE_TEMPLATES\r\n const template = templateId ? templateSource.find(t => t.id === templateId) : null\r\n\r\n // Merge task fields with template (task takes precedence)\r\n const name = task.name || template?.name || 'Unnamed Task'\r\n const category = task.category || template?.category || 'other'\r\n const priority = task.priority || (template?.priority as any) || 'routine'\r\n\r\n const priorityLabel = priority.charAt(0).toUpperCase() + priority.slice(1)\r\n const categoryLabel = category.replace('_', ' ').replace(/\\b\\w/g, l => l.toUpperCase())\r\n\r\n // Handle frequency safely (might be undefined for old tasks)\r\n const frequencyText = task.frequency ? getFrequencyLabel(task.frequency as RecurrenceFrequency) : 'Not Set'\r\n\r\n // Support both field names (next_due_date from context, next_due_at from types)\r\n const nextDueDate = (task as any).next_due_date || task.next_due_at\r\n\r\n // Build subtitle with next due date - last completed shows in expanded view\r\n const subtitleParts: string[] = []\r\n if (nextDueDate) {\r\n subtitleParts.push(`${t('maintenance.next_due')}: ${formatDate(nextDueDate)}`)\r\n }\r\n\r\n // Use notes if no due date\r\n const subtitleText = subtitleParts.length > 0\r\n ? subtitleParts.join(' • ')\r\n : task.notes || ''\r\n\r\n // Base data with priority-colored icon\r\n const data: ListItemData = {\r\n id: task.id,\r\n icon: {\r\n type: 'lucide-icon',\r\n value: getCategoryIcon(category),\r\n bgColor: getPriorityColor(priority),\r\n },\r\n title: name,\r\n subtitle: subtitleText ? {\r\n type: 'text',\r\n text: subtitleText,\r\n } : undefined,\r\n }\r\n\r\n // Build badges and action badges\r\n const badges: ListItemData['badges'] = []\r\n const actionBadges: ListItemActionBadge[] = []\r\n\r\n // Add status badge for overdue/due soon tasks - clickable with completion actions\r\n if (nextDueDate) {\r\n const today = new Date()\r\n today.setHours(0, 0, 0, 0)\r\n const dueDate = new Date(nextDueDate)\r\n dueDate.setHours(0, 0, 0, 0)\r\n const diffTime = dueDate.getTime() - today.getTime()\r\n const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))\r\n\r\n if (diffDays < 0) {\r\n // Overdue - show as actionBadge with completion options\r\n const daysOverdue = Math.abs(diffDays)\r\n actionBadges.push({\r\n id: 'overdue',\r\n text: daysOverdue === 1 ? '1 day overdue' : `${daysOverdue} days overdue`,\r\n variant: 'destructive',\r\n actions: [\r\n {\r\n id: 'complete-today',\r\n label: 'Completed Today',\r\n icon: 'CheckCircle',\r\n onClick: onCompleteToday,\r\n },\r\n ],\r\n customContentType: 'date-picker-complete',\r\n onDateSelect: onCompleteOn,\r\n })\r\n } else if (diffDays <= 7) {\r\n // Due soon (within 7 days) - show as actionBadge with completion options\r\n actionBadges.push({\r\n id: 'due-soon',\r\n text: diffDays === 0 ? 'Due today' : diffDays === 1 ? 'Due tomorrow' : `Due in ${diffDays} days`,\r\n variant: 'outline',\r\n actions: [\r\n {\r\n id: 'complete-today',\r\n label: 'Completed Today',\r\n icon: 'CheckCircle',\r\n onClick: onCompleteToday,\r\n },\r\n ],\r\n customContentType: 'date-picker-complete',\r\n onDateSelect: onCompleteOn,\r\n })\r\n }\r\n }\r\n\r\n // Add assignment badge (either contact or resident)\r\n if (assigneeName) {\r\n badges.push({\r\n id: 'assigned',\r\n text: assigneeName,\r\n icon: assignedToResidentName ? 'User' : 'UserCircle',\r\n variant: 'blue',\r\n })\r\n }\r\n\r\n // Add linked service badge\r\n if (linkedServiceName) {\r\n badges.push({\r\n id: 'linked-service',\r\n text: linkedServiceName,\r\n icon: 'Briefcase',\r\n variant: 'secondary',\r\n })\r\n }\r\n\r\n badges.push({\r\n id: 'frequency',\r\n text: frequencyText,\r\n icon: 'Calendar',\r\n variant: 'secondary',\r\n })\r\n\r\n const sections: ListItemData['sections'] = []\r\n\r\n // Task details\r\n const detailsFields: any[] = []\r\n\r\n // Add category as first field\r\n detailsFields.push({\r\n label: t('maintenance.category'),\r\n value: categoryLabel,\r\n })\r\n\r\n // Add priority\r\n detailsFields.push({\r\n label: t('maintenance.priority'),\r\n value: priorityLabel,\r\n })\r\n\r\n if (task.estimated_cost) {\r\n detailsFields.push({\r\n label: t('maintenance.estimated_cost'),\r\n value: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(task.estimated_cost),\r\n format: 'currency',\r\n })\r\n }\r\n\r\n // Only show last completed in expanded view (due date is in collapsed subtitle)\r\n // Support both field names (last_completed_date from context, last_completed_at from types)\r\n const lastCompletedDate = (task as any).last_completed_date || task.last_completed_at\r\n if (lastCompletedDate) {\r\n const lastCompletedLabel = completedByName\r\n ? `${t('maintenance.last_completed')} (${completedByName})`\r\n : t('maintenance.last_completed')\r\n detailsFields.push({\r\n label: lastCompletedLabel,\r\n value: lastCompletedDate,\r\n format: 'date',\r\n })\r\n }\r\n\r\n if (detailsFields.length > 0) {\r\n sections.push({\r\n id: 'details',\r\n type: 'key-value-grid',\r\n fields: detailsFields,\r\n })\r\n }\r\n\r\n // Notes section\r\n if (task.notes) {\r\n sections.push({\r\n id: 'notes',\r\n type: 'text-block',\r\n title: { text: t('maintenance.notes'), icon: 'FileText' },\r\n text: task.notes,\r\n })\r\n }\r\n\r\n // Build children (assigned contact or resident)\r\n let children: ListItemData['children'] = undefined\r\n if (assignedContact) {\r\n children = {\r\n title: t('maintenance.assigned_contact'),\r\n titleIcon: 'UserCircle',\r\n items: [contactAdapter(assignedContact as Contact & { entity_type?: string }, { t, navigate, renderMode: 'child' })],\r\n }\r\n }\r\n\r\n return {\r\n ...data,\r\n badges,\r\n actionBadges: actionBadges.length > 0 ? actionBadges : undefined,\r\n sections: sections.length > 0 ? sections : undefined,\r\n children,\r\n }\r\n}\r\n","/**\r\n * Subscription Adapter\r\n *\r\n * Transforms subscription data into ListItemData format.\r\n * Supports identities as children with access control.\r\n * Platform-agnostic - uses TranslateFunction instead of IntlShape.\r\n */\r\n\r\nimport type { DigitalIdentity } from '@hearthcoo/types'\r\nimport { daysUntil } from '@hearthcoo/utils'\r\nimport type {\r\n ListItemData,\r\n BaseAdapterContext,\r\n} from '../types'\r\nimport { identityAdapter } from './identityAdapter'\r\nimport { passwordAdapter, Password } from './passwordAdapter'\r\nimport type { ComputedFields } from '../computedFields'\r\nimport { getExpiryStatus } from '../computedFields'\r\n\r\n/**\r\n * Extended subscription type for the adapter\r\n * Includes fields from frontend context that aren't in base Subscription type\r\n */\r\nexport interface SubscriptionData {\r\n id: string\r\n name?: string\r\n provider?: string\r\n category: string\r\n cost?: number\r\n billing_frequency: string\r\n start_date: string\r\n expiration_date?: string\r\n website?: string\r\n account_id?: string\r\n password?: string\r\n totp_secret?: string\r\n notes?: string\r\n identity_id?: string\r\n identity_display_name?: string\r\n password_id?: string\r\n password_display_name?: string\r\n linked_subscription_id?: string\r\n link_type?: 'billed_through' | 'credit_from'\r\n subscription_type?: 'recurring' | 'trial' | 'lifetime'\r\n}\r\n\r\nexport interface SubscriptionAdapterContext extends BaseAdapterContext {\r\n identities: DigitalIdentity[]\r\n passwords: Password[]\r\n getSubscriptionName: (subscription: SubscriptionData) => string\r\n formatCurrency: (amount: number) => string\r\n formatRenewalDate: (date: string, frequency: string) => string\r\n getNextRenewal: (startDate: string, frequency: string) => string\r\n accountHolderName?: string\r\n linkedSubscriptionName?: string\r\n}\r\n\r\n/**\r\n * Icon mapping for subscription categories\r\n */\r\nexport const SUBSCRIPTION_CATEGORY_ICONS: Record<string, string> = {\r\n streaming: 'Waves',\r\n cell_phone: 'Phone',\r\n internet: 'Wifi',\r\n fitness: 'Heart',\r\n cloud_services: 'Globe',\r\n news: 'FileText',\r\n music: 'Sparkles',\r\n gaming: 'Zap',\r\n shopping: 'Package',\r\n other: 'Star',\r\n}\r\n\r\n/**\r\n * Color mapping for subscription categories\r\n */\r\nexport const SUBSCRIPTION_CATEGORY_COLORS: Record<string, string> = {\r\n streaming: 'purple',\r\n cell_phone: 'blue',\r\n internet: 'blue',\r\n fitness: 'pink',\r\n cloud_services: 'green',\r\n news: 'gray',\r\n music: 'pink',\r\n gaming: 'yellow',\r\n shopping: 'orange',\r\n other: 'gray',\r\n}\r\n\r\n/**\r\n * Gets the category-specific icon for a subscription\r\n */\r\nfunction getCategoryIcon(category: string): string {\r\n return SUBSCRIPTION_CATEGORY_ICONS[category] || 'Star'\r\n}\r\n\r\n/**\r\n * Get computed fields for a subscription\r\n * Used by subscriptionAdapter internally and can be used directly by MCP, etc.\r\n */\r\nexport function getSubscriptionComputedFields(sub: SubscriptionData): ComputedFields {\r\n const computed: ComputedFields = {\r\n displayName: sub.name || sub.provider || 'Subscription',\r\n }\r\n\r\n if (sub.expiration_date) {\r\n computed.daysUntilExpiry = daysUntil(sub.expiration_date)\r\n computed.expiryStatus = getExpiryStatus(computed.daysUntilExpiry)\r\n }\r\n\r\n if (sub.category) {\r\n computed.formattedType = sub.category.charAt(0).toUpperCase() + sub.category.slice(1).replace(/_/g, ' ')\r\n }\r\n\r\n return computed\r\n}\r\n\r\n/**\r\n * Converts a Subscription to ListItemData format with identity children\r\n */\r\nexport function subscriptionAdapter(\r\n subscription: SubscriptionData & { entity_type?: string },\r\n context: SubscriptionAdapterContext\r\n): ListItemData {\r\n const {\r\n t,\r\n navigate,\r\n identities,\r\n passwords,\r\n getSubscriptionName,\r\n formatCurrency,\r\n formatRenewalDate,\r\n getNextRenewal,\r\n accountHolderName,\r\n linkedSubscriptionName\r\n } = context\r\n const entityType = subscription.entity_type\r\n if (!entityType) {\r\n throw new Error('subscriptionAdapter requires entity_type on the subscription object')\r\n }\r\n\r\n // Find the linked identity (if any)\r\n const linkedIdentity = subscription.identity_id\r\n ? identities.find(id => id.id === subscription.identity_id)\r\n : null\r\n\r\n // Determine if user has access to the identity\r\n const hasIdentityAccess = !!linkedIdentity\r\n\r\n // Find the linked password (if any)\r\n const linkedPassword = subscription.password_id\r\n ? passwords.find(p => p.id === subscription.password_id)\r\n : null\r\n\r\n // Determine if user has access to the password\r\n const hasPasswordAccess = !!linkedPassword\r\n\r\n // Build subtitle\r\n const subtitleParts: string[] = []\r\n if (accountHolderName) {\r\n subtitleParts.push(accountHolderName)\r\n }\r\n // Show renewal date only for recurring subscriptions (not billed through, not trial, not free/lifetime)\r\n if (subscription.link_type !== 'billed_through' &&\r\n subscription.subscription_type !== 'lifetime' &&\r\n subscription.subscription_type !== 'trial') {\r\n const nextRenewal = getNextRenewal(subscription.start_date, subscription.billing_frequency)\r\n subtitleParts.push(formatRenewalDate(nextRenewal, subscription.billing_frequency))\r\n }\r\n\r\n // Build badges\r\n const badges: ListItemData['badges'] = []\r\n\r\n // Identity badge (always show if there's a reference)\r\n if (subscription.identity_id || subscription.identity_display_name) {\r\n badges.push({\r\n id: 'identity',\r\n text: subscription.identity_display_name || 'Digital Identity',\r\n variant: 'secondary',\r\n icon: 'Key'\r\n })\r\n }\r\n\r\n // Household password badge (show if there's a reference)\r\n if (subscription.password_id || subscription.password_display_name) {\r\n badges.push({\r\n id: 'password',\r\n text: subscription.password_display_name || 'Household Password',\r\n variant: 'secondary',\r\n icon: 'Lock'\r\n })\r\n }\r\n\r\n // Cost badge (no icon since formatCurrency already includes currency symbol)\r\n if (subscription.cost) {\r\n badges.push({\r\n id: 'cost',\r\n text: formatCurrency(subscription.cost),\r\n variant: 'default'\r\n })\r\n }\r\n\r\n // Frequency badge (hide if billed through another subscription, trial, or lifetime)\r\n if (subscription.link_type !== 'billed_through' &&\r\n subscription.subscription_type !== 'lifetime' &&\r\n subscription.subscription_type !== 'trial') {\r\n const isMonthly = subscription.billing_frequency === 'monthly'\r\n const frequencyText = isMonthly ? 'Monthly' : 'Yearly'\r\n badges.push({\r\n id: 'frequency',\r\n text: frequencyText,\r\n variant: isMonthly ? 'cyan' : 'purple',\r\n icon: 'Calendar'\r\n })\r\n }\r\n\r\n // Subscription type badge (Trial or Lifetime)\r\n if (subscription.subscription_type === 'trial') {\r\n badges.push({\r\n id: 'subscription-type',\r\n text: t('subscriptions.type_trial'),\r\n variant: 'warning',\r\n icon: 'Clock'\r\n })\r\n } else if (subscription.subscription_type === 'lifetime') {\r\n badges.push({\r\n id: 'subscription-type',\r\n text: t('subscriptions.type_free'),\r\n variant: 'success',\r\n icon: 'Gift'\r\n })\r\n }\r\n\r\n // Linked subscription badge\r\n if (subscription.linked_subscription_id && linkedSubscriptionName) {\r\n const linkedText = subscription.link_type === 'billed_through'\r\n ? `Billed through ${linkedSubscriptionName}`\r\n : `Credit from ${linkedSubscriptionName}`\r\n badges.push({\r\n id: 'linked',\r\n text: linkedText,\r\n variant: 'secondary'\r\n })\r\n }\r\n\r\n // Build sections\r\n const sections: ListItemData['sections'] = []\r\n\r\n // Basic details section\r\n const detailsFields: Array<{\r\n label: string\r\n value: string | number\r\n format?: 'text' | 'number' | 'currency' | 'date' | 'monospace' | 'password' | 'url'\r\n icon?: string\r\n }> = []\r\n\r\n if (subscription.website) {\r\n detailsFields.push({\r\n label: 'Website',\r\n value: subscription.website,\r\n format: 'url'\r\n })\r\n }\r\n\r\n // Show direct credentials only if NOT using an identity or password reference\r\n if (!subscription.identity_id && !subscription.password_id && subscription.account_id) {\r\n detailsFields.push({\r\n label: 'Account ID',\r\n value: subscription.account_id,\r\n format: 'monospace'\r\n })\r\n }\r\n\r\n if (!subscription.identity_id && !subscription.password_id && subscription.password) {\r\n detailsFields.push({\r\n label: 'Password',\r\n value: subscription.password,\r\n format: 'password'\r\n })\r\n }\r\n\r\n if (subscription.start_date) {\r\n detailsFields.push({\r\n label: 'Start Date',\r\n value: subscription.start_date,\r\n format: 'date'\r\n })\r\n }\r\n\r\n if (subscription.expiration_date) {\r\n detailsFields.push({\r\n label: 'Expiration Date',\r\n value: subscription.expiration_date,\r\n format: 'date'\r\n })\r\n }\r\n\r\n if (detailsFields.length > 0) {\r\n sections.push({\r\n id: 'details',\r\n type: 'key-value-grid',\r\n fields: detailsFields\r\n })\r\n }\r\n\r\n // TOTP code generator (if totp_secret is available and not using identity or password reference)\r\n if (!subscription.identity_id && !subscription.password_id && subscription.totp_secret) {\r\n sections.push({\r\n id: 'totp',\r\n type: 'totp',\r\n title: {\r\n text: t('identities.totp_code'),\r\n icon: 'KeyRound',\r\n },\r\n totp: {\r\n secret: subscription.totp_secret,\r\n },\r\n })\r\n }\r\n\r\n // Notes section\r\n if (subscription.notes) {\r\n sections.push({\r\n id: 'notes',\r\n type: 'text-block',\r\n title: {\r\n text: 'Notes',\r\n icon: 'FileText'\r\n },\r\n text: subscription.notes\r\n })\r\n }\r\n\r\n // Subscribe action for EstateHelm trial subscriptions\r\n if (subscription.provider === 'estatehelm' && subscription.subscription_type === 'trial') {\r\n sections.push({\r\n id: 'subscribe-action',\r\n type: 'action-badge-list',\r\n actionBadges: [{\r\n id: 'subscribe',\r\n text: t('subscriptions.subscribe_now'),\r\n icon: 'CreditCard',\r\n variant: 'default',\r\n onClick: () => navigate('/billing')\r\n }]\r\n })\r\n }\r\n\r\n // Build children (identity or password if available)\r\n const childrenItems: ListItemData[] = []\r\n\r\n if (subscription.identity_id) {\r\n if (hasIdentityAccess && linkedIdentity) {\r\n // User has access - show full identity details\r\n childrenItems.push(identityAdapter(linkedIdentity as DigitalIdentity & { entity_type?: string }, { t, navigate }))\r\n } else {\r\n // User doesn't have access - show placeholder with info message\r\n childrenItems.push({\r\n id: subscription.identity_id,\r\n title: subscription.identity_display_name || 'Digital Identity',\r\n subtitle: {\r\n type: 'icon-text',\r\n text: t('subscriptions.no_identity_access'),\r\n icon: 'Info'\r\n },\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'Lock',\r\n bgColor: 'gray'\r\n },\r\n badges: [\r\n {\r\n id: 'restricted',\r\n text: t('subscriptions.restricted_access'),\r\n variant: 'secondary',\r\n icon: 'Info'\r\n }\r\n ],\r\n sections: [\r\n {\r\n id: 'access-info',\r\n type: 'text-block',\r\n title: {\r\n text: t('subscriptions.access_information'),\r\n icon: 'Info'\r\n },\r\n text: t('subscriptions.identity_access_message')\r\n }\r\n ]\r\n })\r\n }\r\n }\r\n\r\n // Add linked password as child if available\r\n if (subscription.password_id) {\r\n if (hasPasswordAccess && linkedPassword) {\r\n // User has access - show full password details\r\n childrenItems.push(passwordAdapter({ ...linkedPassword, entity_type: 'password' }, { t, navigate }))\r\n } else {\r\n // User doesn't have access - show placeholder with info message\r\n childrenItems.push({\r\n id: subscription.password_id,\r\n title: subscription.password_display_name || 'Household Password',\r\n subtitle: {\r\n type: 'icon-text',\r\n text: t('subscriptions.no_password_access'),\r\n icon: 'Info'\r\n },\r\n icon: {\r\n type: 'lucide-icon',\r\n value: 'Lock',\r\n bgColor: 'gray'\r\n },\r\n badges: [\r\n {\r\n id: 'restricted',\r\n text: t('subscriptions.restricted_access'),\r\n variant: 'secondary',\r\n icon: 'Info'\r\n }\r\n ],\r\n sections: [\r\n {\r\n id: 'access-info',\r\n type: 'text-block',\r\n title: {\r\n text: t('subscriptions.access_information'),\r\n icon: 'Info'\r\n },\r\n text: t('subscriptions.password_access_message')\r\n }\r\n ]\r\n })\r\n }\r\n }\r\n\r\n // Get category-specific color\r\n const categoryColor = SUBSCRIPTION_CATEGORY_COLORS[subscription.category] || 'gray'\r\n\r\n return {\r\n id: subscription.id,\r\n title: getSubscriptionName(subscription),\r\n subtitle: subtitleParts.length > 0 ? {\r\n type: 'text',\r\n text: subtitleParts.join(' • ')\r\n } : undefined,\r\n icon: {\r\n type: 'lucide-icon',\r\n value: getCategoryIcon(subscription.category),\r\n bgColor: categoryColor as any\r\n },\r\n badges,\r\n sections: sections.length > 0 ? sections : undefined,\r\n children: childrenItems.length > 0 ? {\r\n title: subscription.identity_id ? 'Linked Identity' : 'Linked Password',\r\n titleIcon: subscription.identity_id ? 'Key' : 'Lock',\r\n items: childrenItems\r\n } : undefined\r\n }\r\n}\r\n","/**\r\n * Credential Adapter\r\n *\r\n * Converts Credential entities to ListItemData for rendering.\r\n * Platform-agnostic - uses TranslateFunction instead of IntlShape.\r\n */\r\n\r\nimport type {\r\n Credential,\r\n CredentialType,\r\n} from '@hearthcoo/types'\r\nimport { CREDENTIAL_TYPE_ICONS, CREDENTIAL_SUBTYPE_ICONS } from '@hearthcoo/types'\r\nimport type {\r\n ListItemData,\r\n BaseAdapterContext,\r\n IconBgColor,\r\n} from '../types'\r\nimport { defaultFormatDate } from '../types'\r\nimport { daysUntil } from '@hearthcoo/utils'\r\nimport type { ComputedFields } from '../computedFields'\r\nimport { getExpiryStatus } from '../computedFields'\r\n\r\nexport interface CredentialAdapterContext extends BaseAdapterContext {\r\n personName?: string\r\n}\r\n\r\nconst getCredentialIcon = (type: CredentialType, subtype?: string): string => {\r\n // Try subtype icon first, then fall back to type icon\r\n if (subtype && CREDENTIAL_SUBTYPE_ICONS[subtype]) {\r\n return CREDENTIAL_SUBTYPE_ICONS[subtype]\r\n }\r\n return CREDENTIAL_TYPE_ICONS[type] || 'BadgeCheck'\r\n}\r\n\r\nexport const CREDENTIAL_TYPE_COLORS: Record<CredentialType, IconBgColor> = {\r\n professional_license: 'blue',\r\n government_id: 'purple',\r\n travel_credential: 'green',\r\n permit: 'orange',\r\n security_clearance: 'red',\r\n}\r\n\r\n/**\r\n * Get computed fields for a credential\r\n * Used by credentialAdapter internally and can be used directly by MCP, etc.\r\n */\r\nexport function getCredentialComputedFields(credential: Credential): ComputedFields {\r\n const computed: ComputedFields = {\r\n displayName: credential.name || credential.credential_type || 'Credential',\r\n }\r\n\r\n if (credential.expiration_date) {\r\n computed.daysUntilExpiry = daysUntil(credential.expiration_date)\r\n computed.expiryStatus = getExpiryStatus(computed.daysUntilExpiry)\r\n }\r\n\r\n if (credential.credential_type) {\r\n computed.formattedType = credential.credential_type.charAt(0).toUpperCase() +\r\n credential.credential_type.slice(1).replace(/_/g, ' ')\r\n }\r\n\r\n return computed\r\n}\r\n\r\nexport function credentialAdapter(\r\n credential: Credential & { entity_type?: string },\r\n context: CredentialAdapterContext\r\n): ListItemData {\r\n const {\r\n t,\r\n renderMode = 'primary',\r\n } = context\r\n const entityType = credential.entity_type\r\n if (!entityType) {\r\n throw new Error('credentialAdapter requires entity_type on the credential object')\r\n }\r\n const formatDate = context.formatDate || defaultFormatDate\r\n\r\n // Build subtitle\r\n const subtitleParts: string[] = []\r\n if (credential.issuing_authority) {\r\n subtitleParts.push(credential.issuing_authority)\r\n }\r\n if (credential.state) {\r\n subtitleParts.push(credential.state)\r\n }\r\n if (credential.expiration_date) {\r\n subtitleParts.push(`Exp: ${formatDate(credential.expiration_date)}`)\r\n }\r\n\r\n // Build badges\r\n const badges: ListItemData['badges'] = []\r\n\r\n // Type badge\r\n if (credential.credential_type) {\r\n const typeLabel = credential.credential_type\r\n .replace(/_/g, ' ')\r\n .replace(/\\b\\w/g, l => l.toUpperCase())\r\n badges.push({\r\n id: 'type',\r\n text: typeLabel,\r\n variant: 'outline',\r\n })\r\n }\r\n\r\n // Subtype badge\r\n if (credential.credential_subtype && credential.credential_subtype !== 'other') {\r\n const subtypeLabel = credential.credential_subtype\r\n .replace(/_/g, ' ')\r\n .replace(/\\b\\w/g, l => l.toUpperCase())\r\n badges.push({\r\n id: 'subtype',\r\n text: subtypeLabel,\r\n variant: 'secondary',\r\n })\r\n }\r\n\r\n // Expiration warning badge\r\n if (credential.expiration_date) {\r\n const expDate = new Date(credential.expiration_date)\r\n const now = new Date()\r\n const daysUntilExpiry = Math.ceil((expDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))\r\n\r\n if (daysUntilExpiry < 0) {\r\n badges.push({\r\n id: 'expired',\r\n text: 'Expired',\r\n variant: 'destructive',\r\n })\r\n } else if (daysUntilExpiry <= 90) {\r\n badges.push({\r\n id: 'expiring-soon',\r\n text: `Expires in ${daysUntilExpiry} days`,\r\n variant: 'warning',\r\n })\r\n }\r\n }\r\n\r\n // Build sections for expanded view\r\n const sections: ListItemData['sections'] = []\r\n\r\n if (renderMode === 'primary') {\r\n const detailsFields: any[] = []\r\n\r\n if (credential.credential_number) {\r\n detailsFields.push({\r\n label: t('credentials.number'),\r\n value: credential.credential_number,\r\n })\r\n }\r\n\r\n if (credential.issuing_authority) {\r\n detailsFields.push({\r\n label: t('credentials.issuing_authority'),\r\n value: credential.issuing_authority,\r\n })\r\n }\r\n\r\n if (credential.state) {\r\n detailsFields.push({\r\n label: t('credentials.state'),\r\n value: credential.state,\r\n })\r\n }\r\n\r\n if (credential.issue_date) {\r\n detailsFields.push({\r\n label: t('credentials.issue_date'),\r\n value: credential.issue_date,\r\n format: 'date',\r\n })\r\n }\r\n\r\n if (credential.expiration_date) {\r\n detailsFields.push({\r\n label: t('credentials.expiration_date'),\r\n value: credential.expiration_date,\r\n format: 'date',\r\n })\r\n }\r\n\r\n if (credential.renewal_deadline) {\r\n detailsFields.push({\r\n label: t('credentials.renewal_deadline'),\r\n value: credential.renewal_deadline,\r\n format: 'date',\r\n })\r\n }\r\n\r\n if (detailsFields.length > 0) {\r\n sections.push({\r\n id: 'details',\r\n type: 'key-value-grid',\r\n fields: detailsFields,\r\n })\r\n }\r\n\r\n // Notes section\r\n if (credential.notes) {\r\n sections.push({\r\n id: 'notes',\r\n type: 'text-block',\r\n title: { text: t('common.notes'), icon: 'FileText' },\r\n text: credential.notes,\r\n })\r\n }\r\n\r\n // Document sections (show all documents)\r\n const docs = credential.documents || []\r\n docs.forEach((doc, index) => {\r\n if (doc?.file_id) {\r\n // Use different titles for multiple documents (e.g., \"Front\", \"Back\")\r\n const docTitle = docs.length > 1\r\n ? `${t('credentials.document')} ${index + 1}`\r\n : t('credentials.document')\r\n sections.push({\r\n id: `document-${index}`,\r\n type: 'image',\r\n title: { text: docTitle, icon: 'FileText' },\r\n image: {\r\n fileId: doc.file_id,\r\n entityId: credential.id,\r\n entityType,\r\n keyType: 'general',\r\n alt: `${credential.name} - ${docTitle}`,\r\n downloadLabel: t('common.download'),\r\n },\r\n })\r\n }\r\n })\r\n }\r\n\r\n const iconColor = CREDENTIAL_TYPE_COLORS[credential.credential_type] || 'gray'\r\n\r\n return {\r\n id: credential.id,\r\n renderMode,\r\n icon: {\r\n type: 'lucide-icon',\r\n value: getCredentialIcon(credential.credential_type, credential.credential_subtype),\r\n bgColor: iconColor,\r\n },\r\n title: credential.name,\r\n subtitle: subtitleParts.length > 0 ? {\r\n type: 'text',\r\n text: subtitleParts.join(' • '),\r\n } : undefined,\r\n badges,\r\n sections: sections.length > 0 ? sections : undefined,\r\n }\r\n}\r\n","/**\r\n * Entity Enrichment\r\n *\r\n * Re-exports from @hearthcoo/list-data - the single source of truth\r\n * for computed entity fields.\r\n *\r\n * @module estatehelm/enrichment\r\n */\r\n\r\nimport type { ComputedFields, EnrichedEntity } from '@hearthcoo/list-data'\r\nimport {\r\n getExpiryStatus,\r\n getPetComputedFields,\r\n getVehicleComputedFields,\r\n getInsuranceComputedFields,\r\n getSubscriptionComputedFields,\r\n getContactComputedFields,\r\n getCredentialComputedFields,\r\n} from '@hearthcoo/list-data'\r\nimport { getEntityDisplayName } from '@hearthcoo/utils'\r\n\r\n// Re-export types\r\nexport type { ComputedFields, EnrichedEntity }\r\n\r\n/**\r\n * Get computed fields for any entity type\r\n */\r\nfunction getComputedFields(entity: Record<string, any>, entityType: string): ComputedFields {\r\n switch (entityType) {\r\n case 'pet':\r\n return getPetComputedFields(entity as any)\r\n case 'vehicle':\r\n return getVehicleComputedFields(entity as any)\r\n case 'insurance':\r\n return getInsuranceComputedFields(entity as any)\r\n case 'subscription':\r\n return getSubscriptionComputedFields(entity as any)\r\n case 'contact':\r\n return getContactComputedFields(entity as any)\r\n case 'credential':\r\n return getCredentialComputedFields(entity as any)\r\n default:\r\n // Generic enrichment for other types\r\n return {\r\n displayName: getEntityDisplayName(entityType, entity),\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Enrich an entity with computed fields based on its type\r\n */\r\nexport function enrichEntity(\r\n entity: Record<string, any>,\r\n entityType: string\r\n): EnrichedEntity {\r\n const computed = getComputedFields(entity, entityType)\r\n return { ...entity, _computed: computed }\r\n}\r\n\r\n/**\r\n * Enrich an array of entities\r\n */\r\nexport function enrichEntities(\r\n entities: Record<string, any>[],\r\n entityType: string\r\n): EnrichedEntity[] {\r\n return entities.map((entity) => enrichEntity(entity, entityType))\r\n}\r\n\r\nexport { getExpiryStatus }\r\n","/**\r\n * Entities Resource\r\n *\r\n * MCP resource handlers for entity data with enrichment.\r\n *\r\n * @module estatehelm/resources/entities\r\n */\r\n\r\nimport { getDecryptedEntities, getHouseholds } from '../cache.js'\r\nimport { redactEntity } from '../filter.js'\r\nimport { enrichEntity, enrichEntities, type EnrichedEntity } from '../enrichment.js'\r\nimport type { PrivacyMode } from '../config.js'\r\n\r\n/**\r\n * Supported entity types\r\n */\r\nexport const ENTITY_TYPES = [\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] as const\r\n\r\n/**\r\n * Get all entities of a type for a household\r\n */\r\nexport async function getEntities(\r\n householdId: string,\r\n entityType: string,\r\n privateKey: CryptoKey,\r\n privacyMode: PrivacyMode\r\n): Promise<EnrichedEntity[]> {\r\n const entities = await getDecryptedEntities(householdId, entityType, privateKey)\r\n\r\n // Enrich with computed fields, then redact sensitive data\r\n return enrichEntities(entities, entityType).map((entity) =>\r\n redactEntity(entity, entityType, privacyMode)\r\n )\r\n}\r\n\r\n/**\r\n * Get a single entity by ID\r\n */\r\nexport async function getEntity(\r\n householdId: string,\r\n entityType: string,\r\n entityId: string,\r\n privateKey: CryptoKey,\r\n privacyMode: PrivacyMode\r\n): Promise<EnrichedEntity | null> {\r\n const entities = await getDecryptedEntities(householdId, entityType, privateKey)\r\n const entity = entities.find((e) => e.id === entityId)\r\n\r\n if (!entity) {\r\n return null\r\n }\r\n\r\n // Enrich with computed fields, then redact sensitive data\r\n const enriched = enrichEntity(entity, entityType)\r\n return redactEntity(enriched, entityType, privacyMode)\r\n}\r\n\r\n/**\r\n * Expiring item result\r\n */\r\nexport interface ExpiringItem {\r\n householdId: string\r\n householdName: string\r\n type: string\r\n name: string\r\n expiresAt: string\r\n daysUntil: number\r\n}\r\n\r\n/**\r\n * Get items expiring within a given number of days\r\n */\r\nexport async function getExpiringItems(\r\n days: number,\r\n householdId: string | undefined,\r\n privateKey: CryptoKey\r\n): Promise<ExpiringItem[]> {\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: ExpiringItem[] = []\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.expiration_date) {\r\n const expires = new Date(policy.expiration_date)\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.provider || policy.policy_number,\r\n expiresAt: policy.expiration_date,\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 and tabs\r\n const vehicles = await getDecryptedEntities(household.id, 'vehicle', privateKey)\r\n for (const vehicle of vehicles) {\r\n const vehicleName = `${vehicle.year || ''} ${vehicle.make || ''} ${vehicle.model || ''}`.trim() || 'Vehicle'\r\n\r\n if (vehicle.registration_expiration) {\r\n const expires = new Date(vehicle.registration_expiration)\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: vehicleName,\r\n expiresAt: vehicle.registration_expiration,\r\n daysUntil: Math.ceil((expires.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)),\r\n })\r\n }\r\n }\r\n\r\n if (vehicle.tabs_expiration) {\r\n const expires = new Date(vehicle.tabs_expiration)\r\n if (expires <= cutoff) {\r\n expiring.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n type: 'vehicle_tabs',\r\n name: vehicleName,\r\n expiresAt: vehicle.tabs_expiration,\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.expiration_date) {\r\n const expires = new Date(sub.expiration_date)\r\n if (expires <= 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.custom_name || sub.provider || sub.service_name,\r\n expiresAt: sub.expiration_date,\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 credentials (IDs, licenses, passports)\r\n const credentials = await getDecryptedEntities(household.id, 'credential', privateKey)\r\n for (const cred of credentials) {\r\n if (cred.expiration_date) {\r\n const expires = new Date(cred.expiration_date)\r\n if (expires <= cutoff) {\r\n expiring.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n type: 'credential',\r\n name: cred.name || cred.credential_type,\r\n expiresAt: cred.expiration_date,\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\r\n // Sort by days until expiration\r\n expiring.sort((a, b) => a.daysUntil - b.daysUntil)\r\n\r\n return expiring\r\n}\r\n","/**\r\n * Search Tool\r\n *\r\n * MCP tool for searching across all entities using shared search engine.\r\n *\r\n * @module estatehelm/tools/search\r\n */\r\n\r\nimport {\r\n indexAllEntities,\r\n searchEntities,\r\n type AllEntitiesData,\r\n type SearchableEntityType,\r\n} from '@hearthcoo/utils'\r\nimport { getDecryptedEntities, getHouseholds } from '../cache.js'\r\nimport { redactEntity } from '../filter.js'\r\nimport { enrichEntity } from '../enrichment.js'\r\nimport type { PrivacyMode } from '../config.js'\r\n\r\n/**\r\n * Search parameters\r\n */\r\nexport interface SearchParams {\r\n query: string\r\n householdId?: string\r\n entityType?: string\r\n}\r\n\r\n/**\r\n * Search result item\r\n */\r\nexport interface SearchResultItem {\r\n householdId: string\r\n householdName: string\r\n entityType: string\r\n entity: Record<string, any>\r\n score: number\r\n matchedFields: string[]\r\n}\r\n\r\n/**\r\n * Build AllEntitiesData from cached entities for a household\r\n */\r\nasync function buildEntitiesData(\r\n householdId: string,\r\n privateKey: CryptoKey\r\n): Promise<AllEntitiesData> {\r\n // Map of MCP entity types to AllEntitiesData property names\r\n const entityTypeMapping: Record<string, keyof AllEntitiesData> = {\r\n property: 'properties',\r\n vehicle: 'vehicles',\r\n pet: 'pets',\r\n contact: 'contacts',\r\n subscription: 'subscriptions',\r\n service: 'services',\r\n insurance: 'insurancePolicies',\r\n valuable: 'valuables',\r\n bank_account: 'financialAccounts',\r\n investment: 'financialAccounts',\r\n credential: 'credentials',\r\n maintenance_task: 'maintenanceTasks',\r\n access_code: 'accessCodes',\r\n device: 'devices',\r\n person: 'people',\r\n pet_vet_visit: 'petVetVisits',\r\n vehicle_service: 'vehicleServices',\r\n legal: 'legalDocuments',\r\n health_record: 'healthRecords',\r\n education_record: 'educationRecords',\r\n military_record: 'militaryRecords',\r\n membership_record: 'membershipRecords',\r\n home_improvement: 'homeImprovements',\r\n tax_year: 'taxYears',\r\n }\r\n\r\n const data: AllEntitiesData = {}\r\n const entityTypes = Object.keys(entityTypeMapping)\r\n\r\n for (const type of entityTypes) {\r\n try {\r\n const entities = await getDecryptedEntities(householdId, type, privateKey)\r\n if (entities.length > 0) {\r\n const propName = entityTypeMapping[type]\r\n const existing = (data[propName] as any[]) || []\r\n ;(data[propName] as any[]) = [...existing, ...entities]\r\n }\r\n } catch {\r\n // Entity type not found in cache\r\n }\r\n }\r\n\r\n return data\r\n}\r\n\r\n/**\r\n * Execute search across all entities\r\n */\r\nexport async function executeSearch(\r\n params: SearchParams,\r\n privateKey: CryptoKey,\r\n privacyMode: PrivacyMode\r\n): Promise<SearchResultItem[]> {\r\n const { query, householdId, entityType } = params\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: SearchResultItem[] = []\r\n\r\n for (const household of searchHouseholds) {\r\n // Build search index from all cached entities\r\n const entitiesData = await buildEntitiesData(household.id, privateKey)\r\n const searchableEntities = indexAllEntities(entitiesData)\r\n\r\n // Use shared search engine with proper scoring\r\n const searchResults = searchEntities(searchableEntities, query, {\r\n maxTotal: 50,\r\n includeTypes: entityType ? [entityType as SearchableEntityType] : undefined,\r\n })\r\n\r\n // Map search results back to full entity data\r\n for (const result of searchResults) {\r\n const type = result.entity.entityType\r\n const entityId = result.entity.id\r\n\r\n // Get the full entity from cache\r\n const entities = await getDecryptedEntities(household.id, type, privateKey)\r\n const fullEntity = entities.find((e) => e.id === entityId)\r\n\r\n if (fullEntity) {\r\n // Apply enrichment and redaction\r\n const enriched = enrichEntity(fullEntity, type)\r\n const redacted = redactEntity(enriched, type, privacyMode)\r\n\r\n results.push({\r\n householdId: household.id,\r\n householdName: household.name,\r\n entityType: type,\r\n entity: redacted,\r\n score: result.score,\r\n matchedFields: result.matchedFields,\r\n })\r\n }\r\n }\r\n }\r\n\r\n // Sort by score across all households\r\n results.sort((a, b) => b.score - a.score)\r\n\r\n return results\r\n}\r\n","/**\r\n * Files Tool\r\n *\r\n * MCP tool for downloading and decrypting file attachments.\r\n *\r\n * @module estatehelm/tools/files\r\n */\r\n\r\nimport type { ApiClient } from '@hearthcoo/api-client'\r\nimport { downloadAndDecryptFile, type DecryptedFile } from '../cache.js'\r\n\r\n/**\r\n * Download file parameters\r\n */\r\nexport interface DownloadFileParams {\r\n fileId: string\r\n householdId: string\r\n entityId: string\r\n entityType: string\r\n}\r\n\r\n/**\r\n * Download file result\r\n */\r\nexport interface DownloadFileResult {\r\n success: boolean\r\n fileName?: string\r\n mimeType?: string\r\n size?: number\r\n data?: string // base64 encoded\r\n error?: string\r\n}\r\n\r\n/**\r\n * Execute file download\r\n */\r\nexport async function executeFileDownload(\r\n client: ApiClient,\r\n params: DownloadFileParams\r\n): Promise<DownloadFileResult> {\r\n const { fileId, householdId, entityId, entityType } = params\r\n\r\n try {\r\n const result = await downloadAndDecryptFile(\r\n client,\r\n householdId,\r\n fileId,\r\n entityId,\r\n entityType\r\n )\r\n\r\n return {\r\n success: true,\r\n fileName: result.fileName,\r\n mimeType: result.mimeType,\r\n size: result.bytes.length,\r\n data: result.dataBase64,\r\n }\r\n } catch (err: any) {\r\n return {\r\n success: false,\r\n error: err.message,\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":["path","envPaths","hostname","keytar","keytar","open","import_types","fs","path","Database","getDecryptedEntities","results","daysUntil","synced"]}
|