@merklevault/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/merklevault.ts","../../daemon/kubo-manager.ts","../../daemon/database.ts","../../daemon/content-store.ts","../../daemon/pinata-content-store.ts","../../daemon/storage-router.ts","../../daemon/sync-queue-processor.ts","../../daemon/crypto.ts"],"sourcesContent":["/**\n * @merklevault/core\n *\n * SDK MerkleVault — Coffre-fort chiffre sur IPFS\n *\n * Usage :\n * import { MerkleVault } from '@merklevault/core';\n *\n * const vault = new MerkleVault({ dataDir: './my-vault' });\n * await vault.start();\n *\n * // Creer un vault\n * const { mnemonic } = await vault.create('monMotDePasse');\n * console.log('Phrase de recuperation:', mnemonic);\n *\n * // Ajouter un fichier\n * const file = await vault.addFile(Buffer.from('hello'), { name: 'hello.txt' });\n *\n * // Lire un fichier\n * const result = await vault.getFile(file.node.id);\n * console.log(result.data.toString()); // \"hello\"\n *\n * // Ecouter les evenements\n * vault.on('sync:complete', (event) => console.log('Synced!', event));\n *\n * await vault.stop();\n */\n\n// Classe principale\nexport { MerkleVault } from './merklevault.js';\n\n// Types publics\nexport type {\n MerkleVaultOptions,\n FileNode,\n FileVersion,\n SyncStatusValue,\n VaultInfo,\n CreateVaultResult,\n CloudProvider,\n PinataCredentials,\n CloudConfig,\n SyncStatus,\n AddFileResult,\n GetFileResult,\n FolderListing,\n DaemonStatus,\n VaultEventType,\n VaultEvent,\n} from './types.js';\n","/**\n * @merklevault/core — Classe facade principale\n *\n * Encapsule toute la logique metier de MerkleVault :\n * - Gestion du vault (creation, deverrouillage, recovery)\n * - CRUD fichiers/dossiers sur IPFS (Kubo)\n * - Chiffrement E2E (XChaCha20-Poly1305 + Argon2id)\n * - Synchronisation cloud (Pinata)\n *\n * Usage :\n * import { MerkleVault } from '@merklevault/core';\n * const vault = new MerkleVault({ dataDir: './my-vault' });\n * await vault.start();\n * await vault.create({ password: 'secret' });\n * const file = await vault.addFile(buffer, { name: 'doc.pdf' });\n */\n\nimport { EventEmitter } from 'node:events';\nimport { readFileSync, mkdirSync, existsSync } from 'node:fs';\nimport path from 'node:path';\nimport { createHash } from 'node:crypto';\n\n// Imports internes — modules existants du daemon\nimport { KuboManager } from '../../daemon/kubo-manager.js';\nimport { Database, NodeRow, FileVersionRow } from '../../daemon/database.js';\nimport { KuboContentStore, ContentStore, sha256 } from '../../daemon/content-store.js';\nimport { StorageRouter } from '../../daemon/storage-router.js';\nimport { PinataConfig } from '../../daemon/pinata-content-store.js';\nimport { SyncQueueProcessor } from '../../daemon/sync-queue-processor.js';\nimport {\n ENC_ALGO,\n createVault as cryptoCreateVault,\n unlockVault as cryptoUnlockVault,\n recoverVault as cryptoRecoverVault,\n changePassword as cryptoChangePassword,\n encryptFile,\n decryptFile,\n zeroKey,\n VaultConfig as CryptoVaultConfig,\n} from '../../daemon/crypto.js';\n\nimport type {\n MerkleVaultOptions,\n FileNode,\n FileVersion,\n AddFileResult,\n GetFileResult,\n FolderListing,\n CreateVaultResult,\n VaultInfo,\n CloudProvider,\n PinataCredentials,\n CloudConfig,\n SyncStatus,\n DaemonStatus,\n VaultEventType,\n VaultEvent,\n} from './types.js';\n\n// ─── Helpers : conversion DB row → type public ───────────────────\n\nfunction toFileNode(row: NodeRow): FileNode {\n return {\n id: row.id,\n parentId: row.parent_id,\n name: row.name,\n kind: row.kind,\n createdAt: row.created_at,\n modifiedAt: row.modified_at,\n deletedAt: row.deleted_at,\n currentVersionId: row.current_version_id,\n };\n}\n\nfunction toFileVersion(row: any): FileVersion {\n return {\n id: row.id,\n nodeId: row.node_id,\n cid: row.cid,\n sizeBytes: row.size_bytes,\n sha256Plain: row.sha256_plain,\n createdAt: row.created_at,\n encAlgo: row.enc_algo,\n provider: row.provider || 'local',\n pinataCid: row.pinata_cid || null,\n syncStatus: row.sync_status || 'none',\n };\n}\n\n// ─── Classe principale ───────────────────────────────────────────\n\nexport class MerkleVault extends EventEmitter {\n private opts: Required<MerkleVaultOptions> & { disableAutoLock: boolean };\n private kubo!: KuboManager;\n private db!: Database;\n private store!: StorageRouter;\n private kuboStore!: ContentStore;\n private syncProcessor: SyncQueueProcessor | null = null;\n\n // Vault state\n private masterKey: Uint8Array | null = null;\n private lockTimer: ReturnType<typeof setTimeout> | null = null;\n private started = false;\n\n constructor(options: MerkleVaultOptions) {\n super();\n this.opts = {\n dataDir: options.dataDir,\n kuboBinaryPath: options.kuboBinaryPath ?? '',\n kuboApiPort: options.kuboApiPort ?? 5101,\n autoLockMs: options.autoLockMs ?? 15 * 60 * 1000,\n disableAutoLock: options.disableAutoLock ?? false,\n };\n }\n\n // ═══════════════════════════════════════════════════════════════\n // LIFECYCLE\n // ═══════════════════════════════════════════════════════════════\n\n /**\n * Demarre le vault : initialise la DB, lance Kubo, restaure la config cloud.\n * Doit etre appele avant toute autre operation.\n */\n async start(): Promise<void> {\n if (this.started) return;\n\n const { dataDir, kuboBinaryPath, kuboApiPort } = this.opts;\n\n // Creer le dossier data si necessaire\n if (!existsSync(dataDir)) {\n mkdirSync(dataDir, { recursive: true });\n }\n\n // Initialiser la DB\n this.db = new Database(dataDir);\n\n // Initialiser Kubo\n const defaultKuboPath = kuboBinaryPath || path.resolve(path.join(dataDir, '..', 'kubo', 'ipfs'));\n this.kubo = new KuboManager({\n kuboBinaryPath: defaultKuboPath,\n repoPath: path.join(dataDir, 'ipfs-repo'),\n apiPort: kuboApiPort,\n });\n\n // Ecouter les evenements Kubo\n this.kubo.on('ready', () => this.emitEvent('kubo:ready'));\n this.kubo.on('crash', (info: any) => this.emitEvent('kubo:crash', info));\n this.kubo.on('log', (msg: string) => {\n // Log interne, pas d'evenement\n });\n\n // Demarrer Kubo\n await this.kubo.start();\n\n // Initialiser le storage\n this.kuboStore = new KuboContentStore(`http://127.0.0.1:${kuboApiPort}`);\n this.store = new StorageRouter(this.kuboStore);\n\n // Restaurer la config cloud\n this.restoreCloudConfig();\n\n // Demarrer le SyncQueueProcessor\n this.syncProcessor = new SyncQueueProcessor(this.db, this.store, this.kuboStore);\n this.syncProcessor.onEvent((event) => {\n this.emitEvent(event.type as VaultEventType, event.data);\n });\n this.syncProcessor.start();\n\n this.started = true;\n }\n\n /**\n * Arrete le vault proprement : ferme Kubo, la DB, le sync processor.\n */\n async stop(): Promise<void> {\n if (!this.started) return;\n\n // Lock le vault\n this.lock();\n\n // Arreter le sync processor\n if (this.syncProcessor) {\n this.syncProcessor.stop();\n this.syncProcessor = null;\n }\n\n // Arreter Kubo\n await this.kubo.stop();\n\n // Fermer la DB\n this.db.close();\n\n this.started = false;\n }\n\n /**\n * Verifie que le vault est demarre, sinon throw.\n */\n private ensureStarted(): void {\n if (!this.started) {\n throw new Error('MerkleVault not started — call vault.start() first');\n }\n }\n\n /**\n * Verifie que le vault est deverrouille, sinon throw.\n */\n private ensureUnlocked(): void {\n this.ensureStarted();\n if (!this.masterKey) {\n throw new Error('Vault is locked — call vault.unlock() first');\n }\n }\n\n // ═══════════════════════════════════════════════════════════════\n // VAULT MANAGEMENT\n // ═══════════════════════════════════════════════════════════════\n\n /**\n * Cree un nouveau vault avec un mot de passe.\n * Retourne la phrase de recuperation BIP39 (24 mots).\n *\n * @param password - Mot de passe maitre (min 8 caracteres)\n * @returns Phrase de recuperation a afficher UNE SEULE FOIS\n */\n async create(password: string): Promise<CreateVaultResult> {\n this.ensureStarted();\n\n if (this.db.isVaultInitialized()) {\n throw new Error('Vault already initialized');\n }\n if (!password || password.length < 8) {\n throw new Error('Password must be at least 8 characters');\n }\n\n const { mnemonic, config, masterKey } = await cryptoCreateVault(password);\n this.saveVaultConfig(config);\n this.masterKey = masterKey;\n this.resetLockTimer();\n\n this.emitEvent('vault:created');\n this.emitEvent('vault:unlocked');\n\n return { mnemonic };\n }\n\n /**\n * Deverrouille le vault avec le mot de passe.\n *\n * @param password - Mot de passe maitre\n * @returns true si le deverrouillage a reussi\n * @throws Si le mot de passe est invalide\n */\n unlock(password: string): boolean {\n this.ensureStarted();\n\n const config = this.loadVaultConfig();\n const masterKey = cryptoUnlockVault(password, config);\n if (!masterKey) {\n throw new Error('Invalid password');\n }\n\n this.masterKey = masterKey;\n this.resetLockTimer();\n this.emitEvent('vault:unlocked');\n\n return true;\n }\n\n /**\n * Verrouille le vault — efface la master key de la memoire.\n */\n lock(): void {\n if (this.masterKey) {\n zeroKey(this.masterKey);\n this.masterKey = null;\n }\n if (this.lockTimer) {\n clearTimeout(this.lockTimer);\n this.lockTimer = null;\n }\n this.emitEvent('vault:locked', { reason: 'manual' });\n }\n\n /**\n * Recupere le vault via la phrase BIP39 et definit un nouveau mot de passe.\n *\n * @param mnemonic - Phrase de recuperation (24 mots)\n * @param newPassword - Nouveau mot de passe\n */\n async recover(mnemonic: string, newPassword: string): Promise<boolean> {\n this.ensureStarted();\n\n const config = this.loadVaultConfig();\n const result = await cryptoRecoverVault(mnemonic, newPassword, config);\n if (!result) {\n throw new Error('Recovery failed — invalid mnemonic');\n }\n\n this.saveVaultConfig(result.config);\n this.masterKey = result.masterKey;\n this.resetLockTimer();\n this.emitEvent('vault:unlocked');\n\n return true;\n }\n\n /**\n * Change le mot de passe du vault sans re-chiffrer les fichiers.\n *\n * @param oldPassword - Ancien mot de passe\n * @param newPassword - Nouveau mot de passe\n */\n async changePassword(oldPassword: string, newPassword: string): Promise<boolean> {\n this.ensureStarted();\n\n const config = this.loadVaultConfig();\n const newConfig = await cryptoChangePassword(oldPassword, newPassword, config);\n if (!newConfig) {\n throw new Error('Invalid current password');\n }\n\n this.saveVaultConfig(newConfig);\n return true;\n }\n\n /**\n * Retourne l'etat du vault (initialise, deverrouille).\n */\n getVaultInfo(): VaultInfo {\n this.ensureStarted();\n return {\n initialized: this.db.isVaultInitialized(),\n unlocked: this.masterKey !== null,\n };\n }\n\n /**\n * Verifie si le vault est deverrouille.\n */\n isUnlocked(): boolean {\n return this.masterKey !== null;\n }\n\n // ═══════════════════════════════════════════════════════════════\n // FILE OPERATIONS\n // ═══════════════════════════════════════════════════════════════\n\n /**\n * Ajoute un fichier au vault depuis un Buffer.\n *\n * @param data - Contenu du fichier\n * @param options - Nom et dossier parent\n * @returns Noeud cree, version et CID\n */\n async addFile(data: Buffer, options: { name: string; parentId?: number }): Promise<AddFileResult> {\n this.ensureStarted();\n\n const parentId = options.parentId ?? this.db.getRootNode().id;\n const plaintext = data;\n const hash = sha256(plaintext);\n\n let dataToStore: Buffer;\n let encParams: any;\n\n if (this.masterKey) {\n const encrypted = encryptFile(plaintext, this.masterKey);\n dataToStore = encrypted.ciphertext;\n encParams = {\n enc_algo: ENC_ALGO,\n enc_key_wrapped: encrypted.wrappedKey,\n enc_key_nonce: encrypted.keyNonce,\n enc_data_nonce: encrypted.dataNonce,\n };\n } else {\n dataToStore = plaintext;\n }\n\n const { cid, size } = await this.store.put(dataToStore);\n const nodeRow = this.db.createFileNode(parentId, options.name);\n const versionRow = this.db.addFileVersion(nodeRow.id, cid, size, hash, encParams);\n this.db.indexNode(nodeRow.id);\n\n // Si cloud actif, marquer pour sync\n if (this.store.isCloudActive()) {\n this.db.updateSyncStatus(versionRow.id, 'pending');\n this.db.addPendingOperation('pinata_upload', { versionId: versionRow.id, cid });\n }\n\n this.resetLockTimer();\n this.emitEvent('file:added', { nodeId: nodeRow.id, name: options.name, cid });\n\n return {\n node: toFileNode(nodeRow),\n version: toFileVersion(versionRow),\n cid,\n };\n }\n\n /**\n * Ajoute un fichier depuis un chemin sur le disque.\n *\n * @param filePath - Chemin absolu du fichier\n * @param options - Nom (optionnel, deduit du path) et dossier parent\n */\n async addFileFromPath(filePath: string, options?: { name?: string; parentId?: number }): Promise<AddFileResult> {\n const data = readFileSync(filePath);\n const name = options?.name ?? path.basename(filePath);\n return this.addFile(data, { name, parentId: options?.parentId });\n }\n\n /**\n * Recupere le contenu dechiffre d'un fichier.\n *\n * @param nodeId - ID du noeud fichier\n * @returns Contenu dechiffre + metadonnees\n */\n async getFile(nodeId: number): Promise<GetFileResult> {\n this.ensureStarted();\n\n const version = this.db.getCurrentVersion(nodeId);\n if (!version) throw new Error('No version found for this file');\n\n const nodeRow = this.db.getNode(nodeId);\n if (!nodeRow) throw new Error(`Node ${nodeId} not found`);\n\n // Recuperer depuis IPFS\n const rawData = await this.store.get(version.cid);\n\n let data: Buffer;\n if (version.enc_algo !== 'none' && version.enc_algo !== null) {\n if (!this.masterKey) {\n throw new Error('Vault is locked — unlock required to access encrypted files');\n }\n if (!version.enc_key_wrapped || !version.enc_key_nonce || !version.enc_data_nonce) {\n throw new Error('Missing encryption metadata for this file version');\n }\n data = decryptFile({\n ciphertext: rawData,\n wrappedKey: version.enc_key_wrapped,\n keyNonce: version.enc_key_nonce,\n dataNonce: version.enc_data_nonce,\n masterKey: this.masterKey,\n });\n } else {\n data = rawData;\n }\n\n this.resetLockTimer();\n\n return {\n node: toFileNode(nodeRow),\n data,\n cid: version.cid,\n size: data.length,\n };\n }\n\n /**\n * Liste le contenu d'un dossier.\n *\n * @param parentId - ID du dossier (undefined = racine)\n */\n listFolder(parentId?: number): FolderListing {\n this.ensureStarted();\n\n const nodeRow = parentId ? this.db.getNode(parentId)! : this.db.getRootNode();\n const children = this.db.listChildren(nodeRow.id);\n\n return {\n node: toFileNode(nodeRow),\n children: children.map(toFileNode),\n };\n }\n\n /**\n * Recupere un noeud par son ID.\n */\n getNode(nodeId: number): FileNode | null {\n this.ensureStarted();\n const row = this.db.getNode(nodeId);\n return row ? toFileNode(row) : null;\n }\n\n /**\n * Recupere le chemin complet d'un noeud (fil d'ariane).\n */\n getNodePath(nodeId: number): FileNode[] {\n this.ensureStarted();\n const rows = this.db.getNodePath(nodeId) as unknown as NodeRow[];\n return rows.map(toFileNode);\n }\n\n /**\n * Recupere le noeud racine.\n */\n getRootNode(): FileNode {\n this.ensureStarted();\n return toFileNode(this.db.getRootNode());\n }\n\n /**\n * Cree un nouveau dossier.\n *\n * @param name - Nom du dossier\n * @param parentId - ID du dossier parent (defaut: racine)\n */\n createFolder(name: string, parentId?: number): FileNode {\n this.ensureStarted();\n\n const pid = parentId ?? this.db.getRootNode().id;\n const folder = this.db.createFolder(pid, name);\n this.db.indexNode(folder.id);\n\n this.emitEvent('folder:created', { nodeId: folder.id, name });\n return toFileNode(folder);\n }\n\n /**\n * Renomme un noeud (fichier ou dossier).\n */\n rename(nodeId: number, newName: string): void {\n this.ensureStarted();\n this.db.rename(nodeId, newName);\n this.emitEvent('file:renamed', { nodeId, newName });\n }\n\n /**\n * Deplace un noeud vers un autre dossier.\n */\n move(nodeId: number, newParentId: number): void {\n this.ensureStarted();\n this.db.moveNode(nodeId, newParentId);\n this.emitEvent('file:moved', { nodeId, newParentId });\n }\n\n /**\n * Supprime un noeud (soft delete → corbeille).\n * Unpin de Pinata si le fichier y est synchronise.\n */\n async delete(nodeId: number): Promise<void> {\n this.ensureStarted();\n\n const pinataCids = this.db.getPinataCidsForNode(nodeId);\n this.db.softDelete(nodeId);\n\n // Unpin de Pinata en arriere-plan\n if (pinataCids.length > 0 && this.store.isCloudActive()) {\n const pinataStore = this.store.getPinataStore();\n if (pinataStore) {\n for (const cid of pinataCids) {\n try {\n await pinataStore.unpin(cid);\n } catch (err: any) {\n // Non bloquant\n }\n }\n }\n }\n\n this.emitEvent('file:deleted', { nodeId });\n }\n\n /**\n * Restaure un noeud depuis la corbeille.\n */\n restore(nodeId: number): void {\n this.ensureStarted();\n this.db.restore(nodeId);\n }\n\n /**\n * Liste les elements dans la corbeille.\n */\n listTrash(): FileNode[] {\n this.ensureStarted();\n return this.db.listTrash().map(toFileNode);\n }\n\n /**\n * Recupere l'historique des versions d'un fichier.\n */\n getVersions(nodeId: number): FileVersion[] {\n this.ensureStarted();\n return this.db.getFileVersions(nodeId).map(toFileVersion);\n }\n\n /**\n * Recherche dans le vault (FTS5).\n */\n search(query: string): FileNode[] {\n this.ensureStarted();\n const ftsResults = this.db.searchNodes(query);\n return ftsResults\n .map(r => this.db.getNode(r.rowid))\n .filter((row): row is NodeRow => row !== null)\n .map(toFileNode);\n }\n\n // ═══════════════════════════════════════════════════════════════\n // CLOUD SYNC\n // ═══════════════════════════════════════════════════════════════\n\n /**\n * Configure les credentials Pinata et teste la connexion.\n *\n * @param credentials - JWT ou apiKey + secretApiKey\n * @returns true si la connexion a reussi\n */\n async configureCloud(credentials: PinataCredentials): Promise<boolean> {\n this.ensureStarted();\n\n const config: PinataConfig = {\n apiKey: credentials.apiKey || '',\n secretApiKey: credentials.secretApiKey || '',\n jwt: credentials.jwt || undefined,\n gateway: credentials.gateway || undefined,\n };\n\n this.store.setProvider('pinata', config);\n const online = await this.store.isPinataOnline();\n\n if (online) {\n if (credentials.jwt) this.db.setSetting('pinata_jwt', credentials.jwt);\n if (credentials.apiKey) this.db.setSetting('pinata_api_key', credentials.apiKey);\n if (credentials.secretApiKey) this.db.setSetting('pinata_secret_key', credentials.secretApiKey);\n if (credentials.gateway) this.db.setSetting('pinata_gateway', credentials.gateway);\n this.db.setSetting('cloud.provider', 'pinata');\n this.emitEvent('cloud:configured', { provider: 'pinata' });\n } else {\n this.store.setProvider('local');\n }\n\n return online;\n }\n\n /**\n * Active ou desactive la synchronisation cloud.\n */\n toggleCloud(enabled: boolean): CloudProvider {\n this.ensureStarted();\n\n if (enabled) {\n const jwt = this.db.getSetting('pinata_jwt');\n const apiKey = this.db.getSetting('pinata_api_key');\n const secretKey = this.db.getSetting('pinata_secret_key');\n\n if (!jwt && !(apiKey && secretKey)) {\n throw new Error('Pinata credentials not configured — use configureCloud() first');\n }\n\n this.store.setProvider('pinata', {\n apiKey: apiKey || '',\n secretApiKey: secretKey || '',\n jwt: jwt || undefined,\n gateway: this.db.getSetting('pinata_gateway') || undefined,\n });\n this.db.setSetting('cloud.provider', 'pinata');\n } else {\n this.store.setProvider('local');\n this.db.setSetting('cloud.provider', 'local');\n }\n\n const provider = this.store.getProvider();\n this.emitEvent('cloud:toggled', { enabled, provider });\n return provider;\n }\n\n /**\n * Retourne la configuration cloud actuelle.\n */\n getCloudConfig(): CloudConfig {\n this.ensureStarted();\n return {\n provider: this.store.getProvider(),\n active: this.store.isCloudActive(),\n hasCredentials: !!(this.db.getSetting('pinata_jwt') || this.db.getSetting('pinata_api_key')),\n gateway: this.db.getSetting('pinata_gateway'),\n };\n }\n\n /**\n * Retourne le statut de synchronisation.\n */\n getSyncStatus(): SyncStatus {\n this.ensureStarted();\n\n const stats = this.db.getSyncStats();\n const counts: Record<string, number> = {};\n let total = 0;\n for (const s of stats) {\n counts[s.sync_status] = s.count;\n total += s.count;\n }\n\n return {\n total,\n synced: counts['synced'] || 0,\n pending: (counts['pending'] || 0) + (counts['syncing'] || 0),\n errors: counts['error'] || 0,\n provider: this.store.getProvider(),\n };\n }\n\n /**\n * Retourne le statut global du vault.\n */\n async getStatus(): Promise<DaemonStatus> {\n this.ensureStarted();\n return {\n kuboReady: this.kubo.ready,\n kuboPid: this.kubo.pid,\n rootNode: toFileNode(this.db.getRootNode()),\n ipfsOnline: await this.store.isKuboOnline(),\n vaultInitialized: this.db.isVaultInitialized(),\n vaultUnlocked: this.masterKey !== null,\n cloudProvider: this.store.getProvider(),\n cloudActive: this.store.isCloudActive(),\n };\n }\n\n // ═══════════════════════════════════════════════════════════════\n // INTERNALS\n // ═══════════════════════════════════════════════════════════════\n\n private loadVaultConfig(): CryptoVaultConfig {\n const raw = this.db.getVaultConfig();\n if (!raw) throw new Error('Vault not initialized');\n return {\n salt: raw.salt,\n encryptedMasterKey: raw.encryptedMasterKey,\n masterKeyNonce: raw.masterKeyNonce,\n pwEncryptedMasterKey: raw.pwEncryptedMasterKey,\n pwKeyNonce: raw.pwKeyNonce,\n mnemonicHash: raw.mnemonicHash,\n masterKeyHash: raw.masterKeyHash,\n };\n }\n\n private saveVaultConfig(config: CryptoVaultConfig): void {\n this.db.setVaultConfig({\n salt: config.salt,\n encryptedMasterKey: config.encryptedMasterKey,\n masterKeyNonce: config.masterKeyNonce,\n pwEncryptedMasterKey: config.pwEncryptedMasterKey,\n pwKeyNonce: config.pwKeyNonce,\n mnemonicHash: config.mnemonicHash,\n masterKeyHash: config.masterKeyHash,\n });\n }\n\n private restoreCloudConfig(): void {\n const provider = this.db.getSetting('cloud.provider');\n if (provider === 'pinata') {\n const apiKey = this.db.getSetting('pinata_api_key');\n const secretKey = this.db.getSetting('pinata_secret_key');\n const gateway = this.db.getSetting('pinata_gateway');\n const jwt = this.db.getSetting('pinata_jwt');\n\n if (jwt || (apiKey && secretKey)) {\n this.store.setProvider('pinata', {\n apiKey: apiKey || '',\n secretApiKey: secretKey || '',\n gateway: gateway || undefined,\n jwt: jwt || undefined,\n });\n }\n }\n }\n\n private resetLockTimer(): void {\n if (this.opts.disableAutoLock) return;\n\n if (this.lockTimer) {\n clearTimeout(this.lockTimer);\n }\n this.lockTimer = setTimeout(() => {\n this.lock();\n this.emitEvent('vault:locked', { reason: 'inactivity' });\n }, this.opts.autoLockMs);\n }\n\n private emitEvent(type: VaultEventType, data?: any): void {\n const event: VaultEvent = { type, data, timestamp: Date.now() };\n this.emit(type, event);\n this.emit('*', event); // wildcard pour ecouter tous les evenements\n }\n}\n","/**\n * KuboManager — Gère le cycle de vie du process Kubo (IPFS)\n *\n * Responsabilités :\n * - Init du repo IPFS si premier lancement\n * - Spawn du process Kubo avec config MerkleVault\n * - Heartbeat toutes les 30s sur /api/v0/id\n * - Restart auto sur crash (max 3 en 60s)\n * - Shutdown propre (SIGTERM + timeout 30s)\n */\n\nimport { spawn, ChildProcess } from 'node:child_process';\nimport { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport path from 'node:path';\nimport { EventEmitter } from 'node:events';\n\nexport interface KuboManagerConfig {\n kuboBinaryPath: string;\n repoPath: string;\n apiPort?: number;\n}\n\nexport class KuboManager extends EventEmitter {\n private process: ChildProcess | null = null;\n private config: KuboManagerConfig;\n private apiPort: number;\n private crashTimestamps: number[] = [];\n private heartbeatInterval: ReturnType<typeof setInterval> | null = null;\n private isShuttingDown = false;\n private _ready = false;\n\n constructor(config: KuboManagerConfig) {\n super();\n this.config = config;\n this.apiPort = config.apiPort ?? 5001;\n }\n\n get ready(): boolean {\n return this._ready;\n }\n\n get apiUrl(): string {\n return `http://127.0.0.1:${this.apiPort}`;\n }\n\n get pid(): number | null {\n return this.process?.pid ?? null;\n }\n\n /**\n * Initialise le repo IPFS si inexistant, puis lance Kubo\n */\n async start(): Promise<void> {\n // Créer le répertoire data si nécessaire\n if (!existsSync(this.config.repoPath)) {\n mkdirSync(this.config.repoPath, { recursive: true });\n }\n\n // Init repo si premier lancement\n if (!existsSync(path.join(this.config.repoPath, 'config'))) {\n await this.initRepo();\n }\n\n // Configurer Kubo pour MerkleVault\n await this.configureRepo();\n\n // Lancer le daemon\n await this.spawnDaemon();\n\n // Démarrer le heartbeat\n this.startHeartbeat();\n }\n\n /**\n * Arrêt propre : SIGTERM + timeout 30s\n */\n async stop(): Promise<void> {\n this.isShuttingDown = true;\n\n if (this.heartbeatInterval) {\n clearInterval(this.heartbeatInterval);\n this.heartbeatInterval = null;\n }\n\n if (!this.process) return;\n\n return new Promise((resolve) => {\n const timeout = setTimeout(() => {\n // Force kill après 30s\n this.process?.kill('SIGKILL');\n resolve();\n }, 30_000);\n\n this.process!.on('exit', () => {\n clearTimeout(timeout);\n this.process = null;\n this._ready = false;\n resolve();\n });\n\n this.process!.kill('SIGTERM');\n });\n }\n\n /**\n * ipfs init avec profil lowpower\n */\n private async initRepo(): Promise<void> {\n return new Promise((resolve, reject) => {\n const proc = spawn(this.config.kuboBinaryPath, ['init', '--profile=lowpower'], {\n env: { ...process.env, IPFS_PATH: this.config.repoPath },\n stdio: 'pipe',\n });\n\n let stderr = '';\n proc.stderr?.on('data', (d) => { stderr += d.toString(); });\n\n proc.on('exit', (code) => {\n if (code === 0) resolve();\n else reject(new Error(`ipfs init failed (code ${code}): ${stderr}`));\n });\n });\n }\n\n /**\n * Configure le repo pour MerkleVault :\n * - API sur 127.0.0.1:port\n * - Gateway désactivée\n * - Routing dhtclient\n * - Swarm limits bas\n */\n private async configureRepo(): Promise<void> {\n // String values (no --json flag)\n const stringConfigs: [string, string][] = [\n ['Addresses.API', `/ip4/127.0.0.1/tcp/${this.apiPort}`],\n ['Addresses.Gateway', ''],\n ['Routing.Type', 'dhtclient'],\n ['Reprovider.Interval', '0s'],\n ];\n\n // JSON values (with --json flag)\n const jsonConfigs: [string, string][] = [\n ['Swarm.ConnMgr.LowWater', '20'],\n ['Swarm.ConnMgr.HighWater', '40'],\n ];\n\n for (const [key, value] of stringConfigs) {\n await this.runIpfsCommand(['config', key, value]);\n }\n for (const [key, value] of jsonConfigs) {\n await this.runIpfsCommand(['config', key, value, '--json']);\n }\n }\n\n private async runIpfsCommand(args: string[]): Promise<string> {\n return new Promise((resolve, reject) => {\n const proc = spawn(this.config.kuboBinaryPath, args, {\n env: { ...process.env, IPFS_PATH: this.config.repoPath },\n stdio: 'pipe',\n });\n\n let stdout = '';\n let stderr = '';\n proc.stdout?.on('data', (d) => { stdout += d.toString(); });\n proc.stderr?.on('data', (d) => { stderr += d.toString(); });\n\n proc.on('exit', (code) => {\n if (code === 0) resolve(stdout.trim());\n else reject(new Error(`ipfs ${args[0]} failed: ${stderr}`));\n });\n });\n }\n\n /**\n * Lance le daemon Kubo en subprocess\n */\n private async spawnDaemon(): Promise<void> {\n return new Promise((resolve, reject) => {\n const proc = spawn(this.config.kuboBinaryPath, ['daemon', '--migrate'], {\n env: { ...process.env, IPFS_PATH: this.config.repoPath },\n stdio: 'pipe',\n });\n\n this.process = proc;\n\n let resolved = false;\n\n // Attendre que l'API soit prête\n proc.stdout?.on('data', (data: Buffer) => {\n const line = data.toString();\n this.emit('log', line.trim());\n\n if (!resolved && line.includes('Daemon is ready')) {\n resolved = true;\n this._ready = true;\n this.emit('ready');\n resolve();\n }\n });\n\n proc.stderr?.on('data', (data: Buffer) => {\n this.emit('log', `[stderr] ${data.toString().trim()}`);\n });\n\n proc.on('exit', (code, signal) => {\n this._ready = false;\n this.process = null;\n\n if (!resolved) {\n reject(new Error(`Kubo exited before ready (code=${code}, signal=${signal})`));\n return;\n }\n\n if (!this.isShuttingDown) {\n this.emit('crash', { code, signal });\n this.handleCrash();\n }\n });\n\n proc.on('error', (err) => {\n if (!resolved) reject(err);\n });\n\n // Timeout si Kubo ne démarre pas en 30s\n setTimeout(() => {\n if (!resolved) {\n resolved = true;\n proc.kill('SIGKILL');\n reject(new Error('Kubo startup timeout (30s)'));\n }\n }, 30_000);\n });\n }\n\n /**\n * Restart auto avec protection anti-boucle (max 3 en 60s)\n */\n private async handleCrash(): Promise<void> {\n const now = Date.now();\n this.crashTimestamps.push(now);\n // Garder seulement les crashes des 60 dernières secondes\n this.crashTimestamps = this.crashTimestamps.filter(t => now - t < 60_000);\n\n if (this.crashTimestamps.length > 3) {\n this.emit('fatal', 'Kubo crashed 3+ times in 60s, giving up');\n return;\n }\n\n this.emit('restarting');\n\n // Attendre 2s avant restart\n await new Promise(r => setTimeout(r, 2000));\n\n if (!this.isShuttingDown) {\n try {\n await this.spawnDaemon();\n this.emit('ready');\n } catch (err) {\n this.emit('fatal', `Kubo restart failed: ${err}`);\n }\n }\n }\n\n /**\n * Heartbeat toutes les 30s sur /api/v0/id\n */\n private startHeartbeat(): void {\n this.heartbeatInterval = setInterval(async () => {\n if (!this._ready || this.isShuttingDown) return;\n\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 5000);\n const res = await fetch(`${this.apiUrl}/api/v0/id`, {\n method: 'POST',\n signal: controller.signal,\n });\n clearTimeout(timeout);\n\n if (!res.ok) {\n this.emit('unhealthy', `Kubo returned ${res.status}`);\n }\n } catch {\n this.emit('unhealthy', 'Kubo heartbeat failed');\n }\n }, 30_000);\n }\n}\n","/**\n * Database — Couche SQLite pour MerkleVault\n *\n * Schéma conforme au PRD V2 §3.2\n * Sprint 1 : better-sqlite3 sans SQLCipher (chiffrement DB = Sprint 2)\n *\n * Tables : nodes, file_versions, cid_refcount, pending_operations,\n * settings, gc_audit_log, fts_nodes, anchor_jobs (réservée V2)\n */\n\nimport BetterSqlite3 from 'better-sqlite3';\nimport path from 'node:path';\nimport { mkdirSync, existsSync } from 'node:fs';\n\nexport interface NodeRow {\n id: number;\n parent_id: number | null;\n name: string;\n kind: 'file' | 'folder';\n created_at: number;\n modified_at: number;\n deleted_at: number | null;\n current_version_id: number | null;\n}\n\nexport interface FileVersionRow {\n id: number;\n node_id: number;\n cid: string;\n size_bytes: number;\n sha256_plain: string | null;\n created_at: number;\n enc_algo: string;\n enc_key_wrapped: Buffer | null;\n enc_key_nonce: Buffer | null;\n enc_data_nonce: Buffer | null;\n is_pinned: number;\n // Sprint 3 — Cloud sync\n provider: 'local' | 'pinata';\n pinata_cid: string | null;\n sync_status: 'none' | 'pending' | 'syncing' | 'synced' | 'error';\n}\n\nexport interface VaultConfigRow {\n key: string;\n value: string;\n updated_at: number;\n}\n\nexport class Database {\n private db: BetterSqlite3.Database;\n\n constructor(dataDir: string) {\n if (!existsSync(dataDir)) {\n mkdirSync(dataDir, { recursive: true });\n }\n\n const dbPath = path.join(dataDir, 'merklevault.db');\n this.db = new BetterSqlite3(dbPath);\n\n // Configuration SQLite obligatoire (PRD §3.2)\n this.db.pragma('journal_mode = WAL');\n this.db.pragma('synchronous = NORMAL');\n this.db.pragma('foreign_keys = ON');\n this.db.pragma('temp_store = MEMORY');\n this.db.pragma('mmap_size = 268435456'); // 256 MB\n this.db.pragma('cache_size = -64000'); // 64 MB\n this.db.pragma('busy_timeout = 5000'); // 5 secondes\n\n this.migrate();\n }\n\n /**\n * Applique le schéma initial (migration 001)\n */\n private migrate(): void {\n // Vérifier la version actuelle\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n );\n `);\n\n const version = this.getSetting('schema_version');\n\n if (!version) {\n this.migration001();\n this.setSetting('schema_version', '1');\n }\n\n const currentVersion = parseInt(this.getSetting('schema_version') ?? '0', 10);\n if (currentVersion < 2) {\n this.migration002();\n this.setSetting('schema_version', '2');\n }\n if (parseInt(this.getSetting('schema_version') ?? '0', 10) < 3) {\n this.migration003();\n this.setSetting('schema_version', '3');\n }\n }\n\n private migration001(): void {\n this.db.exec(`\n -- Table nodes : structure logique du filesystem\n CREATE TABLE IF NOT EXISTS nodes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n parent_id INTEGER REFERENCES nodes(id) ON DELETE RESTRICT,\n name TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('file', 'folder')),\n created_at INTEGER NOT NULL,\n modified_at INTEGER NOT NULL,\n deleted_at INTEGER,\n current_version_id INTEGER REFERENCES file_versions(id),\n UNIQUE (parent_id, name, deleted_at)\n );\n CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id) WHERE deleted_at IS NULL;\n CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name) WHERE deleted_at IS NULL;\n\n -- Table file_versions : versions des fichiers\n CREATE TABLE IF NOT EXISTS file_versions (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n node_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE RESTRICT,\n cid TEXT NOT NULL,\n size_bytes INTEGER NOT NULL,\n sha256_plain TEXT,\n created_at INTEGER NOT NULL,\n enc_algo TEXT NOT NULL DEFAULT 'none',\n enc_key_wrapped BLOB,\n enc_key_nonce BLOB,\n enc_data_nonce BLOB,\n is_pinned INTEGER NOT NULL DEFAULT 1,\n UNIQUE (node_id, cid)\n );\n CREATE INDEX IF NOT EXISTS idx_versions_node ON file_versions(node_id);\n CREATE INDEX IF NOT EXISTS idx_versions_cid ON file_versions(cid);\n CREATE INDEX IF NOT EXISTS idx_versions_created ON file_versions(created_at);\n\n -- Table cid_refcount : compteur de références par CID\n CREATE TABLE IF NOT EXISTS cid_refcount (\n cid TEXT PRIMARY KEY,\n ref_count INTEGER NOT NULL DEFAULT 0,\n first_seen_at INTEGER NOT NULL,\n last_seen_at INTEGER NOT NULL\n );\n\n -- Triggers pour maintenir le refcount automatiquement\n CREATE TRIGGER IF NOT EXISTS trg_refcount_insert\n AFTER INSERT ON file_versions\n BEGIN\n INSERT INTO cid_refcount (cid, ref_count, first_seen_at, last_seen_at)\n VALUES (NEW.cid, 1, NEW.created_at, NEW.created_at)\n ON CONFLICT(cid) DO UPDATE SET\n ref_count = ref_count + 1,\n last_seen_at = NEW.created_at;\n END;\n\n CREATE TRIGGER IF NOT EXISTS trg_refcount_delete\n AFTER DELETE ON file_versions\n BEGIN\n UPDATE cid_refcount SET ref_count = ref_count - 1 WHERE cid = OLD.cid;\n END;\n\n -- Table pending_operations : file d'attente des opérations async\n CREATE TABLE IF NOT EXISTS pending_operations (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n op_type TEXT NOT NULL,\n op_payload TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n attempts INTEGER NOT NULL DEFAULT 0,\n last_error TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n scheduled_for INTEGER\n );\n CREATE INDEX IF NOT EXISTS idx_ops_status ON pending_operations(status, scheduled_for);\n\n -- Table anchor_jobs : réservée V2 (ancrage blockchain)\n CREATE TABLE IF NOT EXISTS anchor_jobs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n merkle_root TEXT NOT NULL,\n file_cids TEXT NOT NULL,\n anchor_target TEXT NOT NULL,\n tx_hash TEXT,\n block_height INTEGER,\n anchored_at INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n proof_blob BLOB\n );\n\n -- Recherche plein texte sur noms\n CREATE VIRTUAL TABLE IF NOT EXISTS fts_nodes USING fts5(\n name, path, content='', tokenize='unicode61'\n );\n\n -- Journal GC\n CREATE TABLE IF NOT EXISTS gc_audit_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n op_type TEXT NOT NULL,\n target TEXT,\n details TEXT,\n executed_at INTEGER NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_audit_executed ON gc_audit_log(executed_at);\n\n -- Créer le noeud racine \"Accueil\"\n INSERT INTO nodes (parent_id, name, kind, created_at, modified_at)\n VALUES (NULL, 'Accueil', 'folder', unixepoch(), unixepoch());\n `);\n }\n\n /**\n * Migration 002 — Sprint 2 : table vault_config pour le chiffrement\n */\n private migration002(): void {\n this.db.exec(`\n -- Table vault_config : configuration cryptographique du vault\n CREATE TABLE IF NOT EXISTS vault_config (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n );\n `);\n }\n\n /**\n * Migration 003 — Sprint 3 : colonnes cloud sync + settings Pinata\n */\n private migration003(): void {\n this.db.exec(`\n -- Ajouter les colonnes cloud à file_versions\n ALTER TABLE file_versions ADD COLUMN provider TEXT NOT NULL DEFAULT 'local';\n ALTER TABLE file_versions ADD COLUMN pinata_cid TEXT;\n ALTER TABLE file_versions ADD COLUMN sync_status TEXT NOT NULL DEFAULT 'none';\n\n -- Index pour requêtes de sync\n CREATE INDEX IF NOT EXISTS idx_fv_sync_status ON file_versions(sync_status) WHERE sync_status != 'none';\n CREATE INDEX IF NOT EXISTS idx_fv_provider ON file_versions(provider);\n `);\n console.log('[database] Migration 003 applied — cloud sync columns added');\n }\n\n // ─── Cloud Sync (Sprint 3) ───────────────────────────────────\n\n /**\n * Met à jour le statut de sync d'une version de fichier\n */\n updateSyncStatus(versionId: number, status: 'none' | 'pending' | 'syncing' | 'synced' | 'error', pinataCid?: string): void {\n if (pinataCid) {\n this.db.prepare(\n 'UPDATE file_versions SET sync_status = ?, provider = ?, pinata_cid = ? WHERE id = ?'\n ).run(status, 'pinata', pinataCid, versionId);\n } else {\n this.db.prepare(\n 'UPDATE file_versions SET sync_status = ? WHERE id = ?'\n ).run(status, versionId);\n }\n }\n\n /**\n * Récupère les fichiers en attente de synchronisation\n */\n getPendingSyncFiles(): FileVersionRow[] {\n return this.db.prepare(\n 'SELECT fv.*, n.name FROM file_versions fv JOIN nodes n ON n.id = fv.node_id WHERE fv.sync_status = ? ORDER BY fv.created_at ASC'\n ).all('pending') as FileVersionRow[];\n }\n\n /**\n * Ajoute une opération à la queue de sync\n */\n addPendingOperation(opType: string, payload: Record<string, any>): number {\n const now = Math.floor(Date.now() / 1000);\n const info = this.db.prepare(`\n INSERT INTO pending_operations (op_type, op_payload, status, created_at, updated_at)\n VALUES (?, ?, 'pending', ?, ?)\n `).run(opType, JSON.stringify(payload), now, now);\n return Number(info.lastInsertRowid);\n }\n\n /**\n * Récupère les opérations en attente\n */\n getPendingOperations(): Array<{ id: number; op_type: string; op_payload: string; attempts: number }> {\n return this.db.prepare(\n \"SELECT id, op_type, op_payload, attempts FROM pending_operations WHERE status = 'pending' ORDER BY created_at ASC\"\n ).all() as any[];\n }\n\n /**\n * Met à jour le statut d'une opération\n */\n updateOperationStatus(opId: number, status: 'pending' | 'completed' | 'failed', error?: string): void {\n const now = Math.floor(Date.now() / 1000);\n this.db.prepare(\n 'UPDATE pending_operations SET status = ?, last_error = ?, attempts = attempts + 1, updated_at = ? WHERE id = ?'\n ).run(status, error ?? null, now, opId);\n }\n\n /**\n * Supprime les opérations terminées\n */\n clearCompletedOperations(): void {\n this.db.prepare(\"DELETE FROM pending_operations WHERE status = 'completed'\").run();\n }\n\n /**\n * Récupère les CIDs Pinata d'un node et ses descendants (pour unpin cloud)\n */\n getPinataCidsForNode(nodeId: number): string[] {\n const rows = this.db.prepare(`\n WITH RECURSIVE descendants(id) AS (\n SELECT id FROM nodes WHERE id = ?\n UNION ALL\n SELECT n.id FROM nodes n JOIN descendants d ON n.parent_id = d.id\n )\n SELECT DISTINCT fv.pinata_cid\n FROM file_versions fv\n JOIN descendants d ON fv.node_id = d.id\n WHERE fv.pinata_cid IS NOT NULL AND fv.sync_status = 'synced'\n `).all(nodeId) as Array<{ pinata_cid: string }>;\n return rows.map(r => r.pinata_cid);\n }\n\n // ─── Vault Config (Sprint 2) ──────────────────────────────────\n\n getVaultConfig(): Record<string, string> | null {\n const rows = this.db.prepare('SELECT key, value FROM vault_config').all() as VaultConfigRow[];\n if (rows.length === 0) return null;\n const config: Record<string, string> = {};\n for (const row of rows) {\n config[row.key] = row.value;\n }\n return config;\n }\n\n setVaultConfig(config: Record<string, string>): void {\n const stmt = this.db.prepare(`\n INSERT INTO vault_config (key, value, updated_at) VALUES (?, ?, unixepoch())\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at\n `);\n const transaction = this.db.transaction(() => {\n for (const [key, value] of Object.entries(config)) {\n stmt.run(key, value);\n }\n });\n transaction();\n }\n\n isVaultInitialized(): boolean {\n const row = this.db.prepare('SELECT COUNT(*) as count FROM vault_config').get() as { count: number };\n return row.count > 0;\n }\n\n // ─── Settings ─────────────────────────────────────────────────\n\n getSetting(key: string): string | null {\n const row = this.db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;\n return row?.value ?? null;\n }\n\n setSetting(key: string, value: string): void {\n this.db.prepare(`\n INSERT INTO settings (key, value, updated_at) VALUES (?, ?, unixepoch())\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at\n `).run(key, value);\n }\n\n // ─── Nodes (fichiers & dossiers) ──────────────────────────────\n\n getRootNode(): NodeRow {\n return this.db.prepare('SELECT * FROM nodes WHERE parent_id IS NULL AND deleted_at IS NULL').get() as NodeRow;\n }\n\n getNode(id: number): NodeRow | null {\n return (this.db.prepare('SELECT * FROM nodes WHERE id = ?').get(id) as NodeRow) ?? null;\n }\n\n listChildren(parentId: number): NodeRow[] {\n return this.db.prepare(\n 'SELECT * FROM nodes WHERE parent_id = ? AND deleted_at IS NULL ORDER BY kind DESC, name ASC'\n ).all(parentId) as NodeRow[];\n }\n\n createFolder(parentId: number, name: string): NodeRow {\n const now = Math.floor(Date.now() / 1000);\n const info = this.db.prepare(\n 'INSERT INTO nodes (parent_id, name, kind, created_at, modified_at) VALUES (?, ?, ?, ?, ?)'\n ).run(parentId, name, 'folder', now, now);\n return this.getNode(Number(info.lastInsertRowid))!;\n }\n\n createFileNode(parentId: number, name: string): NodeRow {\n const now = Math.floor(Date.now() / 1000);\n const info = this.db.prepare(\n 'INSERT INTO nodes (parent_id, name, kind, created_at, modified_at) VALUES (?, ?, ?, ?, ?)'\n ).run(parentId, name, 'file', now, now);\n return this.getNode(Number(info.lastInsertRowid))!;\n }\n\n rename(nodeId: number, newName: string): void {\n const node = this.getNode(nodeId);\n const oldName = node?.name;\n const oldPath = node ? this.getNodePath(nodeId) : undefined;\n const now = Math.floor(Date.now() / 1000);\n this.db.prepare('UPDATE nodes SET name = ?, modified_at = ? WHERE id = ?').run(newName, now, nodeId);\n // Mettre à jour l'index FTS\n this.reindexNode(nodeId, oldName, oldPath);\n }\n\n moveNode(nodeId: number, newParentId: number): void {\n const now = Math.floor(Date.now() / 1000);\n this.db.prepare('UPDATE nodes SET parent_id = ?, modified_at = ? WHERE id = ?').run(newParentId, now, nodeId);\n }\n\n softDelete(nodeId: number): void {\n const now = Math.floor(Date.now() / 1000);\n // Soft delete cascade : marque le noeud et tous ses descendants\n const deleteRecursive = this.db.prepare(`\n WITH RECURSIVE descendants(id) AS (\n SELECT id FROM nodes WHERE id = ?\n UNION ALL\n SELECT n.id FROM nodes n JOIN descendants d ON n.parent_id = d.id WHERE n.deleted_at IS NULL\n )\n UPDATE nodes SET deleted_at = ? WHERE id IN (SELECT id FROM descendants)\n `);\n deleteRecursive.run(nodeId, now);\n }\n\n restore(nodeId: number): void {\n // Restauration cascade\n const restoreRecursive = this.db.prepare(`\n WITH RECURSIVE descendants(id) AS (\n SELECT id FROM nodes WHERE id = ?\n UNION ALL\n SELECT n.id FROM nodes n JOIN descendants d ON n.parent_id = d.id WHERE n.deleted_at IS NOT NULL\n )\n UPDATE nodes SET deleted_at = NULL WHERE id IN (SELECT id FROM descendants)\n `);\n restoreRecursive.run(nodeId);\n }\n\n listTrash(): NodeRow[] {\n return this.db.prepare(\n 'SELECT * FROM nodes WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC'\n ).all() as NodeRow[];\n }\n\n // ─── File versions ────────────────────────────────────────────\n\n addFileVersion(\n nodeId: number,\n cid: string,\n sizeBytes: number,\n sha256Plain?: string,\n encParams?: {\n enc_algo: string;\n enc_key_wrapped: Buffer;\n enc_key_nonce: Buffer;\n enc_data_nonce: Buffer;\n }\n ): FileVersionRow {\n const now = Math.floor(Date.now() / 1000);\n const info = this.db.prepare(`\n INSERT INTO file_versions (node_id, cid, size_bytes, sha256_plain, created_at, enc_algo, enc_key_wrapped, enc_key_nonce, enc_data_nonce)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)\n `).run(\n nodeId,\n cid,\n sizeBytes,\n sha256Plain ?? null,\n now,\n encParams?.enc_algo ?? 'none',\n encParams?.enc_key_wrapped ?? null,\n encParams?.enc_key_nonce ?? null,\n encParams?.enc_data_nonce ?? null\n );\n\n const versionId = Number(info.lastInsertRowid);\n\n // Mettre à jour current_version_id sur le node\n this.db.prepare('UPDATE nodes SET current_version_id = ?, modified_at = ? WHERE id = ?')\n .run(versionId, now, nodeId);\n\n return this.db.prepare('SELECT * FROM file_versions WHERE id = ?').get(versionId) as FileVersionRow;\n }\n\n getFileVersions(nodeId: number): FileVersionRow[] {\n return this.db.prepare(\n 'SELECT * FROM file_versions WHERE node_id = ? ORDER BY created_at DESC'\n ).all(nodeId) as FileVersionRow[];\n }\n\n getCurrentVersion(nodeId: number): FileVersionRow | null {\n const node = this.getNode(nodeId);\n if (!node?.current_version_id) return null;\n return (this.db.prepare('SELECT * FROM file_versions WHERE id = ?').get(node.current_version_id) as FileVersionRow) ?? null;\n }\n\n // ─── CID refcount ─────────────────────────────────────────────\n\n getOrphanCids(): { cid: string }[] {\n return this.db.prepare('SELECT cid FROM cid_refcount WHERE ref_count <= 0').all() as { cid: string }[];\n }\n\n // ─── Paths ────────────────────────────────────────────────────\n\n getNodePath(nodeId: number): string {\n const parts: string[] = [];\n let current = this.getNode(nodeId);\n while (current) {\n parts.unshift(current.name);\n current = current.parent_id ? this.getNode(current.parent_id) : null;\n }\n return '/' + parts.join('/');\n }\n\n // ─── GC Audit ─────────────────────────────────────────────────\n\n logGcOp(opType: string, target: string | null, details: string | null): void {\n const now = Math.floor(Date.now() / 1000);\n this.db.prepare('INSERT INTO gc_audit_log (op_type, target, details, executed_at) VALUES (?, ?, ?, ?)')\n .run(opType, target, details, now);\n }\n\n // ─── FTS ──────────────────────────────────────────────────────\n\n indexNode(nodeId: number): void {\n const node = this.getNode(nodeId);\n if (!node) return;\n const nodePath = this.getNodePath(nodeId);\n this.db.prepare('INSERT INTO fts_nodes(rowid, name, path) VALUES (?, ?, ?)').run(nodeId, node.name, nodePath);\n }\n\n reindexNode(nodeId: number, oldName?: string, oldPath?: string): void {\n const node = this.getNode(nodeId);\n if (!node) return;\n const nodePath = this.getNodePath(nodeId);\n // FTS5 contentless : delete requires original values, then re-insert\n if (oldName !== undefined && oldPath !== undefined) {\n this.db.prepare(\"INSERT INTO fts_nodes(fts_nodes, rowid, name, path) VALUES ('delete', ?, ?, ?)\").run(nodeId, oldName, oldPath);\n }\n this.db.prepare('INSERT INTO fts_nodes(rowid, name, path) VALUES (?, ?, ?)').run(nodeId, node.name, nodePath);\n }\n\n searchNodes(query: string): { rowid: number; name: string; path: string }[] {\n return this.db.prepare(\n 'SELECT rowid, name, path FROM fts_nodes WHERE fts_nodes MATCH ? ORDER BY rank'\n ).all(query) as any[];\n }\n\n // ─── Sync Stats (Sprint 3) ─────────────────────────────────────\n\n getSyncStats(): Array<{ sync_status: string; count: number }> {\n return this.db.prepare(\n 'SELECT sync_status, COUNT(*) as count FROM file_versions GROUP BY sync_status'\n ).all() as Array<{ sync_status: string; count: number }>;\n }\n\n // ─── Cleanup ──────────────────────────────────────────────────\n\n close(): void {\n this.db.close();\n }\n}\n","/**\n * ContentStore — Abstraction sur le stockage content-addressed\n *\n * PRD §3.1 : \"Le daemon Node expose une interface ContentStore (put, get, pin, unpin, gc)\n * utilisée par la logique métier. Kubo est une implémentation derrière cette interface,\n * remplaçable sans modification du reste du code.\"\n *\n * Sprint 1 : pas de chiffrement, le contenu est stocké en clair dans Kubo.\n */\n\nimport { createHash } from 'node:crypto';\n\nexport interface ContentStore {\n put(data: Buffer): Promise<{ cid: string; size: number }>;\n get(cid: string): Promise<Buffer>;\n pin(cid: string): Promise<void>;\n unpin(cid: string): Promise<void>;\n gc(): Promise<void>;\n isOnline(): Promise<boolean>;\n}\n\n/**\n * Implémentation Kubo via l'API HTTP native (sans kubo-rpc-client pour simplifier Sprint 1)\n */\nexport class KuboContentStore implements ContentStore {\n private apiUrl: string;\n\n constructor(apiUrl: string) {\n this.apiUrl = apiUrl;\n }\n\n /**\n * Ajoute du contenu dans IPFS\n * Équivalent : ipfs add --pin --cid-version=1 --raw-leaves\n */\n async put(data: Buffer): Promise<{ cid: string; size: number }> {\n const boundary = '----MerkleVaultBoundary' + Date.now();\n const header = `--${boundary}\\r\\nContent-Disposition: form-data; name=\"file\"; filename=\"data\"\\r\\nContent-Type: application/octet-stream\\r\\n\\r\\n`;\n const footer = `\\r\\n--${boundary}--\\r\\n`;\n\n const body = Buffer.concat([\n Buffer.from(header),\n data,\n Buffer.from(footer),\n ]);\n\n const res = await fetch(\n `${this.apiUrl}/api/v0/add?cid-version=1&raw-leaves=true&pin=true`,\n {\n method: 'POST',\n headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` },\n body,\n }\n );\n\n if (!res.ok) {\n throw new Error(`IPFS add failed: ${res.status} ${await res.text()}`);\n }\n\n const result = JSON.parse(await res.text());\n return { cid: result.Hash, size: parseInt(result.Size, 10) };\n }\n\n /**\n * Récupère du contenu depuis IPFS\n * Équivalent : ipfs cat <cid>\n */\n async get(cid: string): Promise<Buffer> {\n const res = await fetch(`${this.apiUrl}/api/v0/cat?arg=${cid}`, {\n method: 'POST',\n });\n\n if (!res.ok) {\n throw new Error(`IPFS cat failed: ${res.status} ${await res.text()}`);\n }\n\n return Buffer.from(await res.arrayBuffer());\n }\n\n /**\n * Pin un CID\n */\n async pin(cid: string): Promise<void> {\n const res = await fetch(`${this.apiUrl}/api/v0/pin/add?arg=${cid}`, {\n method: 'POST',\n });\n if (!res.ok) {\n throw new Error(`IPFS pin failed: ${res.status} ${await res.text()}`);\n }\n }\n\n /**\n * Unpin un CID\n */\n async unpin(cid: string): Promise<void> {\n const res = await fetch(`${this.apiUrl}/api/v0/pin/rm?arg=${cid}`, {\n method: 'POST',\n });\n if (!res.ok) {\n const text = await res.text();\n // Ignorer si déjà unpinné\n if (!text.includes('not pinned')) {\n throw new Error(`IPFS unpin failed: ${res.status} ${text}`);\n }\n }\n }\n\n /**\n * Déclenche le garbage collector Kubo\n */\n async gc(): Promise<void> {\n const res = await fetch(`${this.apiUrl}/api/v0/repo/gc`, {\n method: 'POST',\n });\n if (!res.ok) {\n throw new Error(`IPFS gc failed: ${res.status} ${await res.text()}`);\n }\n // Consommer la réponse (stream de CIDs supprimés)\n await res.text();\n }\n\n /**\n * Vérifie si Kubo répond\n */\n async isOnline(): Promise<boolean> {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 3000);\n const res = await fetch(`${this.apiUrl}/api/v0/id`, {\n method: 'POST',\n signal: controller.signal,\n });\n clearTimeout(timeout);\n return res.ok;\n } catch {\n return false;\n }\n }\n}\n\n/**\n * Calcule le SHA-256 d'un buffer (pour le champ sha256_plain)\n */\nexport function sha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n","/**\n * PinataContentStore — Adapter Pinata qui implémente l'interface ContentStore\n *\n * Sprint 3 : Cloud IPFS via Pinata\n * Tous les fichiers uploadés sont déjà chiffrés côté client (Sprint 2).\n * Pinata ne voit que des blobs opaques.\n *\n * API Reference : https://docs.pinata.cloud/api-reference\n */\n\nimport { ContentStore } from './content-store.js';\n\nexport interface PinataConfig {\n apiKey: string;\n secretApiKey: string;\n gateway?: string; // ex: https://mon-gateway.mypinata.cloud\n jwt?: string; // Alternative auth via JWT\n}\n\nexport class PinataContentStore implements ContentStore {\n private baseUrl = 'https://api.pinata.cloud';\n private gatewayUrl: string;\n private headers: Record<string, string>;\n\n constructor(config: PinataConfig) {\n this.gatewayUrl = config.gateway || 'https://gateway.pinata.cloud';\n\n // Authentification : JWT prioritaire, sinon API key pair\n if (config.jwt) {\n this.headers = {\n 'Authorization': `Bearer ${config.jwt}`,\n };\n } else {\n this.headers = {\n 'pinata_api_key': config.apiKey,\n 'pinata_secret_api_key': config.secretApiKey,\n };\n }\n }\n\n /**\n * Upload un fichier chiffré vers Pinata et le pin automatiquement\n * Endpoint : POST /pinning/pinFileToIPFS\n */\n async put(data: Buffer): Promise<{ cid: string; size: number }> {\n const boundary = '----MerkleVaultPinata' + Date.now();\n const metadata = JSON.stringify({\n name: `merklevault-${Date.now()}`,\n keyvalues: { app: 'merklevault', encrypted: 'true' },\n });\n\n // Construire le multipart manuellement (pas de dépendance form-data)\n const parts: Buffer[] = [];\n\n // Part 1 : metadata\n parts.push(Buffer.from(\n `--${boundary}\\r\\n` +\n `Content-Disposition: form-data; name=\"pinataMetadata\"\\r\\n` +\n `Content-Type: application/json\\r\\n\\r\\n` +\n metadata + '\\r\\n'\n ));\n\n // Part 2 : fichier\n parts.push(Buffer.from(\n `--${boundary}\\r\\n` +\n `Content-Disposition: form-data; name=\"file\"; filename=\"encrypted-blob\"\\r\\n` +\n `Content-Type: application/octet-stream\\r\\n\\r\\n`\n ));\n parts.push(data);\n parts.push(Buffer.from(`\\r\\n--${boundary}--\\r\\n`));\n\n const body = Buffer.concat(parts);\n\n const res = await fetch(`${this.baseUrl}/pinning/pinFileToIPFS`, {\n method: 'POST',\n headers: {\n ...this.headers,\n 'Content-Type': `multipart/form-data; boundary=${boundary}`,\n },\n body,\n });\n\n if (!res.ok) {\n const errText = await res.text();\n throw new Error(`Pinata upload failed: ${res.status} ${errText}`);\n }\n\n const result = await res.json() as any;\n return {\n cid: result.IpfsHash,\n size: result.PinSize || data.length,\n };\n }\n\n /**\n * Récupère un fichier depuis le gateway Pinata\n * Endpoint : GET {gateway}/ipfs/{CID}\n */\n async get(cid: string): Promise<Buffer> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 60000); // 60s timeout\n\n try {\n const res = await fetch(`${this.gatewayUrl}/ipfs/${cid}`, {\n method: 'GET',\n signal: controller.signal,\n });\n\n if (!res.ok) {\n throw new Error(`Pinata get failed: ${res.status} ${await res.text()}`);\n }\n\n return Buffer.from(await res.arrayBuffer());\n } finally {\n clearTimeout(timeout);\n }\n }\n\n /**\n * Pin un CID déjà présent sur le réseau IPFS\n * Endpoint : POST /pinning/pinByHash\n */\n async pin(cid: string): Promise<void> {\n const res = await fetch(`${this.baseUrl}/pinning/pinByHash`, {\n method: 'POST',\n headers: {\n ...this.headers,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ hashToPin: cid }),\n });\n\n if (!res.ok) {\n const errText = await res.text();\n throw new Error(`Pinata pin failed: ${res.status} ${errText}`);\n }\n }\n\n /**\n * Supprime le pin d'un CID sur Pinata\n * Endpoint : DELETE /pinning/unpin/{CID}\n */\n async unpin(cid: string): Promise<void> {\n const res = await fetch(`${this.baseUrl}/pinning/unpin/${cid}`, {\n method: 'DELETE',\n headers: this.headers,\n });\n\n if (!res.ok) {\n const errText = await res.text();\n // Ignorer si déjà unpinné\n if (res.status !== 404) {\n throw new Error(`Pinata unpin failed: ${res.status} ${errText}`);\n }\n }\n }\n\n /**\n * Pas de GC côté Pinata — opération no-op\n */\n async gc(): Promise<void> {\n // Pinata gère son propre stockage, rien à faire\n }\n\n /**\n * Vérifie la connectivité et l'authentification avec Pinata\n * Endpoint : GET /data/testAuthentication\n */\n async isOnline(): Promise<boolean> {\n try {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), 5000);\n\n const res = await fetch(`${this.baseUrl}/data/testAuthentication`, {\n method: 'GET',\n headers: this.headers,\n signal: controller.signal,\n });\n\n clearTimeout(timeout);\n return res.ok;\n } catch {\n return false;\n }\n }\n\n /**\n * Vérifie si un CID est déjà pinné sur Pinata\n * Endpoint : GET /data/pinList?cid={CID}\n */\n async stat(cid: string): Promise<{ pinned: boolean; size: number } | null> {\n try {\n const res = await fetch(\n `${this.baseUrl}/data/pinList?status=pinned&hashContains=${cid}`,\n { method: 'GET', headers: this.headers }\n );\n\n if (!res.ok) return null;\n\n const result = await res.json() as any;\n if (result.rows && result.rows.length > 0) {\n return {\n pinned: true,\n size: result.rows[0].size || 0,\n };\n }\n return { pinned: false, size: 0 };\n } catch {\n return null;\n }\n }\n\n /**\n * Met à jour la configuration (clé API, gateway)\n */\n updateConfig(config: Partial<PinataConfig>): void {\n if (config.gateway) {\n this.gatewayUrl = config.gateway;\n }\n if (config.jwt) {\n this.headers = { 'Authorization': `Bearer ${config.jwt}` };\n } else if (config.apiKey && config.secretApiKey) {\n this.headers = {\n 'pinata_api_key': config.apiKey,\n 'pinata_secret_api_key': config.secretApiKey,\n };\n }\n }\n}\n","/**\n * StorageRouter — Routeur intelligent entre backends de stockage\n *\n * Sprint 3 : implémente ContentStore et redirige vers Kubo (local) ou Pinata (cloud)\n * selon la configuration dans settings.cloud.provider\n *\n * Architecture :\n * daemon.ts → StorageRouter → KuboContentStore (local)\n * → PinataContentStore (cloud)\n *\n * Le mode local n'est jamais affecté. Le StorageRouter est un proxy transparent\n * qui ajoute la couche cloud sans modifier le comportement existant.\n */\n\nimport { ContentStore } from './content-store.js';\nimport { PinataContentStore, PinataConfig } from './pinata-content-store.js';\n\nexport type CloudProvider = 'local' | 'pinata';\n\nexport interface StorageRouterConfig {\n provider: CloudProvider;\n pinataConfig?: PinataConfig;\n}\n\nexport class StorageRouter implements ContentStore {\n private kuboStore: ContentStore;\n private pinataStore: PinataContentStore | null = null;\n private provider: CloudProvider = 'local';\n\n constructor(kuboStore: ContentStore) {\n this.kuboStore = kuboStore;\n }\n\n /**\n * Configure le provider cloud.\n * Peut être appelé à tout moment pour basculer local ↔ pinata.\n */\n setProvider(provider: CloudProvider, pinataConfig?: PinataConfig): void {\n this.provider = provider;\n\n if (provider === 'pinata' && pinataConfig) {\n if (this.pinataStore) {\n this.pinataStore.updateConfig(pinataConfig);\n } else {\n this.pinataStore = new PinataContentStore(pinataConfig);\n }\n }\n\n console.log(`[storage-router] Provider set to: ${provider}`);\n }\n\n /**\n * Retourne le provider actif\n */\n getProvider(): CloudProvider {\n return this.provider;\n }\n\n /**\n * Vérifie si le cloud est configuré et actif\n */\n isCloudActive(): boolean {\n return this.provider === 'pinata' && this.pinataStore !== null;\n }\n\n /**\n * Retourne le PinataContentStore pour les opérations spécifiques (stat, etc.)\n */\n getPinataStore(): PinataContentStore | null {\n return this.pinataStore;\n }\n\n // ─── ContentStore interface ────────────────────────────────────\n\n /**\n * Stocke les données selon le provider actif.\n *\n * Mode local : put dans Kubo uniquement\n * Mode pinata : put dans Pinata (le CID IPFS est identique)\n */\n async put(data: Buffer): Promise<{ cid: string; size: number }> {\n // TOUJOURS stocker en local (Kubo) d'abord.\n // La sync vers Pinata se fait en arrière-plan via SyncQueueProcessor.\n // Cela garantit que :\n // 1. Le fichier est toujours disponible localement\n // 2. Le CID est identique (même blob chiffré → même hash)\n // 3. Le SyncQueueProcessor peut récupérer le blob depuis Kubo pour l'uploader\n const result = await this.kuboStore.put(data);\n console.log(`[storage-router] Stored locally: ${result.cid} (cloud=${this.provider})`);\n return result;\n }\n\n /**\n * Récupère les données selon le provider actif.\n *\n * Mode local : get depuis Kubo\n * Mode pinata : essaie Pinata d'abord, fallback sur Kubo\n */\n async get(cid: string): Promise<Buffer> {\n if (this.provider === 'pinata' && this.pinataStore) {\n try {\n return await this.pinataStore.get(cid);\n } catch (err: any) {\n // Fallback sur Kubo si le fichier est aussi en local\n console.warn(`[storage-router] Pinata get failed, trying Kubo: ${err.message}`);\n return this.kuboStore.get(cid);\n }\n }\n\n return this.kuboStore.get(cid);\n }\n\n /**\n * Pin : selon le provider actif\n */\n async pin(cid: string): Promise<void> {\n if (this.provider === 'pinata' && this.pinataStore) {\n await this.pinataStore.pin(cid);\n return;\n }\n await this.kuboStore.pin(cid);\n }\n\n /**\n * Unpin : selon le provider actif\n */\n async unpin(cid: string): Promise<void> {\n if (this.provider === 'pinata' && this.pinataStore) {\n await this.pinataStore.unpin(cid);\n return;\n }\n await this.kuboStore.unpin(cid);\n }\n\n /**\n * GC : toujours sur Kubo (Pinata gère son propre stockage)\n */\n async gc(): Promise<void> {\n await this.kuboStore.gc();\n }\n\n /**\n * Vérifie la connectivité du provider actif\n */\n async isOnline(): Promise<boolean> {\n if (this.provider === 'pinata' && this.pinataStore) {\n return this.pinataStore.isOnline();\n }\n return this.kuboStore.isOnline();\n }\n\n /**\n * Vérifie la connectivité de Kubo (toujours utile même en mode cloud)\n */\n async isKuboOnline(): Promise<boolean> {\n return this.kuboStore.isOnline();\n }\n\n /**\n * Vérifie la connectivité Pinata (même si le provider est local)\n */\n async isPinataOnline(): Promise<boolean> {\n if (!this.pinataStore) return false;\n return this.pinataStore.isOnline();\n }\n}\n","/**\n * SyncQueueProcessor — Processeur de synchronisation en arrière-plan\n *\n * Sprint 3 : gère la queue de sync (pending_operations) et pousse\n * les fichiers chiffrés vers Pinata quand la connexion est disponible.\n *\n * Fonctionne en mode pull : vérifie périodiquement la queue et la connectivité.\n * En cas d'échec réseau, les opérations restent en queue et seront retentées.\n */\n\nimport { Database } from './database.js';\nimport { StorageRouter } from './storage-router.js';\nimport { ContentStore } from './content-store.js';\n\nexport interface SyncEvent {\n type: 'sync:start' | 'sync:progress' | 'sync:complete' | 'sync:error' | 'sync:offline' | 'sync:online';\n data?: any;\n}\n\nexport class SyncQueueProcessor {\n private db: Database;\n private store: StorageRouter;\n private kuboStore: ContentStore;\n private intervalId: ReturnType<typeof setInterval> | null = null;\n private isProcessing = false;\n private isOnline = false;\n private readonly CHECK_INTERVAL_MS = 30_000; // 30 secondes\n private readonly MAX_RETRIES = 5;\n private eventCallback: ((event: SyncEvent) => void) | null = null;\n\n constructor(db: Database, store: StorageRouter, kuboStore: ContentStore) {\n this.db = db;\n this.store = store;\n this.kuboStore = kuboStore;\n }\n\n /**\n * Définit le callback pour les événements de sync\n */\n onEvent(callback: (event: SyncEvent) => void): void {\n this.eventCallback = callback;\n }\n\n private emit(event: SyncEvent): void {\n if (this.eventCallback) {\n this.eventCallback(event);\n }\n }\n\n /**\n * Démarre le processeur de sync en arrière-plan\n */\n start(): void {\n if (this.intervalId) return; // Déjà démarré\n\n console.log('[sync] SyncQueueProcessor started');\n\n // Check immédiat\n this.processQueue().catch(err =>\n console.error('[sync] Initial check failed:', err.message)\n );\n\n // Check périodique\n this.intervalId = setInterval(() => {\n this.processQueue().catch(err =>\n console.error('[sync] Periodic check failed:', err.message)\n );\n }, this.CHECK_INTERVAL_MS);\n }\n\n /**\n * Arrête le processeur\n */\n stop(): void {\n if (this.intervalId) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n }\n console.log('[sync] SyncQueueProcessor stopped');\n }\n\n /**\n * Force un traitement immédiat de la queue\n */\n async forceProcess(): Promise<void> {\n await this.processQueue();\n }\n\n /**\n * Retourne le statut courant du processeur\n */\n getStatus(): { running: boolean; online: boolean; processing: boolean } {\n return {\n running: this.intervalId !== null,\n online: this.isOnline,\n processing: this.isProcessing,\n };\n }\n\n /**\n * Traite la queue de sync\n */\n private async processQueue(): Promise<void> {\n // Éviter les traitements concurrents\n if (this.isProcessing) return;\n if (!this.store.isCloudActive()) {\n console.log('[sync] Skipping — cloud not active');\n return;\n }\n\n this.isProcessing = true;\n\n try {\n // Vérifier la connectivité Pinata\n const online = await this.store.isPinataOnline();\n\n if (online !== this.isOnline) {\n this.isOnline = online;\n this.emit({\n type: online ? 'sync:online' : 'sync:offline',\n });\n }\n\n if (!online) {\n this.isProcessing = false;\n return;\n }\n\n // Récupérer les opérations en attente\n const pendingOps = this.db.getPendingOperations();\n\n if (pendingOps.length === 0) {\n this.isProcessing = false;\n return;\n }\n\n console.log(`[sync] Processing ${pendingOps.length} pending operations`);\n this.emit({ type: 'sync:start', data: { count: pendingOps.length } });\n\n let completed = 0;\n let errors = 0;\n\n for (const op of pendingOps) {\n if (op.attempts >= this.MAX_RETRIES) {\n // Trop de tentatives — marquer comme échoué\n this.db.updateOperationStatus(op.id, 'failed', 'Max retries exceeded');\n errors++;\n continue;\n }\n\n try {\n if (op.op_type === 'pinata_upload') {\n await this.processPinataUpload(op);\n completed++;\n } else {\n console.warn(`[sync] Unknown operation type: ${op.op_type}`);\n this.db.updateOperationStatus(op.id, 'failed', `Unknown op_type: ${op.op_type}`);\n errors++;\n }\n } catch (err: any) {\n console.error(`[sync] Operation ${op.id} failed: ${err.message}`);\n this.db.updateOperationStatus(op.id, 'pending', err.message);\n errors++;\n\n // Si erreur réseau, arrêter le traitement\n if (err.message.includes('fetch') || err.message.includes('network')) {\n this.isOnline = false;\n this.emit({ type: 'sync:offline' });\n break;\n }\n }\n\n this.emit({\n type: 'sync:progress',\n data: { completed, errors, total: pendingOps.length },\n });\n }\n\n // Nettoyage des opérations terminées\n this.db.clearCompletedOperations();\n\n this.emit({\n type: 'sync:complete',\n data: { completed, errors },\n });\n\n console.log(`[sync] Queue processed: ${completed} completed, ${errors} errors`);\n } finally {\n this.isProcessing = false;\n }\n }\n\n /**\n * Traite une opération d'upload vers Pinata\n */\n private async processPinataUpload(op: { id: number; op_payload: string }): Promise<void> {\n const payload = JSON.parse(op.op_payload);\n const { versionId, cid } = payload;\n\n // Marquer la version comme \"syncing\"\n this.db.updateSyncStatus(versionId, 'syncing');\n\n // Récupérer le blob chiffré depuis Kubo local\n const data = await this.kuboStore.get(cid);\n\n // Upload vers Pinata\n const pinataStore = this.store.getPinataStore();\n if (!pinataStore) {\n throw new Error('Pinata store not available');\n }\n\n const result = await pinataStore.put(data);\n\n // Mettre à jour les statuts\n this.db.updateSyncStatus(versionId, 'synced', result.cid);\n this.db.updateOperationStatus(op.id, 'completed');\n\n console.log(`[sync] Uploaded to Pinata: version ${versionId} → ${result.cid}`);\n }\n}\n","/**\n * MerkleVault Crypto — Sprint 2\n *\n * Chiffrement de bout en bout pour les fichiers stockés sur IPFS.\n *\n * Architecture cryptographique :\n * ┌──────────────────────────────────────────────────────────────────┐\n * │ Master Key (256 bits, aléatoire, générée une seule fois) │\n * │ │ │\n * │ ├──▶ Wrappée par Password Key (pour déverrouillage) │\n * │ │ └── Argon2id(password, salt) → Password Key │\n * │ │ └── XChaCha20-Poly1305(PK, nonce) → pwWrapped │\n * │ │ │\n * │ ├──▶ Wrappée par Recovery Key (pour récupération) │\n * │ │ └── BIP39(24 mots) → seed → HKDF → Recovery Key │\n * │ │ └── XChaCha20-Poly1305(RK, nonce) → recWrapped │\n * │ │ │\n * │ └──▶ Wrap/Unwrap des File Keys │\n * │ └── File Key (256 bits, unique par fichier) │\n * │ └── XChaCha20-Poly1305(FK, nonce) → ciphertext│\n * └──────────────────────────────────────────────────────────────────┘\n *\n * Changement de mot de passe : re-wrap Master Key, fichiers inchangés\n * Récupération : unwrap via Recovery Key, re-wrap avec nouveau password\n *\n * Bibliothèques (pure JS, zéro dépendance native) :\n * @noble/ciphers — XChaCha20-Poly1305\n * @noble/hashes — Argon2id, SHA-256, HKDF\n * @scure/bip39 — Génération et validation mnémonique\n */\n\nimport { xchacha20poly1305 } from '@noble/ciphers/chacha.js';\nimport { randomBytes as nobleRandomBytes } from '@noble/ciphers/utils.js';\nimport { argon2id } from '@noble/hashes/argon2.js';\nimport { sha256 } from '@noble/hashes/sha2.js';\nimport { hkdf } from '@noble/hashes/hkdf.js';\nimport { bytesToHex, hexToBytes } from '@noble/hashes/utils.js';\nimport * as bip39 from '@scure/bip39';\nimport { wordlist as english } from '@scure/bip39/wordlists/english.js';\n\n// ─── Constants ──────────────────────────────────────────────\n\n/** Algorithme identifiant stocké dans file_versions.enc_algo */\nexport const ENC_ALGO = 'xchacha20-poly1305';\n\n/** Paramètres Argon2id — conformes aux recommandations OWASP 2024 */\nconst ARGON2_PARAMS = {\n t: 3, // 3 itérations\n m: 65536, // 64 MB mémoire\n p: 4, // 4 threads parallèles\n};\n\n/** Taille de la clé maître et des clés fichier (256 bits) */\nconst KEY_LENGTH = 32;\n\n/** Taille du nonce XChaCha20 (192 bits) */\nconst NONCE_LENGTH = 24;\n\n/** Taille du salt Argon2id (128 bits) */\nconst SALT_LENGTH = 16;\n\n// ─── Types ──────────────────────────────────────────────────\n\nexport interface VaultKeys {\n /** Clé maître dérivée du mot de passe (256 bits) */\n masterKey: Uint8Array;\n /** Clé maître dérivée de la seed BIP39 (256 bits) — pour la récupération */\n recoveryKey: Uint8Array;\n}\n\nexport interface VaultConfig {\n /** Salt pour Argon2id (hex) */\n salt: string;\n /** Clé maître chiffrée avec la recovery key (hex) */\n encryptedMasterKey: string;\n /** Nonce utilisé pour chiffrer la master key avec recovery key (hex) */\n masterKeyNonce: string;\n /** Clé maître chiffrée avec la clé dérivée du mot de passe (hex) */\n pwEncryptedMasterKey: string;\n /** Nonce utilisé pour chiffrer la master key avec password key (hex) */\n pwKeyNonce: string;\n /** Hash SHA-256 du mnémonique pour vérification (hex) */\n mnemonicHash: string;\n /** Hash SHA-256 de la master key pour vérification rapide au unlock (hex) */\n masterKeyHash: string;\n}\n\nexport interface EncryptedFile {\n /** Données chiffrées (ciphertext + tag Poly1305) */\n ciphertext: Buffer;\n /** Clé fichier chiffrée avec la master key */\n wrappedKey: Buffer;\n /** Nonce utilisé pour chiffrer la clé fichier */\n keyNonce: Buffer;\n /** Nonce utilisé pour chiffrer les données */\n dataNonce: Buffer;\n}\n\nexport interface DecryptParams {\n /** Données chiffrées */\n ciphertext: Buffer;\n /** Clé fichier chiffrée */\n wrappedKey: Buffer;\n /** Nonce de la clé fichier */\n keyNonce: Buffer;\n /** Nonce des données */\n dataNonce: Buffer;\n /** Clé maître pour déchiffrer la clé fichier */\n masterKey: Uint8Array;\n}\n\n// ─── BIP39 — Phrase mnémonique ──────────────────────────────\n\n/**\n * Génère une phrase mnémonique BIP39 de 24 mots (256 bits d'entropie).\n * Cette phrase est la clé de récupération ultime du vault.\n */\nexport function generateMnemonic(): string {\n return bip39.generateMnemonic(english, 256);\n}\n\n/**\n * Valide qu'une phrase mnémonique est correcte (checksum BIP39).\n */\nexport function validateMnemonic(mnemonic: string): boolean {\n return bip39.validateMnemonic(mnemonic, english);\n}\n\n/**\n * Dérive une seed de 64 bytes à partir du mnémonique.\n * Utilise PBKDF2-HMAC-SHA512 (standard BIP39).\n */\nexport async function mnemonicToSeed(mnemonic: string): Promise<Uint8Array> {\n return bip39.mnemonicToSeed(mnemonic);\n}\n\n// ─── Argon2id — Dérivation de clé depuis mot de passe ───────\n\n/**\n * Dérive une clé maître de 256 bits à partir du mot de passe.\n *\n * @param password - Mot de passe utilisateur (UTF-8)\n * @param salt - Salt aléatoire de 16 bytes (stocké dans vault_config)\n * @returns - Clé de 32 bytes\n */\nexport function deriveKeyFromPassword(password: string, salt: Uint8Array): Uint8Array {\n const passwordBytes = new TextEncoder().encode(password);\n return argon2id(passwordBytes, salt, {\n t: ARGON2_PARAMS.t,\n m: ARGON2_PARAMS.m,\n p: ARGON2_PARAMS.p,\n dkLen: KEY_LENGTH,\n });\n}\n\n/**\n * Dérive une clé de récupération à partir de la seed BIP39.\n * Utilise HKDF-SHA256 pour extraire 32 bytes de la seed de 64 bytes.\n */\nexport function deriveRecoveryKey(seed: Uint8Array): Uint8Array {\n const info = new TextEncoder().encode('MerkleVault-Recovery-v1');\n return hkdf(sha256, seed, undefined, info, KEY_LENGTH);\n}\n\n// ─── XChaCha20-Poly1305 — Chiffrement fichier ──────────────\n\n/**\n * Chiffre un fichier avec une clé unique (per-file key),\n * puis wrappe cette clé avec la master key.\n *\n * Flux :\n * 1. Générer une clé fichier aléatoire (32 bytes)\n * 2. Chiffrer le fichier avec XChaCha20-Poly1305(fileKey, dataNonce)\n * 3. Chiffrer la fileKey avec XChaCha20-Poly1305(masterKey, keyNonce)\n * 4. Retourner ciphertext + wrappedKey + nonces\n */\nexport function encryptFile(plaintext: Buffer, masterKey: Uint8Array): EncryptedFile {\n // 1. Clé fichier unique\n const fileKey = nobleRandomBytes(KEY_LENGTH);\n\n // 2. Chiffrer les données\n const dataNonce = nobleRandomBytes(NONCE_LENGTH);\n const cipher = xchacha20poly1305(fileKey, dataNonce);\n const ciphertext = cipher.encrypt(new Uint8Array(plaintext));\n\n // 3. Wrapper la clé fichier avec la master key\n const keyNonce = nobleRandomBytes(NONCE_LENGTH);\n const keyCipher = xchacha20poly1305(masterKey, keyNonce);\n const wrappedKey = keyCipher.encrypt(fileKey);\n\n return {\n ciphertext: Buffer.from(ciphertext),\n wrappedKey: Buffer.from(wrappedKey),\n keyNonce: Buffer.from(keyNonce),\n dataNonce: Buffer.from(dataNonce),\n };\n}\n\n/**\n * Déchiffre un fichier :\n * 1. Unwrap la fileKey avec la master key\n * 2. Déchiffrer le fichier avec la fileKey\n */\nexport function decryptFile(params: DecryptParams): Buffer {\n const { ciphertext, wrappedKey, keyNonce, dataNonce, masterKey } = params;\n\n // 1. Unwrap la clé fichier\n const keyCipher = xchacha20poly1305(masterKey, new Uint8Array(keyNonce));\n const fileKey = keyCipher.decrypt(new Uint8Array(wrappedKey));\n\n // 2. Déchiffrer les données\n const dataCipher = xchacha20poly1305(fileKey, new Uint8Array(dataNonce));\n const plaintext = dataCipher.decrypt(new Uint8Array(ciphertext));\n\n return Buffer.from(plaintext);\n}\n\n// ─── Vault — Création et déverrouillage ─────────────────────\n\n/**\n * Crée un nouveau vault : génère le mnémonique, dérive les clés,\n * chiffre la master key avec la recovery key pour la récupération.\n *\n * @returns mnemonic - Les 24 mots à montrer à l'utilisateur (UNE SEULE FOIS)\n * @returns config - Configuration à persister dans SQLite\n * @returns masterKey - Clé maître à garder en mémoire (session)\n */\nexport async function createVault(password: string): Promise<{\n mnemonic: string;\n config: VaultConfig;\n masterKey: Uint8Array;\n}> {\n // 1. Générer le mnémonique BIP39 (24 mots, 256 bits)\n const mnemonic = generateMnemonic();\n\n // 2. Générer un salt aléatoire pour Argon2id\n const salt = nobleRandomBytes(SALT_LENGTH);\n\n // 3. Générer une master key ALÉATOIRE (indépendante du mot de passe)\n const masterKey = nobleRandomBytes(KEY_LENGTH);\n\n // 4. Dériver la clé mot de passe via Argon2id\n const passwordKey = deriveKeyFromPassword(password, salt);\n\n // 5. Wrapper la master key avec la clé mot de passe (pour le déverrouillage quotidien)\n const pwKeyNonce = nobleRandomBytes(NONCE_LENGTH);\n const pwCipher = xchacha20poly1305(passwordKey, pwKeyNonce);\n const pwEncryptedMasterKey = pwCipher.encrypt(masterKey);\n\n // 6. Dériver la recovery key depuis le mnémonique\n const seed = await mnemonicToSeed(mnemonic);\n const recoveryKey = deriveRecoveryKey(seed);\n\n // 7. Wrapper la master key avec la recovery key (pour la récupération)\n const masterKeyNonce = nobleRandomBytes(NONCE_LENGTH);\n const recoveryCipher = xchacha20poly1305(recoveryKey, masterKeyNonce);\n const encryptedMasterKey = recoveryCipher.encrypt(masterKey);\n\n // 8. Hash du mnémonique (pour vérification sans stocker le mnémonique)\n const mnemonicHash = sha256(new TextEncoder().encode(mnemonic));\n\n // 9. Hash de la master key (pour vérification rapide post-unwrap)\n const masterKeyHash = sha256(masterKey);\n\n const config: VaultConfig = {\n salt: bytesToHex(salt),\n encryptedMasterKey: bytesToHex(encryptedMasterKey),\n masterKeyNonce: bytesToHex(masterKeyNonce),\n pwEncryptedMasterKey: bytesToHex(pwEncryptedMasterKey),\n pwKeyNonce: bytesToHex(pwKeyNonce),\n mnemonicHash: bytesToHex(mnemonicHash),\n masterKeyHash: bytesToHex(masterKeyHash),\n };\n\n return { mnemonic, config, masterKey };\n}\n\n/**\n * Déverrouille le vault avec le mot de passe.\n * Dérive la clé mot de passe via Argon2id, puis unwrap la master key.\n * Retourne la master key si le mot de passe est correct, null sinon.\n */\nexport function unlockVault(password: string, config: VaultConfig): Uint8Array | null {\n const salt = hexToBytes(config.salt);\n const passwordKey = deriveKeyFromPassword(password, salt);\n\n // Unwrap la master key avec la clé mot de passe\n let masterKey: Uint8Array;\n try {\n const pwNonce = hexToBytes(config.pwKeyNonce);\n const pwEncrypted = hexToBytes(config.pwEncryptedMasterKey);\n const cipher = xchacha20poly1305(passwordKey, pwNonce);\n masterKey = cipher.decrypt(pwEncrypted);\n } catch {\n return null; // Mot de passe incorrect (Poly1305 tag invalide)\n }\n\n // Vérifier le hash de la master key (double vérification)\n const computedHash = bytesToHex(sha256(masterKey));\n if (computedHash !== config.masterKeyHash) {\n return null;\n }\n\n return masterKey;\n}\n\n/**\n * Récupère le vault via la phrase BIP39.\n * Déchiffre la master key originale, puis la re-chiffre avec le nouveau mot de passe.\n *\n * @returns La master key + une nouvelle config avec le nouveau mot de passe\n */\nexport async function recoverVault(\n mnemonic: string,\n newPassword: string,\n oldConfig: VaultConfig\n): Promise<{ masterKey: Uint8Array; config: VaultConfig } | null> {\n // 1. Valider le mnémonique\n if (!validateMnemonic(mnemonic)) {\n return null;\n }\n\n // 2. Vérifier le hash du mnémonique\n const mnemonicHash = bytesToHex(sha256(new TextEncoder().encode(mnemonic)));\n if (mnemonicHash !== oldConfig.mnemonicHash) {\n return null;\n }\n\n // 3. Dériver la recovery key\n const seed = await mnemonicToSeed(mnemonic);\n const recoveryKey = deriveRecoveryKey(seed);\n\n // 4. Déchiffrer la master key originale via la recovery key\n let masterKey: Uint8Array;\n try {\n const nonce = hexToBytes(oldConfig.masterKeyNonce);\n const encryptedMasterKey = hexToBytes(oldConfig.encryptedMasterKey);\n const cipher = xchacha20poly1305(recoveryKey, nonce);\n masterKey = cipher.decrypt(encryptedMasterKey);\n } catch {\n return null; // Déchiffrement échoué — mnémonique incorrect\n }\n\n // 5. Vérifier que la master key correspond\n const computedHash = bytesToHex(sha256(masterKey));\n if (computedHash !== oldConfig.masterKeyHash) {\n return null;\n }\n\n // 6. Re-wrapper la master key avec le nouveau mot de passe\n const newSalt = nobleRandomBytes(SALT_LENGTH);\n const newPasswordKey = deriveKeyFromPassword(newPassword, newSalt);\n\n const newPwKeyNonce = nobleRandomBytes(NONCE_LENGTH);\n const pwCipher = xchacha20poly1305(newPasswordKey, newPwKeyNonce);\n const newPwEncryptedMasterKey = pwCipher.encrypt(masterKey);\n\n // 7. Re-wrapper avec la recovery key (nouveau nonce pour forward secrecy)\n const newMasterKeyNonce = nobleRandomBytes(NONCE_LENGTH);\n const newRecoveryCipher = xchacha20poly1305(recoveryKey, newMasterKeyNonce);\n const newEncryptedMasterKey = newRecoveryCipher.encrypt(masterKey);\n\n const config: VaultConfig = {\n salt: bytesToHex(newSalt),\n encryptedMasterKey: bytesToHex(newEncryptedMasterKey),\n masterKeyNonce: bytesToHex(newMasterKeyNonce),\n pwEncryptedMasterKey: bytesToHex(newPwEncryptedMasterKey),\n pwKeyNonce: bytesToHex(newPwKeyNonce),\n mnemonicHash: oldConfig.mnemonicHash, // Ne change pas\n masterKeyHash: oldConfig.masterKeyHash, // La master key ne change pas\n };\n\n return { masterKey, config };\n}\n\n/**\n * Change le mot de passe du vault sans changer la master key.\n * Tous les fichiers restent déchiffrables — on ne re-chiffre que les métadonnées vault.\n */\nexport async function changePassword(\n oldPassword: string,\n newPassword: string,\n oldConfig: VaultConfig\n): Promise<VaultConfig | null> {\n // 1. Vérifier l'ancien mot de passe et récupérer la master key\n const masterKey = unlockVault(oldPassword, oldConfig);\n if (!masterKey) return null;\n\n // 2. Nouveau salt + nouvelle clé dérivée du nouveau mot de passe\n const newSalt = nobleRandomBytes(SALT_LENGTH);\n const newPasswordKey = deriveKeyFromPassword(newPassword, newSalt);\n\n // 3. Re-wrapper la master key avec la nouvelle clé mot de passe\n const newPwKeyNonce = nobleRandomBytes(NONCE_LENGTH);\n const pwCipher = xchacha20poly1305(newPasswordKey, newPwKeyNonce);\n const newPwEncryptedMasterKey = pwCipher.encrypt(masterKey);\n\n // 4. La recovery-wrapped master key ne change pas (même recovery key)\n return {\n ...oldConfig,\n salt: bytesToHex(newSalt),\n pwEncryptedMasterKey: bytesToHex(newPwEncryptedMasterKey),\n pwKeyNonce: bytesToHex(newPwKeyNonce),\n // encryptedMasterKey, masterKeyNonce, mnemonicHash, masterKeyHash : inchangés\n };\n}\n\n// ─── Utilitaires ────────────────────────────────────────────\n\n/**\n * Efface une clé de la mémoire (best-effort en JS).\n * Note : en JavaScript, le GC peut garder des copies — c'est\n * une mesure de précaution, pas une garantie absolue.\n */\nexport function zeroKey(key: Uint8Array): void {\n key.fill(0);\n}\n\n/**\n * Convertit un Uint8Array en hex string.\n */\nexport { bytesToHex, hexToBytes };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACiBA,IAAAA,sBAA6B;AAC7B,IAAAC,kBAAoD;AACpD,IAAAC,oBAAiB;;;ACRjB,gCAAoC;AACpC,qBAAqD;AACrD,uBAAiB;AACjB,yBAA6B;AAQtB,IAAM,cAAN,cAA0B,gCAAa;AAAA,EACpC,UAA+B;AAAA,EAC/B;AAAA,EACA;AAAA,EACA,kBAA4B,CAAC;AAAA,EAC7B,oBAA2D;AAAA,EAC3D,iBAAiB;AAAA,EACjB,SAAS;AAAA,EAEjB,YAAY,QAA2B;AACrC,UAAM;AACN,SAAK,SAAS;AACd,SAAK,UAAU,OAAO,WAAW;AAAA,EACnC;AAAA,EAEA,IAAI,QAAiB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,SAAiB;AACnB,WAAO,oBAAoB,KAAK,OAAO;AAAA,EACzC;AAAA,EAEA,IAAI,MAAqB;AACvB,WAAO,KAAK,SAAS,OAAO;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAE3B,QAAI,KAAC,2BAAW,KAAK,OAAO,QAAQ,GAAG;AACrC,oCAAU,KAAK,OAAO,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,IACrD;AAGA,QAAI,KAAC,2BAAW,iBAAAC,QAAK,KAAK,KAAK,OAAO,UAAU,QAAQ,CAAC,GAAG;AAC1D,YAAM,KAAK,SAAS;AAAA,IACtB;AAGA,UAAM,KAAK,cAAc;AAGzB,UAAM,KAAK,YAAY;AAGvB,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,SAAK,iBAAiB;AAEtB,QAAI,KAAK,mBAAmB;AAC1B,oBAAc,KAAK,iBAAiB;AACpC,WAAK,oBAAoB;AAAA,IAC3B;AAEA,QAAI,CAAC,KAAK,QAAS;AAEnB,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,YAAM,UAAU,WAAW,MAAM;AAE/B,aAAK,SAAS,KAAK,SAAS;AAC5B,gBAAQ;AAAA,MACV,GAAG,GAAM;AAET,WAAK,QAAS,GAAG,QAAQ,MAAM;AAC7B,qBAAa,OAAO;AACpB,aAAK,UAAU;AACf,aAAK,SAAS;AACd,gBAAQ;AAAA,MACV,CAAC;AAED,WAAK,QAAS,KAAK,SAAS;AAAA,IAC9B,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,WAA0B;AACtC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,WAAO,iCAAM,KAAK,OAAO,gBAAgB,CAAC,QAAQ,oBAAoB,GAAG;AAAA,QAC7E,KAAK,EAAE,GAAG,QAAQ,KAAK,WAAW,KAAK,OAAO,SAAS;AAAA,QACvD,OAAO;AAAA,MACT,CAAC;AAED,UAAI,SAAS;AACb,WAAK,QAAQ,GAAG,QAAQ,CAAC,MAAM;AAAE,kBAAU,EAAE,SAAS;AAAA,MAAG,CAAC;AAE1D,WAAK,GAAG,QAAQ,CAAC,SAAS;AACxB,YAAI,SAAS,EAAG,SAAQ;AAAA,YACnB,QAAO,IAAI,MAAM,0BAA0B,IAAI,MAAM,MAAM,EAAE,CAAC;AAAA,MACrE,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,gBAA+B;AAE3C,UAAM,gBAAoC;AAAA,MACxC,CAAC,iBAAiB,sBAAsB,KAAK,OAAO,EAAE;AAAA,MACtD,CAAC,qBAAqB,EAAE;AAAA,MACxB,CAAC,gBAAgB,WAAW;AAAA,MAC5B,CAAC,uBAAuB,IAAI;AAAA,IAC9B;AAGA,UAAM,cAAkC;AAAA,MACtC,CAAC,0BAA0B,IAAI;AAAA,MAC/B,CAAC,2BAA2B,IAAI;AAAA,IAClC;AAEA,eAAW,CAAC,KAAK,KAAK,KAAK,eAAe;AACxC,YAAM,KAAK,eAAe,CAAC,UAAU,KAAK,KAAK,CAAC;AAAA,IAClD;AACA,eAAW,CAAC,KAAK,KAAK,KAAK,aAAa;AACtC,YAAM,KAAK,eAAe,CAAC,UAAU,KAAK,OAAO,QAAQ,CAAC;AAAA,IAC5D;AAAA,EACF;AAAA,EAEA,MAAc,eAAe,MAAiC;AAC5D,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,WAAO,iCAAM,KAAK,OAAO,gBAAgB,MAAM;AAAA,QACnD,KAAK,EAAE,GAAG,QAAQ,KAAK,WAAW,KAAK,OAAO,SAAS;AAAA,QACvD,OAAO;AAAA,MACT,CAAC;AAED,UAAI,SAAS;AACb,UAAI,SAAS;AACb,WAAK,QAAQ,GAAG,QAAQ,CAAC,MAAM;AAAE,kBAAU,EAAE,SAAS;AAAA,MAAG,CAAC;AAC1D,WAAK,QAAQ,GAAG,QAAQ,CAAC,MAAM;AAAE,kBAAU,EAAE,SAAS;AAAA,MAAG,CAAC;AAE1D,WAAK,GAAG,QAAQ,CAAC,SAAS;AACxB,YAAI,SAAS,EAAG,SAAQ,OAAO,KAAK,CAAC;AAAA,YAChC,QAAO,IAAI,MAAM,QAAQ,KAAK,CAAC,CAAC,YAAY,MAAM,EAAE,CAAC;AAAA,MAC5D,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,cAA6B;AACzC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,WAAO,iCAAM,KAAK,OAAO,gBAAgB,CAAC,UAAU,WAAW,GAAG;AAAA,QACtE,KAAK,EAAE,GAAG,QAAQ,KAAK,WAAW,KAAK,OAAO,SAAS;AAAA,QACvD,OAAO;AAAA,MACT,CAAC;AAED,WAAK,UAAU;AAEf,UAAI,WAAW;AAGf,WAAK,QAAQ,GAAG,QAAQ,CAAC,SAAiB;AACxC,cAAM,OAAO,KAAK,SAAS;AAC3B,aAAK,KAAK,OAAO,KAAK,KAAK,CAAC;AAE5B,YAAI,CAAC,YAAY,KAAK,SAAS,iBAAiB,GAAG;AACjD,qBAAW;AACX,eAAK,SAAS;AACd,eAAK,KAAK,OAAO;AACjB,kBAAQ;AAAA,QACV;AAAA,MACF,CAAC;AAED,WAAK,QAAQ,GAAG,QAAQ,CAAC,SAAiB;AACxC,aAAK,KAAK,OAAO,YAAY,KAAK,SAAS,EAAE,KAAK,CAAC,EAAE;AAAA,MACvD,CAAC;AAED,WAAK,GAAG,QAAQ,CAAC,MAAM,WAAW;AAChC,aAAK,SAAS;AACd,aAAK,UAAU;AAEf,YAAI,CAAC,UAAU;AACb,iBAAO,IAAI,MAAM,kCAAkC,IAAI,YAAY,MAAM,GAAG,CAAC;AAC7E;AAAA,QACF;AAEA,YAAI,CAAC,KAAK,gBAAgB;AACxB,eAAK,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACnC,eAAK,YAAY;AAAA,QACnB;AAAA,MACF,CAAC;AAED,WAAK,GAAG,SAAS,CAAC,QAAQ;AACxB,YAAI,CAAC,SAAU,QAAO,GAAG;AAAA,MAC3B,CAAC;AAGD,iBAAW,MAAM;AACf,YAAI,CAAC,UAAU;AACb,qBAAW;AACX,eAAK,KAAK,SAAS;AACnB,iBAAO,IAAI,MAAM,4BAA4B,CAAC;AAAA,QAChD;AAAA,MACF,GAAG,GAAM;AAAA,IACX,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,cAA6B;AACzC,UAAM,MAAM,KAAK,IAAI;AACrB,SAAK,gBAAgB,KAAK,GAAG;AAE7B,SAAK,kBAAkB,KAAK,gBAAgB,OAAO,OAAK,MAAM,IAAI,GAAM;AAExE,QAAI,KAAK,gBAAgB,SAAS,GAAG;AACnC,WAAK,KAAK,SAAS,yCAAyC;AAC5D;AAAA,IACF;AAEA,SAAK,KAAK,YAAY;AAGtB,UAAM,IAAI,QAAQ,OAAK,WAAW,GAAG,GAAI,CAAC;AAE1C,QAAI,CAAC,KAAK,gBAAgB;AACxB,UAAI;AACF,cAAM,KAAK,YAAY;AACvB,aAAK,KAAK,OAAO;AAAA,MACnB,SAAS,KAAK;AACZ,aAAK,KAAK,SAAS,wBAAwB,GAAG,EAAE;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAAuB;AAC7B,SAAK,oBAAoB,YAAY,YAAY;AAC/C,UAAI,CAAC,KAAK,UAAU,KAAK,eAAgB;AAEzC,UAAI;AACF,cAAM,aAAa,IAAI,gBAAgB;AACvC,cAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAI;AACzD,cAAM,MAAM,MAAM,MAAM,GAAG,KAAK,MAAM,cAAc;AAAA,UAClD,QAAQ;AAAA,UACR,QAAQ,WAAW;AAAA,QACrB,CAAC;AACD,qBAAa,OAAO;AAEpB,YAAI,CAAC,IAAI,IAAI;AACX,eAAK,KAAK,aAAa,iBAAiB,IAAI,MAAM,EAAE;AAAA,QACtD;AAAA,MACF,QAAQ;AACN,aAAK,KAAK,aAAa,uBAAuB;AAAA,MAChD;AAAA,IACF,GAAG,GAAM;AAAA,EACX;AACF;;;ACrRA,4BAA0B;AAC1B,IAAAC,oBAAiB;AACjB,IAAAC,kBAAsC;AAqC/B,IAAM,WAAN,MAAe;AAAA,EACZ;AAAA,EAER,YAAY,SAAiB;AAC3B,QAAI,KAAC,4BAAW,OAAO,GAAG;AACxB,qCAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,IACxC;AAEA,UAAM,SAAS,kBAAAC,QAAK,KAAK,SAAS,gBAAgB;AAClD,SAAK,KAAK,IAAI,sBAAAC,QAAc,MAAM;AAGlC,SAAK,GAAG,OAAO,oBAAoB;AACnC,SAAK,GAAG,OAAO,sBAAsB;AACrC,SAAK,GAAG,OAAO,mBAAmB;AAClC,SAAK,GAAG,OAAO,qBAAqB;AACpC,SAAK,GAAG,OAAO,uBAAuB;AACtC,SAAK,GAAG,OAAO,qBAAqB;AACpC,SAAK,GAAG,OAAO,qBAAqB;AAEpC,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAKQ,UAAgB;AAEtB,SAAK,GAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAMZ;AAED,UAAM,UAAU,KAAK,WAAW,gBAAgB;AAEhD,QAAI,CAAC,SAAS;AACZ,WAAK,aAAa;AAClB,WAAK,WAAW,kBAAkB,GAAG;AAAA,IACvC;AAEA,UAAM,iBAAiB,SAAS,KAAK,WAAW,gBAAgB,KAAK,KAAK,EAAE;AAC5E,QAAI,iBAAiB,GAAG;AACtB,WAAK,aAAa;AAClB,WAAK,WAAW,kBAAkB,GAAG;AAAA,IACvC;AACA,QAAI,SAAS,KAAK,WAAW,gBAAgB,KAAK,KAAK,EAAE,IAAI,GAAG;AAC9D,WAAK,aAAa;AAClB,WAAK,WAAW,kBAAkB,GAAG;AAAA,IACvC;AAAA,EACF;AAAA,EAEQ,eAAqB;AAC3B,SAAK,GAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAyGZ;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAqB;AAC3B,SAAK,GAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAOZ;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAqB;AAC3B,SAAK,GAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KASZ;AACD,YAAQ,IAAI,kEAA6D;AAAA,EAC3E;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAiB,WAAmB,QAA6D,WAA0B;AACzH,QAAI,WAAW;AACb,WAAK,GAAG;AAAA,QACN;AAAA,MACF,EAAE,IAAI,QAAQ,UAAU,WAAW,SAAS;AAAA,IAC9C,OAAO;AACL,WAAK,GAAG;AAAA,QACN;AAAA,MACF,EAAE,IAAI,QAAQ,SAAS;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,sBAAwC;AACtC,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,IACF,EAAE,IAAI,SAAS;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,oBAAoB,QAAgB,SAAsC;AACxE,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG5B,EAAE,IAAI,QAAQ,KAAK,UAAU,OAAO,GAAG,KAAK,GAAG;AAChD,WAAO,OAAO,KAAK,eAAe;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,uBAAqG;AACnG,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,IACF,EAAE,IAAI;AAAA,EACR;AAAA;AAAA;AAAA;AAAA,EAKA,sBAAsB,MAAc,QAA4C,OAAsB;AACpG,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,SAAK,GAAG;AAAA,MACN;AAAA,IACF,EAAE,IAAI,QAAQ,SAAS,MAAM,KAAK,IAAI;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKA,2BAAiC;AAC/B,SAAK,GAAG,QAAQ,2DAA2D,EAAE,IAAI;AAAA,EACnF;AAAA;AAAA;AAAA;AAAA,EAKA,qBAAqB,QAA0B;AAC7C,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAU5B,EAAE,IAAI,MAAM;AACb,WAAO,KAAK,IAAI,OAAK,EAAE,UAAU;AAAA,EACnC;AAAA;AAAA,EAIA,iBAAgD;AAC9C,UAAM,OAAO,KAAK,GAAG,QAAQ,qCAAqC,EAAE,IAAI;AACxE,QAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,UAAM,SAAiC,CAAC;AACxC,eAAW,OAAO,MAAM;AACtB,aAAO,IAAI,GAAG,IAAI,IAAI;AAAA,IACxB;AACA,WAAO;AAAA,EACT;AAAA,EAEA,eAAe,QAAsC;AACnD,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG5B;AACD,UAAM,cAAc,KAAK,GAAG,YAAY,MAAM;AAC5C,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,aAAK,IAAI,KAAK,KAAK;AAAA,MACrB;AAAA,IACF,CAAC;AACD,gBAAY;AAAA,EACd;AAAA,EAEA,qBAA8B;AAC5B,UAAM,MAAM,KAAK,GAAG,QAAQ,4CAA4C,EAAE,IAAI;AAC9E,WAAO,IAAI,QAAQ;AAAA,EACrB;AAAA;AAAA,EAIA,WAAW,KAA4B;AACrC,UAAM,MAAM,KAAK,GAAG,QAAQ,0CAA0C,EAAE,IAAI,GAAG;AAC/E,WAAO,KAAK,SAAS;AAAA,EACvB;AAAA,EAEA,WAAW,KAAa,OAAqB;AAC3C,SAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAGf,EAAE,IAAI,KAAK,KAAK;AAAA,EACnB;AAAA;AAAA,EAIA,cAAuB;AACrB,WAAO,KAAK,GAAG,QAAQ,oEAAoE,EAAE,IAAI;AAAA,EACnG;AAAA,EAEA,QAAQ,IAA4B;AAClC,WAAQ,KAAK,GAAG,QAAQ,kCAAkC,EAAE,IAAI,EAAE,KAAiB;AAAA,EACrF;AAAA,EAEA,aAAa,UAA6B;AACxC,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,IACF,EAAE,IAAI,QAAQ;AAAA,EAChB;AAAA,EAEA,aAAa,UAAkB,MAAuB;AACpD,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,UAAM,OAAO,KAAK,GAAG;AAAA,MACnB;AAAA,IACF,EAAE,IAAI,UAAU,MAAM,UAAU,KAAK,GAAG;AACxC,WAAO,KAAK,QAAQ,OAAO,KAAK,eAAe,CAAC;AAAA,EAClD;AAAA,EAEA,eAAe,UAAkB,MAAuB;AACtD,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,UAAM,OAAO,KAAK,GAAG;AAAA,MACnB;AAAA,IACF,EAAE,IAAI,UAAU,MAAM,QAAQ,KAAK,GAAG;AACtC,WAAO,KAAK,QAAQ,OAAO,KAAK,eAAe,CAAC;AAAA,EAClD;AAAA,EAEA,OAAO,QAAgB,SAAuB;AAC5C,UAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,UAAM,UAAU,MAAM;AACtB,UAAM,UAAU,OAAO,KAAK,YAAY,MAAM,IAAI;AAClD,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,SAAK,GAAG,QAAQ,yDAAyD,EAAE,IAAI,SAAS,KAAK,MAAM;AAEnG,SAAK,YAAY,QAAQ,SAAS,OAAO;AAAA,EAC3C;AAAA,EAEA,SAAS,QAAgB,aAA2B;AAClD,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,SAAK,GAAG,QAAQ,8DAA8D,EAAE,IAAI,aAAa,KAAK,MAAM;AAAA,EAC9G;AAAA,EAEA,WAAW,QAAsB;AAC/B,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAExC,UAAM,kBAAkB,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAOvC;AACD,oBAAgB,IAAI,QAAQ,GAAG;AAAA,EACjC;AAAA,EAEA,QAAQ,QAAsB;AAE5B,UAAM,mBAAmB,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAOxC;AACD,qBAAiB,IAAI,MAAM;AAAA,EAC7B;AAAA,EAEA,YAAuB;AACrB,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,IACF,EAAE,IAAI;AAAA,EACR;AAAA;AAAA,EAIA,eACE,QACA,KACA,WACA,aACA,WAMgB;AAChB,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,UAAM,OAAO,KAAK,GAAG,QAAQ;AAAA;AAAA;AAAA,KAG5B,EAAE;AAAA,MACD;AAAA,MACA;AAAA,MACA;AAAA,MACA,eAAe;AAAA,MACf;AAAA,MACA,WAAW,YAAY;AAAA,MACvB,WAAW,mBAAmB;AAAA,MAC9B,WAAW,iBAAiB;AAAA,MAC5B,WAAW,kBAAkB;AAAA,IAC/B;AAEA,UAAM,YAAY,OAAO,KAAK,eAAe;AAG7C,SAAK,GAAG,QAAQ,uEAAuE,EACpF,IAAI,WAAW,KAAK,MAAM;AAE7B,WAAO,KAAK,GAAG,QAAQ,0CAA0C,EAAE,IAAI,SAAS;AAAA,EAClF;AAAA,EAEA,gBAAgB,QAAkC;AAChD,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,IACF,EAAE,IAAI,MAAM;AAAA,EACd;AAAA,EAEA,kBAAkB,QAAuC;AACvD,UAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,QAAI,CAAC,MAAM,mBAAoB,QAAO;AACtC,WAAQ,KAAK,GAAG,QAAQ,0CAA0C,EAAE,IAAI,KAAK,kBAAkB,KAAwB;AAAA,EACzH;AAAA;AAAA,EAIA,gBAAmC;AACjC,WAAO,KAAK,GAAG,QAAQ,mDAAmD,EAAE,IAAI;AAAA,EAClF;AAAA;AAAA,EAIA,YAAY,QAAwB;AAClC,UAAM,QAAkB,CAAC;AACzB,QAAI,UAAU,KAAK,QAAQ,MAAM;AACjC,WAAO,SAAS;AACd,YAAM,QAAQ,QAAQ,IAAI;AAC1B,gBAAU,QAAQ,YAAY,KAAK,QAAQ,QAAQ,SAAS,IAAI;AAAA,IAClE;AACA,WAAO,MAAM,MAAM,KAAK,GAAG;AAAA,EAC7B;AAAA;AAAA,EAIA,QAAQ,QAAgB,QAAuB,SAA8B;AAC3E,UAAM,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACxC,SAAK,GAAG,QAAQ,sFAAsF,EACnG,IAAI,QAAQ,QAAQ,SAAS,GAAG;AAAA,EACrC;AAAA;AAAA,EAIA,UAAU,QAAsB;AAC9B,UAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,QAAI,CAAC,KAAM;AACX,UAAM,WAAW,KAAK,YAAY,MAAM;AACxC,SAAK,GAAG,QAAQ,2DAA2D,EAAE,IAAI,QAAQ,KAAK,MAAM,QAAQ;AAAA,EAC9G;AAAA,EAEA,YAAY,QAAgB,SAAkB,SAAwB;AACpE,UAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,QAAI,CAAC,KAAM;AACX,UAAM,WAAW,KAAK,YAAY,MAAM;AAExC,QAAI,YAAY,UAAa,YAAY,QAAW;AAClD,WAAK,GAAG,QAAQ,gFAAgF,EAAE,IAAI,QAAQ,SAAS,OAAO;AAAA,IAChI;AACA,SAAK,GAAG,QAAQ,2DAA2D,EAAE,IAAI,QAAQ,KAAK,MAAM,QAAQ;AAAA,EAC9G;AAAA,EAEA,YAAY,OAAgE;AAC1E,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,IACF,EAAE,IAAI,KAAK;AAAA,EACb;AAAA;AAAA,EAIA,eAA8D;AAC5D,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,IACF,EAAE,IAAI;AAAA,EACR;AAAA;AAAA,EAIA,QAAc;AACZ,SAAK,GAAG,MAAM;AAAA,EAChB;AACF;;;AC3iBA,yBAA2B;AAcpB,IAAM,mBAAN,MAA+C;AAAA,EAC5C;AAAA,EAER,YAAY,QAAgB;AAC1B,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,IAAI,MAAsD;AAC9D,UAAM,WAAW,4BAA4B,KAAK,IAAI;AACtD,UAAM,SAAS,KAAK,QAAQ;AAAA;AAAA;AAAA;AAAA;AAC5B,UAAM,SAAS;AAAA,IAAS,QAAQ;AAAA;AAEhC,UAAM,OAAO,OAAO,OAAO;AAAA,MACzB,OAAO,KAAK,MAAM;AAAA,MAClB;AAAA,MACA,OAAO,KAAK,MAAM;AAAA,IACpB,CAAC;AAED,UAAM,MAAM,MAAM;AAAA,MAChB,GAAG,KAAK,MAAM;AAAA,MACd;AAAA,QACE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,iCAAiC,QAAQ,GAAG;AAAA,QACvE;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,MAAM,oBAAoB,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE;AAAA,IACtE;AAEA,UAAM,SAAS,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC;AAC1C,WAAO,EAAE,KAAK,OAAO,MAAM,MAAM,SAAS,OAAO,MAAM,EAAE,EAAE;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,IAAI,KAA8B;AACtC,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,MAAM,mBAAmB,GAAG,IAAI;AAAA,MAC9D,QAAQ;AAAA,IACV,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,MAAM,oBAAoB,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE;AAAA,IACtE;AAEA,WAAO,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,IAAI,KAA4B;AACpC,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,MAAM,uBAAuB,GAAG,IAAI;AAAA,MAClE,QAAQ;AAAA,IACV,CAAC;AACD,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,MAAM,oBAAoB,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE;AAAA,IACtE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAM,KAA4B;AACtC,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,MAAM,sBAAsB,GAAG,IAAI;AAAA,MACjE,QAAQ;AAAA,IACV,CAAC;AACD,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,UAAI,CAAC,KAAK,SAAS,YAAY,GAAG;AAChC,cAAM,IAAI,MAAM,sBAAsB,IAAI,MAAM,IAAI,IAAI,EAAE;AAAA,MAC5D;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAoB;AACxB,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,MAAM,mBAAmB;AAAA,MACvD,QAAQ;AAAA,IACV,CAAC;AACD,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI,MAAM,mBAAmB,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE;AAAA,IACrE;AAEA,UAAM,IAAI,KAAK;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAA6B;AACjC,QAAI;AACF,YAAM,aAAa,IAAI,gBAAgB;AACvC,YAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAI;AACzD,YAAM,MAAM,MAAM,MAAM,GAAG,KAAK,MAAM,cAAc;AAAA,QAClD,QAAQ;AAAA,QACR,QAAQ,WAAW;AAAA,MACrB,CAAC;AACD,mBAAa,OAAO;AACpB,aAAO,IAAI;AAAA,IACb,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAKO,SAAS,OAAO,MAAsB;AAC3C,aAAO,+BAAW,QAAQ,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AACvD;;;AC9HO,IAAM,qBAAN,MAAiD;AAAA,EAC9C,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EAER,YAAY,QAAsB;AAChC,SAAK,aAAa,OAAO,WAAW;AAGpC,QAAI,OAAO,KAAK;AACd,WAAK,UAAU;AAAA,QACb,iBAAiB,UAAU,OAAO,GAAG;AAAA,MACvC;AAAA,IACF,OAAO;AACL,WAAK,UAAU;AAAA,QACb,kBAAkB,OAAO;AAAA,QACzB,yBAAyB,OAAO;AAAA,MAClC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,IAAI,MAAsD;AAC9D,UAAM,WAAW,0BAA0B,KAAK,IAAI;AACpD,UAAM,WAAW,KAAK,UAAU;AAAA,MAC9B,MAAM,eAAe,KAAK,IAAI,CAAC;AAAA,MAC/B,WAAW,EAAE,KAAK,eAAe,WAAW,OAAO;AAAA,IACrD,CAAC;AAGD,UAAM,QAAkB,CAAC;AAGzB,UAAM,KAAK,OAAO;AAAA,MAChB,KAAK,QAAQ;AAAA;AAAA;AAAA;AAAA,IAGb,WAAW;AAAA,IACb,CAAC;AAGD,UAAM,KAAK,OAAO;AAAA,MAChB,KAAK,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,IAGf,CAAC;AACD,UAAM,KAAK,IAAI;AACf,UAAM,KAAK,OAAO,KAAK;AAAA,IAAS,QAAQ;AAAA,CAAQ,CAAC;AAEjD,UAAM,OAAO,OAAO,OAAO,KAAK;AAEhC,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,0BAA0B;AAAA,MAC/D,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,GAAG,KAAK;AAAA,QACR,gBAAgB,iCAAiC,QAAQ;AAAA,MAC3D;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,UAAU,MAAM,IAAI,KAAK;AAC/B,YAAM,IAAI,MAAM,yBAAyB,IAAI,MAAM,IAAI,OAAO,EAAE;AAAA,IAClE;AAEA,UAAM,SAAS,MAAM,IAAI,KAAK;AAC9B,WAAO;AAAA,MACL,KAAK,OAAO;AAAA,MACZ,MAAM,OAAO,WAAW,KAAK;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,IAAI,KAA8B;AACtC,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAK;AAE1D,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,GAAG,KAAK,UAAU,SAAS,GAAG,IAAI;AAAA,QACxD,QAAQ;AAAA,QACR,QAAQ,WAAW;AAAA,MACrB,CAAC;AAED,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI,MAAM,sBAAsB,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE;AAAA,MACxE;AAEA,aAAO,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC;AAAA,IAC5C,UAAE;AACA,mBAAa,OAAO;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,IAAI,KAA4B;AACpC,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,sBAAsB;AAAA,MAC3D,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,GAAG,KAAK;AAAA,QACR,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU,EAAE,WAAW,IAAI,CAAC;AAAA,IACzC,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,UAAU,MAAM,IAAI,KAAK;AAC/B,YAAM,IAAI,MAAM,sBAAsB,IAAI,MAAM,IAAI,OAAO,EAAE;AAAA,IAC/D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,MAAM,KAA4B;AACtC,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,kBAAkB,GAAG,IAAI;AAAA,MAC9D,QAAQ;AAAA,MACR,SAAS,KAAK;AAAA,IAChB,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,UAAU,MAAM,IAAI,KAAK;AAE/B,UAAI,IAAI,WAAW,KAAK;AACtB,cAAM,IAAI,MAAM,wBAAwB,IAAI,MAAM,IAAI,OAAO,EAAE;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAoB;AAAA,EAE1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,WAA6B;AACjC,QAAI;AACF,YAAM,aAAa,IAAI,gBAAgB;AACvC,YAAM,UAAU,WAAW,MAAM,WAAW,MAAM,GAAG,GAAI;AAEzD,YAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,4BAA4B;AAAA,QACjE,QAAQ;AAAA,QACR,SAAS,KAAK;AAAA,QACd,QAAQ,WAAW;AAAA,MACrB,CAAC;AAED,mBAAa,OAAO;AACpB,aAAO,IAAI;AAAA,IACb,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,KAAK,KAAgE;AACzE,QAAI;AACF,YAAM,MAAM,MAAM;AAAA,QAChB,GAAG,KAAK,OAAO,4CAA4C,GAAG;AAAA,QAC9D,EAAE,QAAQ,OAAO,SAAS,KAAK,QAAQ;AAAA,MACzC;AAEA,UAAI,CAAC,IAAI,GAAI,QAAO;AAEpB,YAAM,SAAS,MAAM,IAAI,KAAK;AAC9B,UAAI,OAAO,QAAQ,OAAO,KAAK,SAAS,GAAG;AACzC,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,MAAM,OAAO,KAAK,CAAC,EAAE,QAAQ;AAAA,QAC/B;AAAA,MACF;AACA,aAAO,EAAE,QAAQ,OAAO,MAAM,EAAE;AAAA,IAClC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,QAAqC;AAChD,QAAI,OAAO,SAAS;AAClB,WAAK,aAAa,OAAO;AAAA,IAC3B;AACA,QAAI,OAAO,KAAK;AACd,WAAK,UAAU,EAAE,iBAAiB,UAAU,OAAO,GAAG,GAAG;AAAA,IAC3D,WAAW,OAAO,UAAU,OAAO,cAAc;AAC/C,WAAK,UAAU;AAAA,QACb,kBAAkB,OAAO;AAAA,QACzB,yBAAyB,OAAO;AAAA,MAClC;AAAA,IACF;AAAA,EACF;AACF;;;AC5MO,IAAM,gBAAN,MAA4C;AAAA,EACzC;AAAA,EACA,cAAyC;AAAA,EACzC,WAA0B;AAAA,EAElC,YAAY,WAAyB;AACnC,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAAY,UAAyB,cAAmC;AACtE,SAAK,WAAW;AAEhB,QAAI,aAAa,YAAY,cAAc;AACzC,UAAI,KAAK,aAAa;AACpB,aAAK,YAAY,aAAa,YAAY;AAAA,MAC5C,OAAO;AACL,aAAK,cAAc,IAAI,mBAAmB,YAAY;AAAA,MACxD;AAAA,IACF;AAEA,YAAQ,IAAI,qCAAqC,QAAQ,EAAE;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA,EAKA,cAA6B;AAC3B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAyB;AACvB,WAAO,KAAK,aAAa,YAAY,KAAK,gBAAgB;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA,EAKA,iBAA4C;AAC1C,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,IAAI,MAAsD;AAO9D,UAAM,SAAS,MAAM,KAAK,UAAU,IAAI,IAAI;AAC5C,YAAQ,IAAI,oCAAoC,OAAO,GAAG,WAAW,KAAK,QAAQ,GAAG;AACrF,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,IAAI,KAA8B;AACtC,QAAI,KAAK,aAAa,YAAY,KAAK,aAAa;AAClD,UAAI;AACF,eAAO,MAAM,KAAK,YAAY,IAAI,GAAG;AAAA,MACvC,SAAS,KAAU;AAEjB,gBAAQ,KAAK,oDAAoD,IAAI,OAAO,EAAE;AAC9E,eAAO,KAAK,UAAU,IAAI,GAAG;AAAA,MAC/B;AAAA,IACF;AAEA,WAAO,KAAK,UAAU,IAAI,GAAG;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,IAAI,KAA4B;AACpC,QAAI,KAAK,aAAa,YAAY,KAAK,aAAa;AAClD,YAAM,KAAK,YAAY,IAAI,GAAG;AAC9B;AAAA,IACF;AACA,UAAM,KAAK,UAAU,IAAI,GAAG;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,MAAM,KAA4B;AACtC,QAAI,KAAK,aAAa,YAAY,KAAK,aAAa;AAClD,YAAM,KAAK,YAAY,MAAM,GAAG;AAChC;AAAA,IACF;AACA,UAAM,KAAK,UAAU,MAAM,GAAG;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAoB;AACxB,UAAM,KAAK,UAAU,GAAG;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,WAA6B;AACjC,QAAI,KAAK,aAAa,YAAY,KAAK,aAAa;AAClD,aAAO,KAAK,YAAY,SAAS;AAAA,IACnC;AACA,WAAO,KAAK,UAAU,SAAS;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAiC;AACrC,WAAO,KAAK,UAAU,SAAS;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAmC;AACvC,QAAI,CAAC,KAAK,YAAa,QAAO;AAC9B,WAAO,KAAK,YAAY,SAAS;AAAA,EACnC;AACF;;;AClJO,IAAM,qBAAN,MAAyB;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAoD;AAAA,EACpD,eAAe;AAAA,EACf,WAAW;AAAA,EACF,oBAAoB;AAAA;AAAA,EACpB,cAAc;AAAA,EACvB,gBAAqD;AAAA,EAE7D,YAAY,IAAc,OAAsB,WAAyB;AACvE,SAAK,KAAK;AACV,SAAK,QAAQ;AACb,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ,UAA4C;AAClD,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEQ,KAAK,OAAwB;AACnC,QAAI,KAAK,eAAe;AACtB,WAAK,cAAc,KAAK;AAAA,IAC1B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,QAAI,KAAK,WAAY;AAErB,YAAQ,IAAI,mCAAmC;AAG/C,SAAK,aAAa,EAAE;AAAA,MAAM,SACxB,QAAQ,MAAM,gCAAgC,IAAI,OAAO;AAAA,IAC3D;AAGA,SAAK,aAAa,YAAY,MAAM;AAClC,WAAK,aAAa,EAAE;AAAA,QAAM,SACxB,QAAQ,MAAM,iCAAiC,IAAI,OAAO;AAAA,MAC5D;AAAA,IACF,GAAG,KAAK,iBAAiB;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACX,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAC7B,WAAK,aAAa;AAAA,IACpB;AACA,YAAQ,IAAI,mCAAmC;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAA8B;AAClC,UAAM,KAAK,aAAa;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,YAAwE;AACtE,WAAO;AAAA,MACL,SAAS,KAAK,eAAe;AAAA,MAC7B,QAAQ,KAAK;AAAA,MACb,YAAY,KAAK;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,eAA8B;AAE1C,QAAI,KAAK,aAAc;AACvB,QAAI,CAAC,KAAK,MAAM,cAAc,GAAG;AAC/B,cAAQ,IAAI,yCAAoC;AAChD;AAAA,IACF;AAEA,SAAK,eAAe;AAEpB,QAAI;AAEF,YAAM,SAAS,MAAM,KAAK,MAAM,eAAe;AAE/C,UAAI,WAAW,KAAK,UAAU;AAC5B,aAAK,WAAW;AAChB,aAAK,KAAK;AAAA,UACR,MAAM,SAAS,gBAAgB;AAAA,QACjC,CAAC;AAAA,MACH;AAEA,UAAI,CAAC,QAAQ;AACX,aAAK,eAAe;AACpB;AAAA,MACF;AAGA,YAAM,aAAa,KAAK,GAAG,qBAAqB;AAEhD,UAAI,WAAW,WAAW,GAAG;AAC3B,aAAK,eAAe;AACpB;AAAA,MACF;AAEA,cAAQ,IAAI,qBAAqB,WAAW,MAAM,qBAAqB;AACvE,WAAK,KAAK,EAAE,MAAM,cAAc,MAAM,EAAE,OAAO,WAAW,OAAO,EAAE,CAAC;AAEpE,UAAI,YAAY;AAChB,UAAI,SAAS;AAEb,iBAAW,MAAM,YAAY;AAC3B,YAAI,GAAG,YAAY,KAAK,aAAa;AAEnC,eAAK,GAAG,sBAAsB,GAAG,IAAI,UAAU,sBAAsB;AACrE;AACA;AAAA,QACF;AAEA,YAAI;AACF,cAAI,GAAG,YAAY,iBAAiB;AAClC,kBAAM,KAAK,oBAAoB,EAAE;AACjC;AAAA,UACF,OAAO;AACL,oBAAQ,KAAK,kCAAkC,GAAG,OAAO,EAAE;AAC3D,iBAAK,GAAG,sBAAsB,GAAG,IAAI,UAAU,oBAAoB,GAAG,OAAO,EAAE;AAC/E;AAAA,UACF;AAAA,QACF,SAAS,KAAU;AACjB,kBAAQ,MAAM,oBAAoB,GAAG,EAAE,YAAY,IAAI,OAAO,EAAE;AAChE,eAAK,GAAG,sBAAsB,GAAG,IAAI,WAAW,IAAI,OAAO;AAC3D;AAGA,cAAI,IAAI,QAAQ,SAAS,OAAO,KAAK,IAAI,QAAQ,SAAS,SAAS,GAAG;AACpE,iBAAK,WAAW;AAChB,iBAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AAClC;AAAA,UACF;AAAA,QACF;AAEA,aAAK,KAAK;AAAA,UACR,MAAM;AAAA,UACN,MAAM,EAAE,WAAW,QAAQ,OAAO,WAAW,OAAO;AAAA,QACtD,CAAC;AAAA,MACH;AAGA,WAAK,GAAG,yBAAyB;AAEjC,WAAK,KAAK;AAAA,QACR,MAAM;AAAA,QACN,MAAM,EAAE,WAAW,OAAO;AAAA,MAC5B,CAAC;AAED,cAAQ,IAAI,2BAA2B,SAAS,eAAe,MAAM,SAAS;AAAA,IAChF,UAAE;AACA,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBAAoB,IAAuD;AACvF,UAAM,UAAU,KAAK,MAAM,GAAG,UAAU;AACxC,UAAM,EAAE,WAAW,IAAI,IAAI;AAG3B,SAAK,GAAG,iBAAiB,WAAW,SAAS;AAG7C,UAAM,OAAO,MAAM,KAAK,UAAU,IAAI,GAAG;AAGzC,UAAM,cAAc,KAAK,MAAM,eAAe;AAC9C,QAAI,CAAC,aAAa;AAChB,YAAM,IAAI,MAAM,4BAA4B;AAAA,IAC9C;AAEA,UAAM,SAAS,MAAM,YAAY,IAAI,IAAI;AAGzC,SAAK,GAAG,iBAAiB,WAAW,UAAU,OAAO,GAAG;AACxD,SAAK,GAAG,sBAAsB,GAAG,IAAI,WAAW;AAEhD,YAAQ,IAAI,sCAAsC,SAAS,WAAM,OAAO,GAAG,EAAE;AAAA,EAC/E;AACF;;;AC5LA,oBAAkC;AAClC,mBAAgD;AAChD,oBAAyB;AACzB,kBAAuB;AACvB,kBAAqB;AACrB,IAAAC,gBAAuC;AACvC,YAAuB;AACvB,qBAAoC;AAK7B,IAAM,WAAW;AAGxB,IAAM,gBAAgB;AAAA,EACpB,GAAG;AAAA;AAAA,EACH,GAAG;AAAA;AAAA,EACH,GAAG;AAAA;AACL;AAGA,IAAM,aAAa;AAGnB,IAAM,eAAe;AAGrB,IAAM,cAAc;AA0Db,SAASC,oBAA2B;AACzC,SAAa,uBAAiB,eAAAC,UAAS,GAAG;AAC5C;AAKO,SAASC,kBAAiB,UAA2B;AAC1D,SAAa,uBAAiB,UAAU,eAAAD,QAAO;AACjD;AAMA,eAAsBE,gBAAe,UAAuC;AAC1E,SAAa,qBAAe,QAAQ;AACtC;AAWO,SAAS,sBAAsB,UAAkB,MAA8B;AACpF,QAAM,gBAAgB,IAAI,YAAY,EAAE,OAAO,QAAQ;AACvD,aAAO,wBAAS,eAAe,MAAM;AAAA,IACnC,GAAG,cAAc;AAAA,IACjB,GAAG,cAAc;AAAA,IACjB,GAAG,cAAc;AAAA,IACjB,OAAO;AAAA,EACT,CAAC;AACH;AAMO,SAAS,kBAAkB,MAA8B;AAC9D,QAAM,OAAO,IAAI,YAAY,EAAE,OAAO,yBAAyB;AAC/D,aAAO,kBAAK,oBAAQ,MAAM,QAAW,MAAM,UAAU;AACvD;AAcO,SAAS,YAAY,WAAmB,WAAsC;AAEnF,QAAM,cAAU,aAAAC,aAAiB,UAAU;AAG3C,QAAM,gBAAY,aAAAA,aAAiB,YAAY;AAC/C,QAAM,aAAS,iCAAkB,SAAS,SAAS;AACnD,QAAM,aAAa,OAAO,QAAQ,IAAI,WAAW,SAAS,CAAC;AAG3D,QAAM,eAAW,aAAAA,aAAiB,YAAY;AAC9C,QAAM,gBAAY,iCAAkB,WAAW,QAAQ;AACvD,QAAM,aAAa,UAAU,QAAQ,OAAO;AAE5C,SAAO;AAAA,IACL,YAAY,OAAO,KAAK,UAAU;AAAA,IAClC,YAAY,OAAO,KAAK,UAAU;AAAA,IAClC,UAAU,OAAO,KAAK,QAAQ;AAAA,IAC9B,WAAW,OAAO,KAAK,SAAS;AAAA,EAClC;AACF;AAOO,SAAS,YAAY,QAA+B;AACzD,QAAM,EAAE,YAAY,YAAY,UAAU,WAAW,UAAU,IAAI;AAGnE,QAAM,gBAAY,iCAAkB,WAAW,IAAI,WAAW,QAAQ,CAAC;AACvE,QAAM,UAAU,UAAU,QAAQ,IAAI,WAAW,UAAU,CAAC;AAG5D,QAAM,iBAAa,iCAAkB,SAAS,IAAI,WAAW,SAAS,CAAC;AACvE,QAAM,YAAY,WAAW,QAAQ,IAAI,WAAW,UAAU,CAAC;AAE/D,SAAO,OAAO,KAAK,SAAS;AAC9B;AAYA,eAAsB,YAAY,UAI/B;AAED,QAAM,WAAWJ,kBAAiB;AAGlC,QAAM,WAAO,aAAAI,aAAiB,WAAW;AAGzC,QAAM,gBAAY,aAAAA,aAAiB,UAAU;AAG7C,QAAM,cAAc,sBAAsB,UAAU,IAAI;AAGxD,QAAM,iBAAa,aAAAA,aAAiB,YAAY;AAChD,QAAM,eAAW,iCAAkB,aAAa,UAAU;AAC1D,QAAM,uBAAuB,SAAS,QAAQ,SAAS;AAGvD,QAAM,OAAO,MAAMD,gBAAe,QAAQ;AAC1C,QAAM,cAAc,kBAAkB,IAAI;AAG1C,QAAM,qBAAiB,aAAAC,aAAiB,YAAY;AACpD,QAAM,qBAAiB,iCAAkB,aAAa,cAAc;AACpE,QAAM,qBAAqB,eAAe,QAAQ,SAAS;AAG3D,QAAM,mBAAe,oBAAO,IAAI,YAAY,EAAE,OAAO,QAAQ,CAAC;AAG9D,QAAM,oBAAgB,oBAAO,SAAS;AAEtC,QAAM,SAAsB;AAAA,IAC1B,UAAM,0BAAW,IAAI;AAAA,IACrB,wBAAoB,0BAAW,kBAAkB;AAAA,IACjD,oBAAgB,0BAAW,cAAc;AAAA,IACzC,0BAAsB,0BAAW,oBAAoB;AAAA,IACrD,gBAAY,0BAAW,UAAU;AAAA,IACjC,kBAAc,0BAAW,YAAY;AAAA,IACrC,mBAAe,0BAAW,aAAa;AAAA,EACzC;AAEA,SAAO,EAAE,UAAU,QAAQ,UAAU;AACvC;AAOO,SAAS,YAAY,UAAkB,QAAwC;AACpF,QAAM,WAAO,0BAAW,OAAO,IAAI;AACnC,QAAM,cAAc,sBAAsB,UAAU,IAAI;AAGxD,MAAI;AACJ,MAAI;AACF,UAAM,cAAU,0BAAW,OAAO,UAAU;AAC5C,UAAM,kBAAc,0BAAW,OAAO,oBAAoB;AAC1D,UAAM,aAAS,iCAAkB,aAAa,OAAO;AACrD,gBAAY,OAAO,QAAQ,WAAW;AAAA,EACxC,QAAQ;AACN,WAAO;AAAA,EACT;AAGA,QAAM,mBAAe,8BAAW,oBAAO,SAAS,CAAC;AACjD,MAAI,iBAAiB,OAAO,eAAe;AACzC,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAQA,eAAsB,aACpB,UACA,aACA,WACgE;AAEhE,MAAI,CAACF,kBAAiB,QAAQ,GAAG;AAC/B,WAAO;AAAA,EACT;AAGA,QAAM,mBAAe,8BAAW,oBAAO,IAAI,YAAY,EAAE,OAAO,QAAQ,CAAC,CAAC;AAC1E,MAAI,iBAAiB,UAAU,cAAc;AAC3C,WAAO;AAAA,EACT;AAGA,QAAM,OAAO,MAAMC,gBAAe,QAAQ;AAC1C,QAAM,cAAc,kBAAkB,IAAI;AAG1C,MAAI;AACJ,MAAI;AACF,UAAM,YAAQ,0BAAW,UAAU,cAAc;AACjD,UAAM,yBAAqB,0BAAW,UAAU,kBAAkB;AAClE,UAAM,aAAS,iCAAkB,aAAa,KAAK;AACnD,gBAAY,OAAO,QAAQ,kBAAkB;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AAGA,QAAM,mBAAe,8BAAW,oBAAO,SAAS,CAAC;AACjD,MAAI,iBAAiB,UAAU,eAAe;AAC5C,WAAO;AAAA,EACT;AAGA,QAAM,cAAU,aAAAC,aAAiB,WAAW;AAC5C,QAAM,iBAAiB,sBAAsB,aAAa,OAAO;AAEjE,QAAM,oBAAgB,aAAAA,aAAiB,YAAY;AACnD,QAAM,eAAW,iCAAkB,gBAAgB,aAAa;AAChE,QAAM,0BAA0B,SAAS,QAAQ,SAAS;AAG1D,QAAM,wBAAoB,aAAAA,aAAiB,YAAY;AACvD,QAAM,wBAAoB,iCAAkB,aAAa,iBAAiB;AAC1E,QAAM,wBAAwB,kBAAkB,QAAQ,SAAS;AAEjE,QAAM,SAAsB;AAAA,IAC1B,UAAM,0BAAW,OAAO;AAAA,IACxB,wBAAoB,0BAAW,qBAAqB;AAAA,IACpD,oBAAgB,0BAAW,iBAAiB;AAAA,IAC5C,0BAAsB,0BAAW,uBAAuB;AAAA,IACxD,gBAAY,0BAAW,aAAa;AAAA,IACpC,cAAc,UAAU;AAAA;AAAA,IACxB,eAAe,UAAU;AAAA;AAAA,EAC3B;AAEA,SAAO,EAAE,WAAW,OAAO;AAC7B;AAMA,eAAsB,eACpB,aACA,aACA,WAC6B;AAE7B,QAAM,YAAY,YAAY,aAAa,SAAS;AACpD,MAAI,CAAC,UAAW,QAAO;AAGvB,QAAM,cAAU,aAAAA,aAAiB,WAAW;AAC5C,QAAM,iBAAiB,sBAAsB,aAAa,OAAO;AAGjE,QAAM,oBAAgB,aAAAA,aAAiB,YAAY;AACnD,QAAM,eAAW,iCAAkB,gBAAgB,aAAa;AAChE,QAAM,0BAA0B,SAAS,QAAQ,SAAS;AAG1D,SAAO;AAAA,IACL,GAAG;AAAA,IACH,UAAM,0BAAW,OAAO;AAAA,IACxB,0BAAsB,0BAAW,uBAAuB;AAAA,IACxD,gBAAY,0BAAW,aAAa;AAAA;AAAA,EAEtC;AACF;AASO,SAAS,QAAQ,KAAuB;AAC7C,MAAI,KAAK,CAAC;AACZ;;;APnWA,SAAS,WAAW,KAAwB;AAC1C,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,UAAU,IAAI;AAAA,IACd,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,WAAW,IAAI;AAAA,IACf,YAAY,IAAI;AAAA,IAChB,WAAW,IAAI;AAAA,IACf,kBAAkB,IAAI;AAAA,EACxB;AACF;AAEA,SAAS,cAAc,KAAuB;AAC5C,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,KAAK,IAAI;AAAA,IACT,WAAW,IAAI;AAAA,IACf,aAAa,IAAI;AAAA,IACjB,WAAW,IAAI;AAAA,IACf,SAAS,IAAI;AAAA,IACb,UAAU,IAAI,YAAY;AAAA,IAC1B,WAAW,IAAI,cAAc;AAAA,IAC7B,YAAY,IAAI,eAAe;AAAA,EACjC;AACF;AAIO,IAAM,cAAN,cAA0B,iCAAa;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,gBAA2C;AAAA;AAAA,EAG3C,YAA+B;AAAA,EAC/B,YAAkD;AAAA,EAClD,UAAU;AAAA,EAElB,YAAY,SAA6B;AACvC,UAAM;AACN,SAAK,OAAO;AAAA,MACV,SAAS,QAAQ;AAAA,MACjB,gBAAgB,QAAQ,kBAAkB;AAAA,MAC1C,aAAa,QAAQ,eAAe;AAAA,MACpC,YAAY,QAAQ,cAAc,KAAK,KAAK;AAAA,MAC5C,iBAAiB,QAAQ,mBAAmB;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,QAAuB;AAC3B,QAAI,KAAK,QAAS;AAElB,UAAM,EAAE,SAAS,gBAAgB,YAAY,IAAI,KAAK;AAGtD,QAAI,KAAC,4BAAW,OAAO,GAAG;AACxB,qCAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,IACxC;AAGA,SAAK,KAAK,IAAI,SAAS,OAAO;AAG9B,UAAM,kBAAkB,kBAAkB,kBAAAC,QAAK,QAAQ,kBAAAA,QAAK,KAAK,SAAS,MAAM,QAAQ,MAAM,CAAC;AAC/F,SAAK,OAAO,IAAI,YAAY;AAAA,MAC1B,gBAAgB;AAAA,MAChB,UAAU,kBAAAA,QAAK,KAAK,SAAS,WAAW;AAAA,MACxC,SAAS;AAAA,IACX,CAAC;AAGD,SAAK,KAAK,GAAG,SAAS,MAAM,KAAK,UAAU,YAAY,CAAC;AACxD,SAAK,KAAK,GAAG,SAAS,CAAC,SAAc,KAAK,UAAU,cAAc,IAAI,CAAC;AACvE,SAAK,KAAK,GAAG,OAAO,CAAC,QAAgB;AAAA,IAErC,CAAC;AAGD,UAAM,KAAK,KAAK,MAAM;AAGtB,SAAK,YAAY,IAAI,iBAAiB,oBAAoB,WAAW,EAAE;AACvE,SAAK,QAAQ,IAAI,cAAc,KAAK,SAAS;AAG7C,SAAK,mBAAmB;AAGxB,SAAK,gBAAgB,IAAI,mBAAmB,KAAK,IAAI,KAAK,OAAO,KAAK,SAAS;AAC/E,SAAK,cAAc,QAAQ,CAAC,UAAU;AACpC,WAAK,UAAU,MAAM,MAAwB,MAAM,IAAI;AAAA,IACzD,CAAC;AACD,SAAK,cAAc,MAAM;AAEzB,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,QAAI,CAAC,KAAK,QAAS;AAGnB,SAAK,KAAK;AAGV,QAAI,KAAK,eAAe;AACtB,WAAK,cAAc,KAAK;AACxB,WAAK,gBAAgB;AAAA,IACvB;AAGA,UAAM,KAAK,KAAK,KAAK;AAGrB,SAAK,GAAG,MAAM;AAEd,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKQ,gBAAsB;AAC5B,QAAI,CAAC,KAAK,SAAS;AACjB,YAAM,IAAI,MAAM,yDAAoD;AAAA,IACtE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAAuB;AAC7B,SAAK,cAAc;AACnB,QAAI,CAAC,KAAK,WAAW;AACnB,YAAM,IAAI,MAAM,kDAA6C;AAAA,IAC/D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,OAAO,UAA8C;AACzD,SAAK,cAAc;AAEnB,QAAI,KAAK,GAAG,mBAAmB,GAAG;AAChC,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AACA,QAAI,CAAC,YAAY,SAAS,SAAS,GAAG;AACpC,YAAM,IAAI,MAAM,wCAAwC;AAAA,IAC1D;AAEA,UAAM,EAAE,UAAU,QAAQ,UAAU,IAAI,MAAM,YAAkB,QAAQ;AACxE,SAAK,gBAAgB,MAAM;AAC3B,SAAK,YAAY;AACjB,SAAK,eAAe;AAEpB,SAAK,UAAU,eAAe;AAC9B,SAAK,UAAU,gBAAgB;AAE/B,WAAO,EAAE,SAAS;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,OAAO,UAA2B;AAChC,SAAK,cAAc;AAEnB,UAAM,SAAS,KAAK,gBAAgB;AACpC,UAAM,YAAY,YAAkB,UAAU,MAAM;AACpD,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,kBAAkB;AAAA,IACpC;AAEA,SAAK,YAAY;AACjB,SAAK,eAAe;AACpB,SAAK,UAAU,gBAAgB;AAE/B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACX,QAAI,KAAK,WAAW;AAClB,cAAQ,KAAK,SAAS;AACtB,WAAK,YAAY;AAAA,IACnB;AACA,QAAI,KAAK,WAAW;AAClB,mBAAa,KAAK,SAAS;AAC3B,WAAK,YAAY;AAAA,IACnB;AACA,SAAK,UAAU,gBAAgB,EAAE,QAAQ,SAAS,CAAC;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,QAAQ,UAAkB,aAAuC;AACrE,SAAK,cAAc;AAEnB,UAAM,SAAS,KAAK,gBAAgB;AACpC,UAAM,SAAS,MAAM,aAAmB,UAAU,aAAa,MAAM;AACrE,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,yCAAoC;AAAA,IACtD;AAEA,SAAK,gBAAgB,OAAO,MAAM;AAClC,SAAK,YAAY,OAAO;AACxB,SAAK,eAAe;AACpB,SAAK,UAAU,gBAAgB;AAE/B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,eAAe,aAAqB,aAAuC;AAC/E,SAAK,cAAc;AAEnB,UAAM,SAAS,KAAK,gBAAgB;AACpC,UAAM,YAAY,MAAM,eAAqB,aAAa,aAAa,MAAM;AAC7E,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,0BAA0B;AAAA,IAC5C;AAEA,SAAK,gBAAgB,SAAS;AAC9B,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,eAA0B;AACxB,SAAK,cAAc;AACnB,WAAO;AAAA,MACL,aAAa,KAAK,GAAG,mBAAmB;AAAA,MACxC,UAAU,KAAK,cAAc;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAsB;AACpB,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,QAAQ,MAAc,SAAsE;AAChG,SAAK,cAAc;AAEnB,UAAM,WAAW,QAAQ,YAAY,KAAK,GAAG,YAAY,EAAE;AAC3D,UAAM,YAAY;AAClB,UAAM,OAAO,OAAO,SAAS;AAE7B,QAAI;AACJ,QAAI;AAEJ,QAAI,KAAK,WAAW;AAClB,YAAM,YAAY,YAAY,WAAW,KAAK,SAAS;AACvD,oBAAc,UAAU;AACxB,kBAAY;AAAA,QACV,UAAU;AAAA,QACV,iBAAiB,UAAU;AAAA,QAC3B,eAAe,UAAU;AAAA,QACzB,gBAAgB,UAAU;AAAA,MAC5B;AAAA,IACF,OAAO;AACL,oBAAc;AAAA,IAChB;AAEA,UAAM,EAAE,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM,IAAI,WAAW;AACtD,UAAM,UAAU,KAAK,GAAG,eAAe,UAAU,QAAQ,IAAI;AAC7D,UAAM,aAAa,KAAK,GAAG,eAAe,QAAQ,IAAI,KAAK,MAAM,MAAM,SAAS;AAChF,SAAK,GAAG,UAAU,QAAQ,EAAE;AAG5B,QAAI,KAAK,MAAM,cAAc,GAAG;AAC9B,WAAK,GAAG,iBAAiB,WAAW,IAAI,SAAS;AACjD,WAAK,GAAG,oBAAoB,iBAAiB,EAAE,WAAW,WAAW,IAAI,IAAI,CAAC;AAAA,IAChF;AAEA,SAAK,eAAe;AACpB,SAAK,UAAU,cAAc,EAAE,QAAQ,QAAQ,IAAI,MAAM,QAAQ,MAAM,IAAI,CAAC;AAE5E,WAAO;AAAA,MACL,MAAM,WAAW,OAAO;AAAA,MACxB,SAAS,cAAc,UAAU;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,gBAAgB,UAAkB,SAAwE;AAC9G,UAAM,WAAO,8BAAa,QAAQ;AAClC,UAAM,OAAO,SAAS,QAAQ,kBAAAA,QAAK,SAAS,QAAQ;AACpD,WAAO,KAAK,QAAQ,MAAM,EAAE,MAAM,UAAU,SAAS,SAAS,CAAC;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,QAAQ,QAAwC;AACpD,SAAK,cAAc;AAEnB,UAAM,UAAU,KAAK,GAAG,kBAAkB,MAAM;AAChD,QAAI,CAAC,QAAS,OAAM,IAAI,MAAM,gCAAgC;AAE9D,UAAM,UAAU,KAAK,GAAG,QAAQ,MAAM;AACtC,QAAI,CAAC,QAAS,OAAM,IAAI,MAAM,QAAQ,MAAM,YAAY;AAGxD,UAAM,UAAU,MAAM,KAAK,MAAM,IAAI,QAAQ,GAAG;AAEhD,QAAI;AACJ,QAAI,QAAQ,aAAa,UAAU,QAAQ,aAAa,MAAM;AAC5D,UAAI,CAAC,KAAK,WAAW;AACnB,cAAM,IAAI,MAAM,kEAA6D;AAAA,MAC/E;AACA,UAAI,CAAC,QAAQ,mBAAmB,CAAC,QAAQ,iBAAiB,CAAC,QAAQ,gBAAgB;AACjF,cAAM,IAAI,MAAM,mDAAmD;AAAA,MACrE;AACA,aAAO,YAAY;AAAA,QACjB,YAAY;AAAA,QACZ,YAAY,QAAQ;AAAA,QACpB,UAAU,QAAQ;AAAA,QAClB,WAAW,QAAQ;AAAA,QACnB,WAAW,KAAK;AAAA,MAClB,CAAC;AAAA,IACH,OAAO;AACL,aAAO;AAAA,IACT;AAEA,SAAK,eAAe;AAEpB,WAAO;AAAA,MACL,MAAM,WAAW,OAAO;AAAA,MACxB;AAAA,MACA,KAAK,QAAQ;AAAA,MACb,MAAM,KAAK;AAAA,IACb;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAW,UAAkC;AAC3C,SAAK,cAAc;AAEnB,UAAM,UAAU,WAAW,KAAK,GAAG,QAAQ,QAAQ,IAAK,KAAK,GAAG,YAAY;AAC5E,UAAM,WAAW,KAAK,GAAG,aAAa,QAAQ,EAAE;AAEhD,WAAO;AAAA,MACL,MAAM,WAAW,OAAO;AAAA,MACxB,UAAU,SAAS,IAAI,UAAU;AAAA,IACnC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ,QAAiC;AACvC,SAAK,cAAc;AACnB,UAAM,MAAM,KAAK,GAAG,QAAQ,MAAM;AAClC,WAAO,MAAM,WAAW,GAAG,IAAI;AAAA,EACjC;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,QAA4B;AACtC,SAAK,cAAc;AACnB,UAAM,OAAO,KAAK,GAAG,YAAY,MAAM;AACvC,WAAO,KAAK,IAAI,UAAU;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,cAAwB;AACtB,SAAK,cAAc;AACnB,WAAO,WAAW,KAAK,GAAG,YAAY,CAAC;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,aAAa,MAAc,UAA6B;AACtD,SAAK,cAAc;AAEnB,UAAM,MAAM,YAAY,KAAK,GAAG,YAAY,EAAE;AAC9C,UAAM,SAAS,KAAK,GAAG,aAAa,KAAK,IAAI;AAC7C,SAAK,GAAG,UAAU,OAAO,EAAE;AAE3B,SAAK,UAAU,kBAAkB,EAAE,QAAQ,OAAO,IAAI,KAAK,CAAC;AAC5D,WAAO,WAAW,MAAM;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,QAAgB,SAAuB;AAC5C,SAAK,cAAc;AACnB,SAAK,GAAG,OAAO,QAAQ,OAAO;AAC9B,SAAK,UAAU,gBAAgB,EAAE,QAAQ,QAAQ,CAAC;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA,EAKA,KAAK,QAAgB,aAA2B;AAC9C,SAAK,cAAc;AACnB,SAAK,GAAG,SAAS,QAAQ,WAAW;AACpC,SAAK,UAAU,cAAc,EAAE,QAAQ,YAAY,CAAC;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,QAA+B;AAC1C,SAAK,cAAc;AAEnB,UAAM,aAAa,KAAK,GAAG,qBAAqB,MAAM;AACtD,SAAK,GAAG,WAAW,MAAM;AAGzB,QAAI,WAAW,SAAS,KAAK,KAAK,MAAM,cAAc,GAAG;AACvD,YAAM,cAAc,KAAK,MAAM,eAAe;AAC9C,UAAI,aAAa;AACf,mBAAW,OAAO,YAAY;AAC5B,cAAI;AACF,kBAAM,YAAY,MAAM,GAAG;AAAA,UAC7B,SAAS,KAAU;AAAA,UAEnB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,SAAK,UAAU,gBAAgB,EAAE,OAAO,CAAC;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKA,QAAQ,QAAsB;AAC5B,SAAK,cAAc;AACnB,SAAK,GAAG,QAAQ,MAAM;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,YAAwB;AACtB,SAAK,cAAc;AACnB,WAAO,KAAK,GAAG,UAAU,EAAE,IAAI,UAAU;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,QAA+B;AACzC,SAAK,cAAc;AACnB,WAAO,KAAK,GAAG,gBAAgB,MAAM,EAAE,IAAI,aAAa;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,OAA2B;AAChC,SAAK,cAAc;AACnB,UAAM,aAAa,KAAK,GAAG,YAAY,KAAK;AAC5C,WAAO,WACJ,IAAI,OAAK,KAAK,GAAG,QAAQ,EAAE,KAAK,CAAC,EACjC,OAAO,CAAC,QAAwB,QAAQ,IAAI,EAC5C,IAAI,UAAU;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,eAAe,aAAkD;AACrE,SAAK,cAAc;AAEnB,UAAM,SAAuB;AAAA,MAC3B,QAAQ,YAAY,UAAU;AAAA,MAC9B,cAAc,YAAY,gBAAgB;AAAA,MAC1C,KAAK,YAAY,OAAO;AAAA,MACxB,SAAS,YAAY,WAAW;AAAA,IAClC;AAEA,SAAK,MAAM,YAAY,UAAU,MAAM;AACvC,UAAM,SAAS,MAAM,KAAK,MAAM,eAAe;AAE/C,QAAI,QAAQ;AACV,UAAI,YAAY,IAAK,MAAK,GAAG,WAAW,cAAc,YAAY,GAAG;AACrE,UAAI,YAAY,OAAQ,MAAK,GAAG,WAAW,kBAAkB,YAAY,MAAM;AAC/E,UAAI,YAAY,aAAc,MAAK,GAAG,WAAW,qBAAqB,YAAY,YAAY;AAC9F,UAAI,YAAY,QAAS,MAAK,GAAG,WAAW,kBAAkB,YAAY,OAAO;AACjF,WAAK,GAAG,WAAW,kBAAkB,QAAQ;AAC7C,WAAK,UAAU,oBAAoB,EAAE,UAAU,SAAS,CAAC;AAAA,IAC3D,OAAO;AACL,WAAK,MAAM,YAAY,OAAO;AAAA,IAChC;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY,SAAiC;AAC3C,SAAK,cAAc;AAEnB,QAAI,SAAS;AACX,YAAM,MAAM,KAAK,GAAG,WAAW,YAAY;AAC3C,YAAM,SAAS,KAAK,GAAG,WAAW,gBAAgB;AAClD,YAAM,YAAY,KAAK,GAAG,WAAW,mBAAmB;AAExD,UAAI,CAAC,OAAO,EAAE,UAAU,YAAY;AAClC,cAAM,IAAI,MAAM,qEAAgE;AAAA,MAClF;AAEA,WAAK,MAAM,YAAY,UAAU;AAAA,QAC/B,QAAQ,UAAU;AAAA,QAClB,cAAc,aAAa;AAAA,QAC3B,KAAK,OAAO;AAAA,QACZ,SAAS,KAAK,GAAG,WAAW,gBAAgB,KAAK;AAAA,MACnD,CAAC;AACD,WAAK,GAAG,WAAW,kBAAkB,QAAQ;AAAA,IAC/C,OAAO;AACL,WAAK,MAAM,YAAY,OAAO;AAC9B,WAAK,GAAG,WAAW,kBAAkB,OAAO;AAAA,IAC9C;AAEA,UAAM,WAAW,KAAK,MAAM,YAAY;AACxC,SAAK,UAAU,iBAAiB,EAAE,SAAS,SAAS,CAAC;AACrD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,iBAA8B;AAC5B,SAAK,cAAc;AACnB,WAAO;AAAA,MACL,UAAU,KAAK,MAAM,YAAY;AAAA,MACjC,QAAQ,KAAK,MAAM,cAAc;AAAA,MACjC,gBAAgB,CAAC,EAAE,KAAK,GAAG,WAAW,YAAY,KAAK,KAAK,GAAG,WAAW,gBAAgB;AAAA,MAC1F,SAAS,KAAK,GAAG,WAAW,gBAAgB;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,gBAA4B;AAC1B,SAAK,cAAc;AAEnB,UAAM,QAAQ,KAAK,GAAG,aAAa;AACnC,UAAM,SAAiC,CAAC;AACxC,QAAI,QAAQ;AACZ,eAAW,KAAK,OAAO;AACrB,aAAO,EAAE,WAAW,IAAI,EAAE;AAC1B,eAAS,EAAE;AAAA,IACb;AAEA,WAAO;AAAA,MACL;AAAA,MACA,QAAQ,OAAO,QAAQ,KAAK;AAAA,MAC5B,UAAU,OAAO,SAAS,KAAK,MAAM,OAAO,SAAS,KAAK;AAAA,MAC1D,QAAQ,OAAO,OAAO,KAAK;AAAA,MAC3B,UAAU,KAAK,MAAM,YAAY;AAAA,IACnC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAmC;AACvC,SAAK,cAAc;AACnB,WAAO;AAAA,MACL,WAAW,KAAK,KAAK;AAAA,MACrB,SAAS,KAAK,KAAK;AAAA,MACnB,UAAU,WAAW,KAAK,GAAG,YAAY,CAAC;AAAA,MAC1C,YAAY,MAAM,KAAK,MAAM,aAAa;AAAA,MAC1C,kBAAkB,KAAK,GAAG,mBAAmB;AAAA,MAC7C,eAAe,KAAK,cAAc;AAAA,MAClC,eAAe,KAAK,MAAM,YAAY;AAAA,MACtC,aAAa,KAAK,MAAM,cAAc;AAAA,IACxC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAqC;AAC3C,UAAM,MAAM,KAAK,GAAG,eAAe;AACnC,QAAI,CAAC,IAAK,OAAM,IAAI,MAAM,uBAAuB;AACjD,WAAO;AAAA,MACL,MAAM,IAAI;AAAA,MACV,oBAAoB,IAAI;AAAA,MACxB,gBAAgB,IAAI;AAAA,MACpB,sBAAsB,IAAI;AAAA,MAC1B,YAAY,IAAI;AAAA,MAChB,cAAc,IAAI;AAAA,MAClB,eAAe,IAAI;AAAA,IACrB;AAAA,EACF;AAAA,EAEQ,gBAAgB,QAAiC;AACvD,SAAK,GAAG,eAAe;AAAA,MACrB,MAAM,OAAO;AAAA,MACb,oBAAoB,OAAO;AAAA,MAC3B,gBAAgB,OAAO;AAAA,MACvB,sBAAsB,OAAO;AAAA,MAC7B,YAAY,OAAO;AAAA,MACnB,cAAc,OAAO;AAAA,MACrB,eAAe,OAAO;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEQ,qBAA2B;AACjC,UAAM,WAAW,KAAK,GAAG,WAAW,gBAAgB;AACpD,QAAI,aAAa,UAAU;AACzB,YAAM,SAAS,KAAK,GAAG,WAAW,gBAAgB;AAClD,YAAM,YAAY,KAAK,GAAG,WAAW,mBAAmB;AACxD,YAAM,UAAU,KAAK,GAAG,WAAW,gBAAgB;AACnD,YAAM,MAAM,KAAK,GAAG,WAAW,YAAY;AAE3C,UAAI,OAAQ,UAAU,WAAY;AAChC,aAAK,MAAM,YAAY,UAAU;AAAA,UAC/B,QAAQ,UAAU;AAAA,UAClB,cAAc,aAAa;AAAA,UAC3B,SAAS,WAAW;AAAA,UACpB,KAAK,OAAO;AAAA,QACd,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,iBAAuB;AAC7B,QAAI,KAAK,KAAK,gBAAiB;AAE/B,QAAI,KAAK,WAAW;AAClB,mBAAa,KAAK,SAAS;AAAA,IAC7B;AACA,SAAK,YAAY,WAAW,MAAM;AAChC,WAAK,KAAK;AACV,WAAK,UAAU,gBAAgB,EAAE,QAAQ,aAAa,CAAC;AAAA,IACzD,GAAG,KAAK,KAAK,UAAU;AAAA,EACzB;AAAA,EAEQ,UAAU,MAAsB,MAAkB;AACxD,UAAM,QAAoB,EAAE,MAAM,MAAM,WAAW,KAAK,IAAI,EAAE;AAC9D,SAAK,KAAK,MAAM,KAAK;AACrB,SAAK,KAAK,KAAK,KAAK;AAAA,EACtB;AACF;","names":["import_node_events","import_node_fs","import_node_path","path","import_node_path","import_node_fs","path","BetterSqlite3","import_utils","generateMnemonic","english","validateMnemonic","mnemonicToSeed","nobleRandomBytes","path"]}
@@ -0,0 +1,303 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ /**
4
+ * @merklevault/core — Types publics du SDK
5
+ *
6
+ * Ces types constituent l'API publique du SDK.
7
+ * Toute modification est un breaking change.
8
+ */
9
+ interface MerkleVaultOptions {
10
+ /** Chemin vers le dossier de donnees du vault */
11
+ dataDir: string;
12
+ /** Chemin vers le binaire Kubo/IPFS (optionnel, auto-detecte sinon) */
13
+ kuboBinaryPath?: string;
14
+ /** Port de l'API Kubo (defaut: 5101) */
15
+ kuboApiPort?: number;
16
+ /** Delai d'auto-lock en millisecondes (defaut: 15 min = 900000) */
17
+ autoLockMs?: number;
18
+ /** Desactiver l'auto-lock (defaut: false) */
19
+ disableAutoLock?: boolean;
20
+ }
21
+ interface FileNode {
22
+ id: number;
23
+ parentId: number | null;
24
+ name: string;
25
+ kind: 'file' | 'folder';
26
+ createdAt: number;
27
+ modifiedAt: number;
28
+ deletedAt: number | null;
29
+ currentVersionId: number | null;
30
+ }
31
+ interface FileVersion {
32
+ id: number;
33
+ nodeId: number;
34
+ cid: string;
35
+ sizeBytes: number;
36
+ sha256Plain: string | null;
37
+ createdAt: number;
38
+ encAlgo: string;
39
+ provider: 'local' | 'pinata';
40
+ pinataCid: string | null;
41
+ syncStatus: SyncStatusValue;
42
+ }
43
+ type SyncStatusValue = 'none' | 'pending' | 'syncing' | 'synced' | 'error';
44
+ interface VaultInfo {
45
+ initialized: boolean;
46
+ unlocked: boolean;
47
+ }
48
+ interface CreateVaultResult {
49
+ /** Phrase de recuperation BIP39 (24 mots) — a afficher UNE SEULE FOIS */
50
+ mnemonic: string;
51
+ }
52
+ type CloudProvider = 'local' | 'pinata';
53
+ interface PinataCredentials {
54
+ apiKey?: string;
55
+ secretApiKey?: string;
56
+ jwt?: string;
57
+ gateway?: string;
58
+ }
59
+ interface CloudConfig {
60
+ provider: CloudProvider;
61
+ active: boolean;
62
+ hasCredentials: boolean;
63
+ gateway: string | null;
64
+ }
65
+ interface SyncStatus {
66
+ total: number;
67
+ synced: number;
68
+ pending: number;
69
+ errors: number;
70
+ provider: CloudProvider;
71
+ }
72
+ interface AddFileResult {
73
+ node: FileNode;
74
+ version: FileVersion;
75
+ cid: string;
76
+ }
77
+ interface GetFileResult {
78
+ node: FileNode;
79
+ data: Buffer;
80
+ cid: string;
81
+ size: number;
82
+ }
83
+ interface FolderListing {
84
+ node: FileNode;
85
+ children: FileNode[];
86
+ }
87
+ interface DaemonStatus {
88
+ kuboReady: boolean;
89
+ kuboPid: number | null;
90
+ rootNode: FileNode;
91
+ ipfsOnline: boolean;
92
+ vaultInitialized: boolean;
93
+ vaultUnlocked: boolean;
94
+ cloudProvider: CloudProvider;
95
+ cloudActive: boolean;
96
+ }
97
+ type VaultEventType = 'vault:created' | 'vault:unlocked' | 'vault:locked' | 'file:added' | 'file:deleted' | 'file:renamed' | 'file:moved' | 'folder:created' | 'sync:started' | 'sync:progress' | 'sync:complete' | 'sync:error' | 'cloud:configured' | 'cloud:toggled' | 'kubo:ready' | 'kubo:crash' | 'error';
98
+ interface VaultEvent {
99
+ type: VaultEventType;
100
+ data?: any;
101
+ timestamp: number;
102
+ }
103
+
104
+ /**
105
+ * @merklevault/core — Classe facade principale
106
+ *
107
+ * Encapsule toute la logique metier de MerkleVault :
108
+ * - Gestion du vault (creation, deverrouillage, recovery)
109
+ * - CRUD fichiers/dossiers sur IPFS (Kubo)
110
+ * - Chiffrement E2E (XChaCha20-Poly1305 + Argon2id)
111
+ * - Synchronisation cloud (Pinata)
112
+ *
113
+ * Usage :
114
+ * import { MerkleVault } from '@merklevault/core';
115
+ * const vault = new MerkleVault({ dataDir: './my-vault' });
116
+ * await vault.start();
117
+ * await vault.create({ password: 'secret' });
118
+ * const file = await vault.addFile(buffer, { name: 'doc.pdf' });
119
+ */
120
+
121
+ declare class MerkleVault extends EventEmitter {
122
+ private opts;
123
+ private kubo;
124
+ private db;
125
+ private store;
126
+ private kuboStore;
127
+ private syncProcessor;
128
+ private masterKey;
129
+ private lockTimer;
130
+ private started;
131
+ constructor(options: MerkleVaultOptions);
132
+ /**
133
+ * Demarre le vault : initialise la DB, lance Kubo, restaure la config cloud.
134
+ * Doit etre appele avant toute autre operation.
135
+ */
136
+ start(): Promise<void>;
137
+ /**
138
+ * Arrete le vault proprement : ferme Kubo, la DB, le sync processor.
139
+ */
140
+ stop(): Promise<void>;
141
+ /**
142
+ * Verifie que le vault est demarre, sinon throw.
143
+ */
144
+ private ensureStarted;
145
+ /**
146
+ * Verifie que le vault est deverrouille, sinon throw.
147
+ */
148
+ private ensureUnlocked;
149
+ /**
150
+ * Cree un nouveau vault avec un mot de passe.
151
+ * Retourne la phrase de recuperation BIP39 (24 mots).
152
+ *
153
+ * @param password - Mot de passe maitre (min 8 caracteres)
154
+ * @returns Phrase de recuperation a afficher UNE SEULE FOIS
155
+ */
156
+ create(password: string): Promise<CreateVaultResult>;
157
+ /**
158
+ * Deverrouille le vault avec le mot de passe.
159
+ *
160
+ * @param password - Mot de passe maitre
161
+ * @returns true si le deverrouillage a reussi
162
+ * @throws Si le mot de passe est invalide
163
+ */
164
+ unlock(password: string): boolean;
165
+ /**
166
+ * Verrouille le vault — efface la master key de la memoire.
167
+ */
168
+ lock(): void;
169
+ /**
170
+ * Recupere le vault via la phrase BIP39 et definit un nouveau mot de passe.
171
+ *
172
+ * @param mnemonic - Phrase de recuperation (24 mots)
173
+ * @param newPassword - Nouveau mot de passe
174
+ */
175
+ recover(mnemonic: string, newPassword: string): Promise<boolean>;
176
+ /**
177
+ * Change le mot de passe du vault sans re-chiffrer les fichiers.
178
+ *
179
+ * @param oldPassword - Ancien mot de passe
180
+ * @param newPassword - Nouveau mot de passe
181
+ */
182
+ changePassword(oldPassword: string, newPassword: string): Promise<boolean>;
183
+ /**
184
+ * Retourne l'etat du vault (initialise, deverrouille).
185
+ */
186
+ getVaultInfo(): VaultInfo;
187
+ /**
188
+ * Verifie si le vault est deverrouille.
189
+ */
190
+ isUnlocked(): boolean;
191
+ /**
192
+ * Ajoute un fichier au vault depuis un Buffer.
193
+ *
194
+ * @param data - Contenu du fichier
195
+ * @param options - Nom et dossier parent
196
+ * @returns Noeud cree, version et CID
197
+ */
198
+ addFile(data: Buffer, options: {
199
+ name: string;
200
+ parentId?: number;
201
+ }): Promise<AddFileResult>;
202
+ /**
203
+ * Ajoute un fichier depuis un chemin sur le disque.
204
+ *
205
+ * @param filePath - Chemin absolu du fichier
206
+ * @param options - Nom (optionnel, deduit du path) et dossier parent
207
+ */
208
+ addFileFromPath(filePath: string, options?: {
209
+ name?: string;
210
+ parentId?: number;
211
+ }): Promise<AddFileResult>;
212
+ /**
213
+ * Recupere le contenu dechiffre d'un fichier.
214
+ *
215
+ * @param nodeId - ID du noeud fichier
216
+ * @returns Contenu dechiffre + metadonnees
217
+ */
218
+ getFile(nodeId: number): Promise<GetFileResult>;
219
+ /**
220
+ * Liste le contenu d'un dossier.
221
+ *
222
+ * @param parentId - ID du dossier (undefined = racine)
223
+ */
224
+ listFolder(parentId?: number): FolderListing;
225
+ /**
226
+ * Recupere un noeud par son ID.
227
+ */
228
+ getNode(nodeId: number): FileNode | null;
229
+ /**
230
+ * Recupere le chemin complet d'un noeud (fil d'ariane).
231
+ */
232
+ getNodePath(nodeId: number): FileNode[];
233
+ /**
234
+ * Recupere le noeud racine.
235
+ */
236
+ getRootNode(): FileNode;
237
+ /**
238
+ * Cree un nouveau dossier.
239
+ *
240
+ * @param name - Nom du dossier
241
+ * @param parentId - ID du dossier parent (defaut: racine)
242
+ */
243
+ createFolder(name: string, parentId?: number): FileNode;
244
+ /**
245
+ * Renomme un noeud (fichier ou dossier).
246
+ */
247
+ rename(nodeId: number, newName: string): void;
248
+ /**
249
+ * Deplace un noeud vers un autre dossier.
250
+ */
251
+ move(nodeId: number, newParentId: number): void;
252
+ /**
253
+ * Supprime un noeud (soft delete → corbeille).
254
+ * Unpin de Pinata si le fichier y est synchronise.
255
+ */
256
+ delete(nodeId: number): Promise<void>;
257
+ /**
258
+ * Restaure un noeud depuis la corbeille.
259
+ */
260
+ restore(nodeId: number): void;
261
+ /**
262
+ * Liste les elements dans la corbeille.
263
+ */
264
+ listTrash(): FileNode[];
265
+ /**
266
+ * Recupere l'historique des versions d'un fichier.
267
+ */
268
+ getVersions(nodeId: number): FileVersion[];
269
+ /**
270
+ * Recherche dans le vault (FTS5).
271
+ */
272
+ search(query: string): FileNode[];
273
+ /**
274
+ * Configure les credentials Pinata et teste la connexion.
275
+ *
276
+ * @param credentials - JWT ou apiKey + secretApiKey
277
+ * @returns true si la connexion a reussi
278
+ */
279
+ configureCloud(credentials: PinataCredentials): Promise<boolean>;
280
+ /**
281
+ * Active ou desactive la synchronisation cloud.
282
+ */
283
+ toggleCloud(enabled: boolean): CloudProvider;
284
+ /**
285
+ * Retourne la configuration cloud actuelle.
286
+ */
287
+ getCloudConfig(): CloudConfig;
288
+ /**
289
+ * Retourne le statut de synchronisation.
290
+ */
291
+ getSyncStatus(): SyncStatus;
292
+ /**
293
+ * Retourne le statut global du vault.
294
+ */
295
+ getStatus(): Promise<DaemonStatus>;
296
+ private loadVaultConfig;
297
+ private saveVaultConfig;
298
+ private restoreCloudConfig;
299
+ private resetLockTimer;
300
+ private emitEvent;
301
+ }
302
+
303
+ export { type AddFileResult, type CloudConfig, type CloudProvider, type CreateVaultResult, type DaemonStatus, type FileNode, type FileVersion, type FolderListing, type GetFileResult, MerkleVault, type MerkleVaultOptions, type PinataCredentials, type SyncStatus, type SyncStatusValue, type VaultEvent, type VaultEventType, type VaultInfo };