1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 * CREA IDB â le coffre sur le disque, là où la mémoire tient quand tout se recharge 3 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 4(function () { 5 const DB_NAME = 'crea-ai-vault' 6 // VERSION 3 : le store `fichiers` â le registre des fichiers du projet 7 // (src/crea-fichiers-registre.js). Il ne tient pas dans localStorage : une 8 // arborescence de projet avec ses images en base64 y ferait sauter le quota, 9 // et le quota qui saute emporte les conversations avec lui. 10 // Ajouter un store est sans risque ici : onupgradeneeded crée ceux qui 11 // manquent, sans toucher aux données des autres. 12 const DB_VERSION = 3 13 const STORES = [ 14 'meta', 15 'fichiers', 16 'conversations', 17 'media', 18 'memoria', 19 'knowledgeGraph', 20 'sanctiCache', 21 'actions', 22 'automations', 23 'outbox', 24 'datasets', 25 'backups', 26 'sync', 27 ] 28 const CURRENT_SCHEMA = 2 29 const NAMESPACE_KEY = 'crea-ai-active-workspace' 30 31 let dbPromise = null 32 let currentNamespace = normalizeNamespace((function () { try { return localStorage.getItem(NAMESPACE_KEY) } catch (e) { return null } })() || 'legacy') 33 34 function normalizeNamespace(value = '') { 35 const normalized = String(value || 'legacy') 36 .trim() 37 .toLowerCase() 38 .replace(/[^a-z0-9._-]+/g, '-') 39 .replace(/^-+|-+$/g, '') 40 .slice(0, 120) 41 return normalized || 'legacy' 42 } 43 44 function namespacePrefix(namespace = currentNamespace) { 45 const normalized = normalizeNamespace(namespace) 46 return normalized === 'legacy' ? '' : `${normalized}::` 47 } 48 49 function scopedId(id, namespace = currentNamespace) { 50 return `${namespacePrefix(namespace)}${String(id || '')}` 51 } 52 53 function scopedRecord(value = {}, namespace = currentNamespace) { 54 return { ...value, id:scopedId(value.id, namespace) } 55 } 56 57 function stripScopedRecord(value, namespace = currentNamespace) { 58 if (!value) return value 59 const prefix = namespacePrefix(namespace) 60 if (!prefix) return value 61 return { ...value, id:String(value.id || '').slice(prefix.length) } 62 } 63 64 function belongsToNamespace(value, namespace = currentNamespace) { 65 const prefix = namespacePrefix(namespace) 66 const id = String(value?.id || '') 67 return prefix ? id.startsWith(prefix) : !id.includes('::') 68 } 69 70 function openDB() { 71 if (dbPromise) return dbPromise 72 dbPromise = new Promise((resolve, reject) => { 73 if (!('indexedDB' in window)) { 74 reject(new Error('IndexedDB indisponible')) 75 return 76 } 77 const req = indexedDB.open(DB_NAME, DB_VERSION) 78 req.onupgradeneeded = (event) => { 79 const db = req.result 80 STORES.forEach((name) => { 81 if (!db.objectStoreNames.contains(name)) { 82 db.createObjectStore(name, { keyPath: 'id' }) 83 } 84 }) 85 const fromVersion = event.oldVersion || 0 86 const now = Date.now() 87 try { 88 const tx = req.transaction 89 tx.objectStore('meta').put({ 90 id: 'schema', 91 version: DB_VERSION, 92 stateSchema: CURRENT_SCHEMA, 93 migratedFrom: fromVersion, 94 migratedAt: now, 95 }) 96 tx.objectStore('backups').put({ 97 id: `migration-${fromVersion}-to-${DB_VERSION}-${now}`, 98 type: 'migration-marker', 99 createdAt: now, 100 fromVersion, 101 toVersion: DB_VERSION, 102 }) 103 } catch {} 104 } 105 req.onsuccess = () => resolve(req.result) 106 req.onerror = () => reject(req.error || new Error('Ouverture IndexedDB impossible')) 107 req.onblocked = () => reject(new Error('Migration IndexedDB bloquée par un ancien onglet CREA')) 108 }) 109 return dbPromise 110 } 111 112 function txStore(storeName, mode = 'readonly') { 113 return openDB().then(db => { 114 if (!db.objectStoreNames.contains(storeName)) throw new Error(`Store inconnu: ${storeName}`) 115 const tx = db.transaction(storeName, mode) 116 return { tx, store: tx.objectStore(storeName) } 117 }) 118 } 119 120 async function put(storeName, value) { 121 const { tx, store } = await txStore(storeName, 'readwrite') 122 return new Promise((resolve, reject) => { 123 const record = scopedRecord(value) 124 store.put(record) 125 tx.oncomplete = () => resolve(stripScopedRecord(record)) 126 tx.onerror = () => reject(tx.error) 127 }) 128 } 129 130 async function get(storeName, id) { 131 const { tx, store } = await txStore(storeName, 'readonly') 132 return new Promise((resolve, reject) => { 133 const req = store.get(scopedId(id)) 134 req.onsuccess = () => resolve(stripScopedRecord(req.result || null)) 135 req.onerror = () => reject(req.error) 136 }) 137 } 138 139 async function bulkReplace(storeName, items) { 140 await clearStore(storeName) 141 const { tx, store } = await txStore(storeName, 'readwrite') 142 return new Promise((resolve, reject) => { 143 ;
143(items || []).forEach((item, index) => { 144 store.put(scopedRecord({ ...item, id:item.id || `${storeName}-${index}` })) 145 }) 146 tx.oncomplete = () => resolve() 147 tx.onerror = () => reject(tx.error) 148 }) 149 } 150 151 async function getAll(storeName) { 152 const { tx, store } = await txStore(storeName, 'readonly') 153 return new Promise((resolve, reject) => { 154 const req = store.getAll() 155 req.onsuccess = () => resolve( 156 (req.result || []) 157 .filter(item => belongsToNamespace(item)) 158 .map(item => stripScopedRecord(item)) 159 ) 160 req.onerror = () => reject(req.error) 161 tx.onerror = () => reject(tx.error) 162 }) 163 } 164 165 async function clearStore(storeName) { 166 const { tx, store } = await txStore(storeName, 'readwrite') 167 return new Promise((resolve, reject) => { 168 if (currentNamespace === 'legacy') { 169 const req = store.openCursor() 170 req.onsuccess = () => { 171 const cursor = req.result 172 if (!cursor) return 173 if (belongsToNamespace(cursor.value)) cursor.delete() 174 cursor.continue() 175 } 176 req.onerror = () => reject(req.error) 177 } else { 178 const prefix = namespacePrefix() 179 const range = IDBKeyRange.bound(prefix, `${prefix}\uffff`) 180 const req = store.openCursor(range) 181 req.onsuccess = () => { 182 const cursor = req.result 183 if (!cursor) return 184 cursor.delete() 185 cursor.continue() 186 } 187 req.onerror = () => reject(req.error) 188 } 189 tx.oncomplete = () => resolve() 190 tx.onerror = () => reject(tx.error) 191 }) 192 } 193 194 async function getRaw(storeName, id) { 195 const { tx, store } = await txStore(storeName, 'readonly') 196 return new Promise((resolve, reject) => { 197 tx.onerror = () => reject(tx.error) 198 const req = store.get(id) 199 req.onsuccess = () => resolve(req.result || null) 200 req.onerror = () => reject(req.error) 201 }) 202 } 203 204 function setNamespace(namespace) { 205 currentNamespace = normalizeNamespace(namespace) 206 return currentNamespace 207 } 208 209 async function copyStateFromNamespace(sourceNamespace, targetNamespace, opts = {}) { 210 const previous = currentNamespace 211 const source = normalizeNamespace(sourceNamespace) 212 const target = normalizeNamespace(targetNamespace) 213 if (!source || !target || source === target) return { copied:false, reason:'same-namespace', namespace:target } 214 try { 215 currentNamespace = target 216 if (opts.onlyIfEmpty !== false) { 217 const targetState = await loadState().catch(() => null) 218 if (targetState) return { copied:false, reason:'target-exists', namespace:target } 219 } 220 currentNamespace = source 221 const sourceState = await loadState().catch(() => null) 222 if (!sourceState) return { copied:false, reason:'source-empty', namespace:source } 223 currentNamespace = target 224 await saveState(sourceState) 225 return { copied:true, from:source, to:target } 226 } finally { 227 currentNamespace = previous 228 } 229 } 230 231 async function claimLegacyState() { 232 if (currentNamespace === 'legacy') return { claimed:false, reason:'legacy-active' } 233 const existing = await get('meta', 'stateSnapshot') 234 if (existing?.value) return { claimed:false, reason:'workspace-exists' } 235 const legacy = await getRaw('meta', 'stateSnapshot') 236 if (!legacy?.value) return { claimed:false, reason:'legacy-empty' } 237 await saveState(legacy.value) 238 return { claimed:true, namespace:currentNamespace } 239 } 240 241 function migrateState(rawState) { 242 const state = sanitizeSecrets({ ...(rawState || {}) }) 243 const fromSchema = Number(state.__schema || state.stateVersion || 0) 244 if (!Array.isArray(state.conversations)) state.conversations = [] 245 if (!Array.isArray(state.mediaLibrary)) state.mediaLibrary = [] 246 if (!Array.isArray(state.memories)) state.memories = [] 247 if (!state.sessionRegistry || typeof state.sessionRegistry !== 'object') state.sessionRegistry = { items: [], activeId: null } 248 if (!Array.isArray(state.sessionRegistry.items)) state.sessionRegistry.items = [] 249 if (!Array.isArray(state.sessionEvents)) state.sessionEvents = [] 250 if (!state.surfaceContinuity || typeof state.surfaceContinuity !== 'object') state.surfaceContinuity = { links: [] } 251 if (!Array.isArray(state.surfaceContinuity.links)) state.surfaceContinuity.links = [] 252 if (!state.browserAgent || typeof state.browserAgent !== 'object') state.browserAgent = { pendingLaunches: [] } 253 if (!Array.isArray(state.browserAgent.pendingLaunches)) state.browserAgent.pendingLaunches = [] 254 if (!state.missionControl || typeof state.missionControl !== 'object') state.missionControl = { items: [], activeId: null, journal: [], queue: [] } 255 if (!Array.isArray(state.missionControl.items)) state.missionControl.items = [] 256 if (!Array.isArray(state.missionControl.journal)) state.missionControl.journal = [] 257 if (!Array.isArray(state.missionControl.queue)) state.missionControl.queue = [] 258 if (!Array.isArray(state.sanctiMemoria)) state.sanctiMemoria = [] 259 if (!state.knowledgeGraph || typeof state.knowledgeGraph !== 'object') state.knowledgeGraph = { nodes: [], edges: [] } 260 if (!Array.isArray(state.knowledgeGraph.nodes)) state.knowledgeGraph.nodes = []
261 if (!Array.isArray(state.knowledgeGraph.edges)) state.knowledgeGraph.edges = [] 262 if (!Array.isArray(state.sanctiDataset)) state.sanctiDataset = [] 263 if (!Array.isArray(state.automations)) state.automations = [] 264 if (!Array.isArray(state.sanctiActions)) state.sanctiActions = [] 265 state.__schema = CURRENT_SCHEMA 266 state.__migratedFrom = fromSchema 267 state.__updatedAt = Number(state.__updatedAt || state.savedAt || Date.now()) 268 return state 269 } 270 271 function isSecretKey(key = '') { 272 const k = String(key || '').toLowerCase() 273 return k === 'token' 274 || k === 'apikey' 275 || k === 'apikeys' 276 || k === 'clientkeys' 277 || k === 'authtoken' 278 || k === 'authkey' 279 || k.endsWith('secret') 280 || k.endsWith('password') 281 || k.includes('keyvault') 282 || k.includes('passwordhash') 283 || k.includes('backendsession') 284 } 285 286 function sanitizeSecrets(value, depth = 0) { 287 if (depth > 32) return null 288 if (Array.isArray(value)) return value.map(item => sanitizeSecrets(item, depth + 1)) 289 if (!value || typeof value !== 'object') return value 290 const out = {} 291 Object.entries(value).forEach(([key, item]) => { 292 if (isSecretKey(key)) { 293 out[key + 'Present'] = Boolean(item) 294 return 295 } 296 out[key] = sanitizeSecrets(item, depth + 1) 297 }) 298 return out 299 } 300 301 function compactStateFromWindow() { 302 const state = window.STATE || {} 303 const stripVec = (node) => { 304 const { vec, ...rest } = node || {} 305 return rest 306 } 307 return { 308 __schema: CURRENT_SCHEMA, 309 __updatedAt: Date.now(), 310 conversations: state.conversations || [], 311 activeId: state.activeId || null, 312 mediaLibrary: state.mediaLibrary || [], 313 memories: state.memories || [], 314 sessionRegistry: state.sessionRegistry || { items: [], activeId: null }, 315 sessionEvents: state.sessionEvents || [], 316 surfaceContinuity: state.surfaceContinuity || { links: [] }, 317 browserAgent: state.browserAgent || { pendingLaunches: [] }, 318 missionControl: state.missionControl || { items: [], activeId: null, journal: [], queue: [] }, 319 sanctiMemoria: (state.sanctiMemoria || []).map(stripVec), 320 knowledgeGraph: state.knowledgeGraph || { nodes: [], edges: [] }, 321 sanctiDataset: state.sanctiDataset || [], 322 sanctiLoops: state.sanctiLoops || [], 323 automations: state.automations || [], 324 sanctiConnect: state.sanctiConnect ? { 325 url: state.sanctiConnect.url, 326 useRuntime: state.sanctiConnect.useRuntime, 327 syncEnabled: state.sanctiConnect.syncEnabled, 328 pendingPush: (state.sanctiConnect.pendingPush || []).slice(-500), 329 } : null, 330 sanctiDisk: state.sanctiDisk || null, 331 sanctiActions: state.sanctiActions || [], 332 sanctiDiscover: state.sanctiDiscover || null, 333 ecosystemIndex: state.ecosystemIndex || null, 334 userEmail: state.userEmail || '', 335 userProfile: state.userProfile || null, 336 usage: state.usage || null, 337 credits: state.credits || 0, 338 plan: state.plan || 'free', 339 theme: state.theme || 'light', 340 } 341 } 342 343 async function saveState(snapshot) {
344 const data = migrateState({ ...(snapshot || compactStateFromWindow()), __updatedAt: Date.now() }) 345 await put('meta', { id: 'stateSnapshot', value: data, updatedAt: data.__updatedAt }) 346 await Promise.allSettled([ 347 bulkReplace('conversations', data.conversations || []), 348 bulkReplace('media', data.mediaLibrary || []), 349 bulkReplace('memoria', data.sanctiMemoria || []), 350 bulkReplace('datasets', data.sanctiDataset || []), 351 bulkReplace('automations', data.automations || []), 352 bulkReplace('actions', data.sanctiActions || []), 353 put('knowledgeGraph', { id: 'main', value: data.knowledgeGraph || { nodes: [], edges: [] }, updatedAt: data.__updatedAt }), 354 put('sanctiCache', { id: 'runtime', value: { connect: data.sanctiConnect, disk: data.sanctiDisk, discover: data.sanctiDiscover, index: data.ecosystemIndex }, updatedAt: data.__updatedAt }), 355 bulkReplace('outbox', data.sanctiConnect?.pendingPush || []), 356 put('sync', { id: 'localState', updatedAt: data.__updatedAt, schema: data.__schema, counts: countState(data) }), 357 ]) 358 return data 359 } 360 361 async function saveFromLocalStorage(key) { 362 let raw 363 try { raw = localStorage.getItem(key || 'crea-ai') } catch { raw = null } 364 if (!raw) return saveState(compactStateFromWindow()) 365 let parsed 366 try { parsed = JSON.parse(raw) } catch { return saveState(compactStateFromWindow()) } 367 return saveState(migrateState(parsed)) 368 } 369 370 async function loadState() { 371 const record = await get('meta', 'stateSnapshot') 372 return record?.value ? migrateState(record.value) : null 373 } 374 375 function countState(data = {}) { 376 return { 377 conversations: (data.conversations || []).length, 378 media: (data.mediaLibrary || []).length, 379 memoria: (data.sanctiMemoria || []).length, 380 memories: (data.memories || []).length, 381 sessions: (data.sessionRegistry?.items || []).length, 382 sessionEvents: (data.sessionEvents || []).length, 383 missions: (data.missionControl?.items || []).length, 384 actions: (data.sanctiActions || []).length, 385 automations: (data.automations || []).length, 386 datasets: (data.sanctiDataset || []).length, 387 } 388 } 389 390 async function backupCurrentState(reason = 'manual') { 391 const snapshot = await loadState().catch(() => null) 392 const value = snapshot || compactStateFromWindow() 393 const createdAt = Date.now() 394 const record = { 395 id: `backup-${reason}-${createdAt}`, 396 type: 'state-backup', 397 reason, 398 createdAt, 399 value, 400 counts: countState(value), 401 } 402 await put('backups', record) 403 return record 404 } 405 406 async function repairFromLocalStorage(key = 'crea-ai') { 407 let raw 408 try { raw = localStorage.getItem(key) } catch { raw = null } 409 if (raw) { 410 try { 411 const parsed = JSON.parse(raw) 412 if (parsed?.idbPrimary === true || parsed?.storageMode === 'indexeddb-primary') { 413 throw new Error('localStorage est un bootstrap IndexedDB; réparation refusée pour éviter d\'écraser le coffre local.') 414 } 415 } catch (e) { 416 if (/bootstrap IndexedDB/.test(e.message || '')) throw e 417 } 418 } 419 await backupCurrentState('pre-repair').catch(() => null) 420 return saveFromLocalStorage(key) 421 } 422 423 async function exportAllStores() { 424 const stores = {} 425 for (const name of STORES) { 426 stores[name] = sanitizeSecrets(await getAll(name).catch(() => [])) 427 } 428 return { 429 format: 'crea-ai-indexeddb-export', 430 db: DB_NAME, 431 version: DB_VERSION, 432 schema: CURRENT_SCHEMA, 433 namespace: currentNamespace, 434 exportedAt: new Date().toISOString(), 435 stores, 436 } 437 } 438 439 async function importAllStores(payload = {}, opts = {}) { 440 if (!payload?.stores || typeof payload.stores !== 'object') throw new Error('Export IndexedDB invalide') 441 const preImportBackup = await backupCurrentState('pre-idb-import').catch(() => null) 442 for (const name of STORES) { 443 if (!Object.prototype.hasOwnProperty.call(payload.stores, name)) continue 444 if (opts.includeMeta === false && name === 'meta') continue 445 if (!opts.includeBackups && name === 'backups') continue 446 await bulkReplace(name, payload.stores[name] || []) 447 } 448 if (preImportBackup) await put('backups', preImportBackup).catch(() => null) 449 const snapshot = await loadState().catch(() => null) 450 if (snapshot) await saveState(snapshot) 451 return status() 452 } 453 454 async function status() { 455 const snapshot = await loadState().catch(() => null)
456 const backups = await getAll('backups').catch(() => []) 457 return { 458 ok: true, 459 db: DB_NAME, 460 version: DB_VERSION, 461 schema: CURRENT_SCHEMA, 462 namespace: currentNamespace, 463 stores: STORES, 464 updatedAt: snapshot?.__updatedAt || 0, 465 hasSnapshot: Boolean(snapshot), 466 counts: countState(snapshot || {}), 467 backups: backups.length, 468 } 469 } 470 471 window.CREA_IDB = { 472 openDB, 473 put, 474 get, 475 saveState, 476 saveFromLocalStorage, 477 loadState, 478 compactStateFromWindow, 479 migrateState, 480 backupCurrentState, 481 repairFromLocalStorage, 482 exportAllStores, 483 importAllStores, 484 getAll, 485 clearStore, 486 status, 487 setNamespace, 488 copyStateFromNamespace, 489 claimLegacyState, 490 getNamespace: () => currentNamespace, 491 } 492})()
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.