| |
| |
| import { createClient } from '@supabase/supabase-js'; |
| import crypto from 'crypto'; |
| import { saveEncryptedJson, loadEncryptedJson } from './cryptoUtils.js'; |
| import path from 'path'; |
|
|
| let _SUPABASE_URL, _SUPABASE_ANON_KEY; |
| export function initStoreConfig(url, key) { _SUPABASE_URL = url; _SUPABASE_ANON_KEY = key; } |
|
|
| const TEMP_TTL_MS = 24 * 60 * 60 * 1000; |
| const TEMP_INACTIVITY = 12 * 60 * 60 * 1000; |
| const TEMP_MSG_LIMIT = 10; |
|
|
| const userCache = new Map(); |
| const tempStore = new Map(); |
| const devSessions = new Map(); |
|
|
| const TEMP_STORE_FILE = '/data/temp_sessions.json'; |
|
|
| async function loadTempStore() { |
| const data = await loadEncryptedJson(TEMP_STORE_FILE); |
| if (data) { |
| for (const [id, d] of Object.entries(data)) { |
| tempStore.set(id, { |
| sessions: new Map(Object.entries(d.sessions || {})), |
| msgCount: d.msgCount || 0, |
| created: d.created || Date.now(), |
| lastActive: d.lastActive || Date.now(), |
| }); |
| } |
| } |
| } |
|
|
| async function saveTempStore() { |
| const data = {}; |
| for (const [id, d] of tempStore) { |
| data[id] = { |
| sessions: Object.fromEntries(d.sessions), |
| msgCount: d.msgCount, |
| created: d.created, |
| lastActive: d.lastActive, |
| }; |
| } |
| await saveEncryptedJson(TEMP_STORE_FILE, data); |
| } |
|
|
| |
| loadTempStore().catch(err => console.error('Failed to load temp store:', err)); |
|
|
| setInterval(async () => { |
| const now = Date.now(); |
| for (const [id, d] of tempStore) |
| if (now - d.created > TEMP_TTL_MS || now - d.lastActive > TEMP_INACTIVITY) |
| tempStore.delete(id); |
| |
| await saveTempStore().catch(err => console.error('Failed to save temp store:', err)); |
| }, 30 * 60 * 1000); |
|
|
| function userClient(accessToken) { |
| return createClient(_SUPABASE_URL, _SUPABASE_ANON_KEY, { |
| global: { headers: { Authorization: `Bearer ${accessToken}` } }, |
| auth: { persistSession: false }, |
| }); |
| } |
|
|
| export const sessionStore = { |
| |
| initTemp(t) { |
| if (!tempStore.has(t)) |
| tempStore.set(t, { sessions: new Map(), msgCount: 0, created: Date.now(), lastActive: Date.now() }); |
| return tempStore.get(t); |
| }, |
| tempCanSend(t) { const d = tempStore.get(t); return d ? d.msgCount < TEMP_MSG_LIMIT : false; }, |
| tempBump(t) { const d = tempStore.get(t); if (d) { d.msgCount++; d.lastActive = Date.now(); } }, |
| getTempSessions(t) { return [...(tempStore.get(t)?.sessions.values() || [])]; }, |
| getTempSession(t, id) { return tempStore.get(t)?.sessions.get(id) || null; }, |
| createTempSession(t) { |
| const d = this.initTemp(t); |
| const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] }; |
| d.sessions.set(s.id, s); d.lastActive = Date.now(); |
| saveTempStore().catch(err => console.error('Failed to save temp store:', err)); |
| return s; |
| }, |
| updateTempSession(t, id, patch) { |
| const d = tempStore.get(t); if (!d) return null; |
| const s = d.sessions.get(id); if (!s) return null; |
| Object.assign(s, patch); d.lastActive = Date.now(); |
| saveTempStore().catch(err => console.error('Failed to save temp store:', err)); |
| return s; |
| }, |
| deleteTempSession(t, id) { tempStore.get(t)?.sessions.delete(id); saveTempStore().catch(err => console.error('Failed to save temp store:', err)); }, |
| deleteTempAll(t) { tempStore.get(t)?.sessions.clear(); saveTempStore().catch(err => console.error('Failed to save temp store:', err)); }, |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async transferTempToUser(tempId, userId, accessToken) { |
| const d = tempStore.get(tempId); |
| if (!d || !d.sessions.size) return; |
|
|
| const uc = userClient(accessToken); |
| const user = this._ensureUser(userId); |
|
|
| for (const s of d.sessions.values()) { |
| |
| if (!s.history || s.history.length === 0) continue; |
|
|
| |
| if (user.sessions.has(s.id)) continue; |
|
|
| |
| const copy = JSON.parse(JSON.stringify(s)); |
| user.sessions.set(copy.id, copy); |
| await this._persist(uc, userId, copy).catch(err => |
| console.error('transferTempToUser persist error:', err.message)); |
| } |
| }, |
|
|
| |
| _ensureUser(uid) { |
| if (!userCache.has(uid)) userCache.set(uid, { sessions: new Map(), online: new Set() }); |
| return userCache.get(uid); |
| }, |
| async loadUserSessions(userId, accessToken) { |
| const uc = userClient(accessToken); |
| const { data, error } = await uc.from('web_sessions').select('*') |
| .eq('user_id', userId).order('updated_at', { ascending: false }); |
| if (error) { console.error('loadUserSessions', error.message); return []; } |
| const user = this._ensureUser(userId); |
| for (const row of data || []) |
| user.sessions.set(row.id, { id: row.id, name: row.name, |
| created: new Date(row.created_at).getTime(), history: row.history || [], model: row.model }); |
| return [...user.sessions.values()]; |
| }, |
| getUserSessions(uid) { return [...(userCache.get(uid)?.sessions.values() || [])]; }, |
| getUserSession(uid, id) { return userCache.get(uid)?.sessions.get(id) || null; }, |
| async createUserSession(userId, accessToken) { |
| const s = { id: crypto.randomUUID(), name: 'New Chat', created: Date.now(), history: [] }; |
| this._ensureUser(userId).sessions.set(s.id, s); |
| await this._persist(userClient(accessToken), userId, s).catch(() => {}); |
| return s; |
| }, |
| async updateUserSession(userId, accessToken, sessionId, patch) { |
| const user = userCache.get(userId); if (!user) { console.log ("No user for " + userId); return null; } |
| const s = user.sessions.get(sessionId); if (!s) { console.log ("No session found for " + sessionId); return null; } |
| Object.assign(s, patch); |
| await this._persist(userClient(accessToken), userId, s).catch(() => {}); |
| return s; |
| }, |
| async deleteUserSession(userId, accessToken, id) { |
| try { |
| userCache.get(userId)?.sessions.delete(id); |
| const { error } = await userClient(accessToken) |
| .from('web_sessions') |
| .delete() |
| .eq('id', id) |
| .eq('user_id', userId); |
| if (error) console.error('Supabase delete error:', error.message); |
| } catch (ex) { |
| console.error('Unexpected deleteUserSession error:', ex); |
| } |
| }, |
| async deleteAllUserSessions(userId, accessToken) { |
| const u = userCache.get(userId); |
| if (u) { |
| u.sessions.clear(); |
| } else { |
| console.log('No user for ' + userId); |
| return null; |
| } |
| try { |
| const { error } = await userClient(accessToken) |
| .from('web_sessions') |
| .delete() |
| .eq('user_id', userId); |
| if (error) console.error('Supabase bulk delete error:', error.message); |
| } catch (ex) { |
| console.error('Unexpected deleteAllUserSessions error:', ex); |
| } |
| }, |
| async _persist(uc, userId, s) { |
| await uc.from('web_sessions').upsert({ |
| id: s.id, user_id: userId, name: s.name, history: s.history || [], |
| model: s.model || null, updated_at: new Date().toISOString(), |
| created_at: new Date(s.created).toISOString(), |
| }); |
| }, |
| markOnline(uid, ws) { this._ensureUser(uid).online.add(ws); }, |
| markOffline(uid, ws) { userCache.get(uid)?.online.delete(ws); }, |
| |
| async createShareToken(userId, accessToken, sessionId) { |
| const s = this.getUserSession(userId, sessionId); if (!s) return null; |
| const token = crypto.randomBytes(24).toString('base64url'); |
| const uc = userClient(accessToken); |
| const { error } = await uc.from('shared_sessions').insert({ |
| token, owner_id: userId, session_snapshot: s, created_at: new Date().toISOString(), |
| }); |
| return error ? null : token; |
| }, |
| async resolveShareToken(token) { |
| |
| const uc = createClient(_SUPABASE_URL, _SUPABASE_ANON_KEY, { auth: { persistSession: false } }); |
| const { data } = await uc.from('shared_sessions').select('*').eq('token', token).single(); |
| return data || null; |
| }, |
| async importSharedSession(userId, accessToken, token) { |
| const shared = await this.resolveShareToken(token); if (!shared) return null; |
| const snap = shared.session_snapshot; |
| const newSession = { ...snap, id: crypto.randomUUID(), |
| name: `${snap.name} (shared)`, created: Date.now() }; |
| const uc = userClient(accessToken); |
| this._ensureUser(userId).sessions.set(newSession.id, newSession); |
| await this._persist(uc, userId, newSession).catch(() => {}); |
| return newSession; |
| }, |
| }; |
|
|
| export const deviceSessionStore = { |
| create(userId, ip, userAgent) { |
| const token = crypto.randomBytes(32).toString('hex'); |
| devSessions.set(token, { token, userId, ip, userAgent, |
| createdAt: new Date().toISOString(), lastSeen: new Date().toISOString(), active: true }); |
| return token; |
| }, |
| getForUser(uid) { return [...devSessions.values()].filter(s => s.userId === uid); }, |
| revoke(token) { const s = devSessions.get(token); if (s) s.active = false; }, |
| revokeAllExcept(uid, except) { |
| for (const [t, s] of devSessions) if (s.userId === uid && t !== except) s.active = false; |
| }, |
| validate(token) { |
| const s = devSessions.get(token); if (!s || !s.active) return null; |
| s.lastSeen = new Date().toISOString(); return s; |
| }, |
| }; |