PageSourceSearch

https://creaconnect.world/src/crea-idb.js

js creaconnect.world collected 2026-09-24 10:07:21 UTC 18,960 bytes, 492 lines download raw bytes

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.