local-lite 0.0.20 → 0.0.22

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.
@@ -1,6 +1,7 @@
1
1
  import { RemoteDBAdapter } from './index.js';
2
2
  import { DeltaChanges, SyncOperation, PushResult } from '../../core/types.js';
3
3
  import { SyncLogger, CustomLogger, LogLevel } from '../../utils/logger.js';
4
+ import { createConvexStorageUploader, ConvexStorageUploaderOptions, ConvexStorageUploaderInstance } from '../../storage/convex.js';
4
5
  export interface ConvexRemoteAdapterOptions {
5
6
  /**
6
7
  * Tables synchronized with Convex.
@@ -68,4 +69,10 @@ export declare class ConvexRemoteAdapter implements RemoteDBAdapter {
68
69
  * Cleans up live subscriptions.
69
70
  */
70
71
  destroy(): void;
72
+ /**
73
+ * Helper to create a remote storage uploader targeting Convex File Storage.
74
+ * Connects local-lite offline blob/file storage directly to Convex File Storage.
75
+ */
76
+ createStorageUploader(options?: ConvexStorageUploaderOptions): ConvexStorageUploaderInstance;
71
77
  }
78
+ export { createConvexStorageUploader, type ConvexStorageUploaderOptions, type ConvexStorageUploaderInstance, };
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ConvexRemoteAdapter = void 0;
3
+ exports.createConvexStorageUploader = exports.ConvexRemoteAdapter = void 0;
4
4
  const logger_js_1 = require("../../utils/logger.js");
5
+ const convex_js_1 = require("../../storage/convex.js");
6
+ Object.defineProperty(exports, "createConvexStorageUploader", { enumerable: true, get: function () { return convex_js_1.createConvexStorageUploader; } });
5
7
  class ConvexRemoteAdapter {
6
8
  client;
7
9
  tables;
@@ -177,5 +179,12 @@ class ConvexRemoteAdapter {
177
179
  this.unsubscribeLive = undefined;
178
180
  }
179
181
  }
182
+ /**
183
+ * Helper to create a remote storage uploader targeting Convex File Storage.
184
+ * Connects local-lite offline blob/file storage directly to Convex File Storage.
185
+ */
186
+ createStorageUploader(options = {}) {
187
+ return (0, convex_js_1.createConvexStorageUploader)(this.client, options);
188
+ }
180
189
  }
181
190
  exports.ConvexRemoteAdapter = ConvexRemoteAdapter;
@@ -1,6 +1,7 @@
1
1
  import { RemoteDBAdapter } from './index.js';
2
2
  import { DeltaChanges, SyncOperation, PushResult } from '../../core/types.js';
3
3
  import { SyncLogger, CustomLogger, LogLevel } from '../../utils/logger.js';
4
+ import { createFirebaseStorageUploader, FirebaseStorageUploaderOptions, FirebaseStorageUploaderInstance } from '../../storage/firebase.js';
4
5
  export interface FirestoreAdapterOptions {
5
6
  /**
6
7
  * Collections (tables) synchronized with Firestore.
@@ -62,4 +63,10 @@ export declare class FirestoreRemoteAdapter implements RemoteDBAdapter {
62
63
  * Cleans up all active real-time listeners.
63
64
  */
64
65
  destroy(): void;
66
+ /**
67
+ * Helper to create a remote storage uploader targeting Firebase Cloud Storage.
68
+ * Connects local-lite offline blob/file storage directly to Firebase Storage.
69
+ */
70
+ createStorageUploader(storageOrApp?: any, options?: FirebaseStorageUploaderOptions): FirebaseStorageUploaderInstance;
65
71
  }
72
+ export { FirestoreRemoteAdapter as FirestoreAdapter, createFirebaseStorageUploader, type FirebaseStorageUploaderOptions, type FirebaseStorageUploaderInstance, };
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FirestoreRemoteAdapter = void 0;
3
+ exports.createFirebaseStorageUploader = exports.FirestoreAdapter = exports.FirestoreRemoteAdapter = void 0;
4
4
  const logger_js_1 = require("../../utils/logger.js");
5
+ const firebase_js_1 = require("../../storage/firebase.js");
6
+ Object.defineProperty(exports, "createFirebaseStorageUploader", { enumerable: true, get: function () { return firebase_js_1.createFirebaseStorageUploader; } });
5
7
  /**
6
8
  * Safely extracts epoch milliseconds from numbers, Firestore Timestamps, or ISO strings.
7
9
  */
@@ -328,5 +330,25 @@ class FirestoreRemoteAdapter {
328
330
  }
329
331
  this.unsubscribers = [];
330
332
  }
333
+ /**
334
+ * Helper to create a remote storage uploader targeting Firebase Cloud Storage.
335
+ * Connects local-lite offline blob/file storage directly to Firebase Storage.
336
+ */
337
+ createStorageUploader(storageOrApp, options = {}) {
338
+ let client = storageOrApp;
339
+ let opts = options;
340
+ if (storageOrApp &&
341
+ !storageOrApp.ref &&
342
+ !storageOrApp.storage &&
343
+ !storageOrApp.app &&
344
+ typeof storageOrApp === 'object' &&
345
+ ('bucket' in storageOrApp || 'storageModule' in storageOrApp || 'basePath' in storageOrApp)) {
346
+ opts = storageOrApp;
347
+ client = this.db.app || this.db;
348
+ }
349
+ const resolvedClient = client || this.db.app || this.db;
350
+ return (0, firebase_js_1.createFirebaseStorageUploader)(resolvedClient, opts);
351
+ }
331
352
  }
332
353
  exports.FirestoreRemoteAdapter = FirestoreRemoteAdapter;
354
+ exports.FirestoreAdapter = FirestoreRemoteAdapter;
@@ -2,6 +2,7 @@ import type { SupabaseClient } from '@supabase/supabase-js';
2
2
  import { RemoteDBAdapter } from './index.js';
3
3
  import { DeltaChanges, PushResult, SyncOperation } from '../../core/types.js';
4
4
  import { SyncLogger, CustomLogger, LogLevel } from '../../utils/logger.js';
5
+ import { createSupabaseStorageUploader, type SupabaseStorageUploaderOptions, type SupabaseStorageUploaderInstance } from '../../storage/supabase.js';
5
6
  export interface SupabaseAdapterOptions {
6
7
  /**
7
8
  * The list of Postgres tables to sync.
@@ -123,4 +124,10 @@ export declare class SupabaseRemoteAdapter implements RemoteDBAdapter {
123
124
  * Clean up active realtime channels.
124
125
  */
125
126
  destroy(): void;
127
+ /**
128
+ * Helper to create a remote storage uploader targeting Supabase Storage buckets.
129
+ * Connects local-lite offline blob/file storage directly to Supabase Storage.
130
+ */
131
+ createStorageUploader(options: SupabaseStorageUploaderOptions): SupabaseStorageUploaderInstance;
126
132
  }
133
+ export { SupabaseRemoteAdapter as SupabaseAdapter, createSupabaseStorageUploader, type SupabaseStorageUploaderOptions, type SupabaseStorageUploaderInstance, };
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SupabaseRemoteAdapter = void 0;
3
+ exports.createSupabaseStorageUploader = exports.SupabaseAdapter = exports.SupabaseRemoteAdapter = void 0;
4
4
  const logger_js_1 = require("../../utils/logger.js");
5
+ const supabase_js_1 = require("../../storage/supabase.js");
6
+ Object.defineProperty(exports, "createSupabaseStorageUploader", { enumerable: true, get: function () { return supabase_js_1.createSupabaseStorageUploader; } });
5
7
  class SupabaseRemoteAdapter {
6
8
  clientResolver;
7
9
  activeLiveCallback;
@@ -25,7 +27,8 @@ class SupabaseRemoteAdapter {
25
27
  typeof clientOrOptions === 'object' &&
26
28
  !('channel' in clientOrOptions) &&
27
29
  !('from' in clientOrOptions) &&
28
- !('auth' in clientOrOptions)) {
30
+ !('auth' in clientOrOptions) &&
31
+ !('storage' in clientOrOptions)) {
29
32
  client = null;
30
33
  options = clientOrOptions;
31
34
  }
@@ -640,5 +643,13 @@ class SupabaseRemoteAdapter {
640
643
  this.activeChannel = undefined;
641
644
  }
642
645
  }
646
+ /**
647
+ * Helper to create a remote storage uploader targeting Supabase Storage buckets.
648
+ * Connects local-lite offline blob/file storage directly to Supabase Storage.
649
+ */
650
+ createStorageUploader(options) {
651
+ return (0, supabase_js_1.createSupabaseStorageUploader)(this.getClient(), options);
652
+ }
643
653
  }
644
654
  exports.SupabaseRemoteAdapter = SupabaseRemoteAdapter;
655
+ exports.SupabaseAdapter = SupabaseRemoteAdapter;
@@ -2,3 +2,4 @@ export * from './google.js';
2
2
  export * from './microsoft.js';
3
3
  export * from './github.js';
4
4
  export * from './apple.js';
5
+ export * from './session.js';
@@ -18,3 +18,4 @@ __exportStar(require("./google.js"), exports);
18
18
  __exportStar(require("./microsoft.js"), exports);
19
19
  __exportStar(require("./github.js"), exports);
20
20
  __exportStar(require("./apple.js"), exports);
21
+ __exportStar(require("./session.js"), exports);
@@ -0,0 +1,107 @@
1
+ import { DBExecutable } from '../core/schema-manager.js';
2
+ import { EventEmitter } from '../reactive/event-emitter.js';
3
+ export declare const DEFAULT_SESSIONS_TABLE = "_local_sessions";
4
+ export declare const DEFAULT_OFFLINE_DURATION_MS: number;
5
+ export interface SessionRecord<TUser = any> {
6
+ id: string;
7
+ userId: string;
8
+ user: TUser;
9
+ token?: string | null;
10
+ refreshToken?: string | null;
11
+ provider?: string | null;
12
+ createdAt: number;
13
+ lastActiveAt: number;
14
+ expiresAt?: number | null;
15
+ offlineAccessUntil?: number | null;
16
+ metadata?: Record<string, any> | null;
17
+ isActive: boolean;
18
+ }
19
+ export interface CreateSessionOptions<TUser = any> {
20
+ id?: string;
21
+ userId: string;
22
+ user: TUser;
23
+ token?: string | null;
24
+ refreshToken?: string | null;
25
+ provider?: string | null;
26
+ expiresAt?: number | null;
27
+ /**
28
+ * Duration in ms the session remains valid while offline without re-authenticating.
29
+ * Default: 30 days. Set to 0 or Infinity for unlimited offline access.
30
+ */
31
+ offlineDurationMs?: number;
32
+ metadata?: Record<string, any>;
33
+ /**
34
+ * Whether to activate this session as the active user immediately.
35
+ * Default: true
36
+ */
37
+ setActive?: boolean;
38
+ }
39
+ export interface SessionManagerOptions {
40
+ tableName?: string;
41
+ defaultOfflineDurationMs?: number;
42
+ network?: {
43
+ isOnline(): boolean | Promise<boolean>;
44
+ };
45
+ }
46
+ export declare function mapSessionRow<TUser = any>(r: any): SessionRecord<TUser>;
47
+ export declare class SessionManager {
48
+ private db;
49
+ private events?;
50
+ private tableName;
51
+ private defaultOfflineDurationMs;
52
+ private network?;
53
+ constructor(db: DBExecutable, events?: EventEmitter | undefined, options?: SessionManagerOptions);
54
+ /**
55
+ * Ensures the _local_sessions schema exists.
56
+ */
57
+ initialize(): Promise<void>;
58
+ /**
59
+ * Creates or updates a user session, persisting it to local SQLite so it remains available offline.
60
+ */
61
+ createSession<TUser = any>(options: CreateSessionOptions<TUser>): Promise<SessionRecord<TUser>>;
62
+ /**
63
+ * Retrieves the currently active session.
64
+ * If offline, the session remains active even if server tokens expired, as long as offlineAccessUntil is valid.
65
+ */
66
+ getActiveSession<TUser = any>(): Promise<SessionRecord<TUser> | null>;
67
+ /**
68
+ * Returns true if there is an active valid session (works offline and online).
69
+ */
70
+ isAuthenticated(): Promise<boolean>;
71
+ /**
72
+ * Retrieves the authenticated user profile object, or null if signed out.
73
+ */
74
+ getUser<TUser = any>(): Promise<TUser | null>;
75
+ /**
76
+ * Updates last active timestamp.
77
+ */
78
+ touch(sessionId?: string): Promise<void>;
79
+ /**
80
+ * Partially updates the active user's profile data in SQLite.
81
+ */
82
+ updateUser<TUser = any>(partialUser: Partial<TUser>): Promise<SessionRecord<TUser> | null>;
83
+ /**
84
+ * Updates access tokens without re-authenticating or resetting the offline grace period.
85
+ */
86
+ updateTokens(tokens: {
87
+ token?: string | null;
88
+ refreshToken?: string | null;
89
+ expiresAt?: number | null;
90
+ }): Promise<void>;
91
+ /**
92
+ * Signs out the active user session.
93
+ */
94
+ clearSession(): Promise<void>;
95
+ /**
96
+ * Completely purges all stored sessions (e.g. factory reset or privacy wipe).
97
+ */
98
+ purgeAllSessions(): Promise<void>;
99
+ /**
100
+ * Lists all sessions currently cached in SQLite.
101
+ */
102
+ listSessions<TUser = any>(): Promise<SessionRecord<TUser>[]>;
103
+ /**
104
+ * Switches the active session to a different account (e.g., multi-account or organization switching offline).
105
+ */
106
+ switchSession<TUser = any>(sessionIdOrUserId: string): Promise<SessionRecord<TUser> | null>;
107
+ }
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionManager = exports.DEFAULT_OFFLINE_DURATION_MS = exports.DEFAULT_SESSIONS_TABLE = void 0;
4
+ exports.mapSessionRow = mapSessionRow;
5
+ const uuid_js_1 = require("../utils/uuid.js");
6
+ exports.DEFAULT_SESSIONS_TABLE = '_local_sessions';
7
+ exports.DEFAULT_OFFLINE_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
8
+ function mapSessionRow(r) {
9
+ let userData;
10
+ try {
11
+ userData = typeof r.user_data === 'string' ? JSON.parse(r.user_data) : r.user_data;
12
+ }
13
+ catch {
14
+ userData = r.user_data;
15
+ }
16
+ let metadata = null;
17
+ if (r.metadata) {
18
+ try {
19
+ metadata = typeof r.metadata === 'string' ? JSON.parse(r.metadata) : r.metadata;
20
+ }
21
+ catch {
22
+ metadata = null;
23
+ }
24
+ }
25
+ return {
26
+ id: String(r.id),
27
+ userId: String(r.user_id),
28
+ user: userData,
29
+ token: r.token || null,
30
+ refreshToken: r.refresh_token || null,
31
+ provider: r.provider || null,
32
+ createdAt: Number(r.created_at || 0),
33
+ lastActiveAt: Number(r.last_active_at || 0),
34
+ expiresAt: r.expires_at != null ? Number(r.expires_at) : null,
35
+ offlineAccessUntil: r.offline_access_until != null ? Number(r.offline_access_until) : null,
36
+ metadata,
37
+ isActive: r.is_active === 1 || r.is_active === '1' || r.is_active === true,
38
+ };
39
+ }
40
+ class SessionManager {
41
+ db;
42
+ events;
43
+ tableName;
44
+ defaultOfflineDurationMs;
45
+ network;
46
+ constructor(db, events, options = {}) {
47
+ this.db = db;
48
+ this.events = events;
49
+ this.tableName = options.tableName ?? exports.DEFAULT_SESSIONS_TABLE;
50
+ this.defaultOfflineDurationMs = options.defaultOfflineDurationMs ?? exports.DEFAULT_OFFLINE_DURATION_MS;
51
+ this.network = options.network;
52
+ }
53
+ /**
54
+ * Ensures the _local_sessions schema exists.
55
+ */
56
+ async initialize() {
57
+ await this.db.execute(`CREATE TABLE IF NOT EXISTS ${this.tableName} (
58
+ id TEXT PRIMARY KEY,
59
+ user_id TEXT NOT NULL,
60
+ user_data TEXT NOT NULL,
61
+ token TEXT,
62
+ refresh_token TEXT,
63
+ provider TEXT,
64
+ created_at INTEGER NOT NULL,
65
+ last_active_at INTEGER NOT NULL,
66
+ expires_at INTEGER,
67
+ offline_access_until INTEGER,
68
+ metadata TEXT,
69
+ is_active INTEGER NOT NULL DEFAULT 1
70
+ );`);
71
+ await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_${this.tableName}_user ON ${this.tableName} (user_id);`);
72
+ await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_${this.tableName}_active ON ${this.tableName} (is_active);`);
73
+ }
74
+ /**
75
+ * Creates or updates a user session, persisting it to local SQLite so it remains available offline.
76
+ */
77
+ async createSession(options) {
78
+ const id = options.id || `sess_${(0, uuid_js_1.generateUUID)()}`;
79
+ const now = Date.now();
80
+ const offlineDuration = options.offlineDurationMs !== undefined
81
+ ? options.offlineDurationMs
82
+ : this.defaultOfflineDurationMs;
83
+ const offlineAccessUntil = offlineDuration === 0 || offlineDuration === Infinity
84
+ ? null
85
+ : now + offlineDuration;
86
+ const setActive = options.setActive !== false;
87
+ const userDataStr = JSON.stringify(options.user ?? {});
88
+ const metadataStr = options.metadata ? JSON.stringify(options.metadata) : null;
89
+ if (setActive) {
90
+ // Deactivate other sessions
91
+ await this.db.execute(`UPDATE ${this.tableName} SET is_active = ?;`, [0]);
92
+ }
93
+ const sql = `
94
+ INSERT INTO ${this.tableName} (id, user_id, user_data, token, refresh_token, provider, created_at, last_active_at, expires_at, offline_access_until, metadata, is_active)
95
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
96
+ ON CONFLICT(id) DO UPDATE SET
97
+ user_id = excluded.user_id,
98
+ user_data = excluded.user_data,
99
+ token = excluded.token,
100
+ refresh_token = excluded.refresh_token,
101
+ provider = excluded.provider,
102
+ last_active_at = excluded.last_active_at,
103
+ expires_at = excluded.expires_at,
104
+ offline_access_until = excluded.offline_access_until,
105
+ metadata = excluded.metadata,
106
+ is_active = excluded.is_active;
107
+ `;
108
+ await this.db.execute(sql, [
109
+ id,
110
+ options.userId,
111
+ userDataStr,
112
+ options.token ?? null,
113
+ options.refreshToken ?? null,
114
+ options.provider ?? null,
115
+ now,
116
+ now,
117
+ options.expiresAt ?? null,
118
+ offlineAccessUntil,
119
+ metadataStr,
120
+ setActive ? 1 : 0,
121
+ ]);
122
+ const session = {
123
+ id,
124
+ userId: options.userId,
125
+ user: options.user,
126
+ token: options.token || null,
127
+ refreshToken: options.refreshToken || null,
128
+ provider: options.provider || null,
129
+ createdAt: now,
130
+ lastActiveAt: now,
131
+ expiresAt: options.expiresAt || null,
132
+ offlineAccessUntil,
133
+ metadata: options.metadata || null,
134
+ isActive: setActive,
135
+ };
136
+ this.events?.emit(this.tableName, { operation: 'SET_SESSION', session });
137
+ return session;
138
+ }
139
+ /**
140
+ * Retrieves the currently active session.
141
+ * If offline, the session remains active even if server tokens expired, as long as offlineAccessUntil is valid.
142
+ */
143
+ async getActiveSession() {
144
+ const rows = await this.db.execute(`SELECT * FROM ${this.tableName} WHERE is_active = ? LIMIT 1;`, [1]);
145
+ if (!rows || rows.length === 0) {
146
+ return null;
147
+ }
148
+ const session = mapSessionRow(rows[0]);
149
+ const now = Date.now();
150
+ // Check if offline access grace period has expired
151
+ if (session.offlineAccessUntil != null && now > session.offlineAccessUntil) {
152
+ await this.clearSession();
153
+ return null;
154
+ }
155
+ // Touch last active
156
+ await this.touch(session.id);
157
+ return session;
158
+ }
159
+ /**
160
+ * Returns true if there is an active valid session (works offline and online).
161
+ */
162
+ async isAuthenticated() {
163
+ const session = await this.getActiveSession();
164
+ return session !== null;
165
+ }
166
+ /**
167
+ * Retrieves the authenticated user profile object, or null if signed out.
168
+ */
169
+ async getUser() {
170
+ const session = await this.getActiveSession();
171
+ return session ? session.user : null;
172
+ }
173
+ /**
174
+ * Updates last active timestamp.
175
+ */
176
+ async touch(sessionId) {
177
+ const now = Date.now();
178
+ if (sessionId) {
179
+ await this.db.execute(`UPDATE ${this.tableName} SET last_active_at = ? WHERE id = ?;`, [now, sessionId]);
180
+ }
181
+ else {
182
+ await this.db.execute(`UPDATE ${this.tableName} SET last_active_at = ? WHERE is_active = ?;`, [now, 1]);
183
+ }
184
+ }
185
+ /**
186
+ * Partially updates the active user's profile data in SQLite.
187
+ */
188
+ async updateUser(partialUser) {
189
+ const session = await this.getActiveSession();
190
+ if (!session)
191
+ return null;
192
+ const updatedUser = {
193
+ ...session.user,
194
+ ...partialUser,
195
+ };
196
+ const userDataStr = JSON.stringify(updatedUser);
197
+ const now = Date.now();
198
+ await this.db.execute(`UPDATE ${this.tableName} SET user_data = ?, last_active_at = ? WHERE id = ?;`, [userDataStr, now, session.id]);
199
+ session.user = updatedUser;
200
+ session.lastActiveAt = now;
201
+ this.events?.emit(this.tableName, { operation: 'UPDATE_USER', session });
202
+ return session;
203
+ }
204
+ /**
205
+ * Updates access tokens without re-authenticating or resetting the offline grace period.
206
+ */
207
+ async updateTokens(tokens) {
208
+ const session = await this.getActiveSession();
209
+ if (!session)
210
+ return;
211
+ const token = tokens.token !== undefined ? tokens.token : session.token;
212
+ const refreshToken = tokens.refreshToken !== undefined ? tokens.refreshToken : session.refreshToken;
213
+ const expiresAt = tokens.expiresAt !== undefined ? tokens.expiresAt : session.expiresAt;
214
+ const now = Date.now();
215
+ await this.db.execute(`UPDATE ${this.tableName} SET token = ?, refresh_token = ?, expires_at = ?, last_active_at = ? WHERE id = ?;`, [token, refreshToken, expiresAt, now, session.id]);
216
+ session.token = token;
217
+ session.refreshToken = refreshToken;
218
+ session.expiresAt = expiresAt;
219
+ this.events?.emit(this.tableName, { operation: 'UPDATE_TOKENS', session });
220
+ }
221
+ /**
222
+ * Signs out the active user session.
223
+ */
224
+ async clearSession() {
225
+ await this.db.execute(`UPDATE ${this.tableName} SET is_active = ? WHERE is_active = ?;`, [0, 1]);
226
+ this.events?.emit(this.tableName, { operation: 'CLEAR_SESSION' });
227
+ }
228
+ /**
229
+ * Completely purges all stored sessions (e.g. factory reset or privacy wipe).
230
+ */
231
+ async purgeAllSessions() {
232
+ await this.db.execute(`DELETE FROM ${this.tableName};`);
233
+ this.events?.emit(this.tableName, { operation: 'PURGE_SESSIONS' });
234
+ }
235
+ /**
236
+ * Lists all sessions currently cached in SQLite.
237
+ */
238
+ async listSessions() {
239
+ const rows = await this.db.execute(`SELECT * FROM ${this.tableName} ORDER BY last_active_at DESC;`);
240
+ return (rows || []).map((r) => mapSessionRow(r));
241
+ }
242
+ /**
243
+ * Switches the active session to a different account (e.g., multi-account or organization switching offline).
244
+ */
245
+ async switchSession(sessionIdOrUserId) {
246
+ const rows = await this.db.execute(`SELECT * FROM ${this.tableName} WHERE id = ? OR user_id = ? LIMIT 1;`, [sessionIdOrUserId, sessionIdOrUserId]);
247
+ if (!rows || rows.length === 0) {
248
+ return null;
249
+ }
250
+ const targetSession = mapSessionRow(rows[0]);
251
+ await this.db.execute(`UPDATE ${this.tableName} SET is_active = ?;`, [0]);
252
+ await this.db.execute(`UPDATE ${this.tableName} SET is_active = ?, last_active_at = ? WHERE id = ?;`, [1, Date.now(), targetSession.id]);
253
+ targetSession.isActive = true;
254
+ targetSession.lastActiveAt = Date.now();
255
+ this.events?.emit(this.tableName, { operation: 'SWITCH_SESSION', session: targetSession });
256
+ return targetSession;
257
+ }
258
+ }
259
+ exports.SessionManager = SessionManager;
@@ -15,6 +15,7 @@ import { KVManager, type KVStoreOptions } from '../kv/index.js';
15
15
  import { SearchManager } from '../search/index.js';
16
16
  import { BlobStorageManager, type BlobStorageManagerOptions } from '../storage/index.js';
17
17
  import { JobScheduler, type JobSchedulerOptions } from '../scheduler/index.js';
18
+ import { SessionManager, type SessionManagerOptions } from '../auth/session.js';
18
19
  export interface LocalFirstClientConfig {
19
20
  local: LocalDBAdapter;
20
21
  remote: RemoteDBAdapter;
@@ -50,6 +51,7 @@ export interface LocalFirstClientConfig {
50
51
  kv?: KVStoreOptions;
51
52
  storage?: BlobStorageManagerOptions;
52
53
  scheduler?: JobSchedulerOptions;
54
+ session?: SessionManagerOptions;
53
55
  }
54
56
  export declare class TransactionalClient {
55
57
  private parent;
@@ -103,6 +105,7 @@ export declare class LocalFirstClient {
103
105
  readonly search: SearchManager;
104
106
  readonly storage: BlobStorageManager;
105
107
  readonly scheduler: JobScheduler;
108
+ readonly session: SessionManager;
106
109
  private primaryKey;
107
110
  private migrations;
108
111
  private autoStart;
@@ -20,6 +20,7 @@ const index_js_4 = require("../kv/index.js");
20
20
  const index_js_5 = require("../search/index.js");
21
21
  const index_js_6 = require("../storage/index.js");
22
22
  const index_js_7 = require("../scheduler/index.js");
23
+ const session_js_1 = require("../auth/session.js");
23
24
  function hasFunctionFilter(value) {
24
25
  if (typeof value === 'function')
25
26
  return true;
@@ -178,6 +179,7 @@ class LocalFirstClient {
178
179
  search;
179
180
  storage;
180
181
  scheduler;
182
+ session;
181
183
  primaryKey;
182
184
  migrations;
183
185
  autoStart;
@@ -210,16 +212,34 @@ class LocalFirstClient {
210
212
  '_local_job_runs',
211
213
  '_local_api_keys',
212
214
  '_local_actions',
215
+ '_local_sessions',
216
+ '_local_sessions',
213
217
  ]);
214
218
  const actionNetwork = config.actions?.network ?? config.network;
215
219
  this.keys = new index_js_3.ApiKeyManager(this.db, config.keys);
216
220
  this.kv = new index_js_4.KVManager(this.db, this.events, config.kv);
217
221
  this.search = new index_js_5.SearchManager(this.db, this.events, (table, id) => this.findById(table, id));
222
+ let storageUploader = config.storage?.uploader;
223
+ if (!storageUploader && config.storage) {
224
+ if (typeof this.remote?.createStorageUploader === 'function' &&
225
+ (config.storage.autoRemote !== false &&
226
+ (config.storage.autoRemote || config.storage.bucket || config.storage.uploadMutation))) {
227
+ try {
228
+ storageUploader = this.remote.createStorageUploader(config.storage);
229
+ }
230
+ catch { }
231
+ }
232
+ }
218
233
  this.storage = new index_js_6.BlobStorageManager(this.db, {
219
234
  network: config.network,
220
235
  ...config.storage,
236
+ uploader: storageUploader,
221
237
  });
222
238
  this.scheduler = new index_js_7.JobScheduler(this.db, config.scheduler);
239
+ this.session = new session_js_1.SessionManager(this.db, this.events, {
240
+ network: config.network,
241
+ ...config.session,
242
+ });
223
243
  this.actions = new index_js_2.ActionManager(this.db, this.events, () => Promise.resolve(actionNetwork ? actionNetwork.isOnline() : true), {
224
244
  network: actionNetwork,
225
245
  ...config.actions,
@@ -387,6 +407,7 @@ class LocalFirstClient {
387
407
  await this.kv.initialize();
388
408
  await this.storage.initialize();
389
409
  await this.scheduler.initialize();
410
+ await this.session.initialize();
390
411
  if (this.migrations.length > 0) {
391
412
  await this.db.transaction(async (tx) => {
392
413
  for (const query of this.migrations) {
@@ -133,3 +133,4 @@ export * from './auth.js';
133
133
  export * from './backups.js';
134
134
  export * from './kv.js';
135
135
  export * from './search.js';
136
+ export * from './session.js';
@@ -648,3 +648,4 @@ __exportStar(require("./auth.js"), exports);
648
648
  __exportStar(require("./backups.js"), exports);
649
649
  __exportStar(require("./kv.js"), exports);
650
650
  __exportStar(require("./search.js"), exports);
651
+ __exportStar(require("./session.js"), exports);
@@ -0,0 +1,26 @@
1
+ import { LocalFirstClient } from '../core/client.js';
2
+ import { SessionRecord, CreateSessionOptions } from '../auth/session.js';
3
+ export interface UseSessionResult<TUser = any> {
4
+ session: SessionRecord<TUser> | null;
5
+ user: TUser | null;
6
+ isAuthenticated: boolean;
7
+ isOffline: boolean;
8
+ loading: boolean;
9
+ createSession: (options: CreateSessionOptions<TUser>) => Promise<SessionRecord<TUser>>;
10
+ updateUser: (partialUser: Partial<TUser>) => Promise<SessionRecord<TUser> | null>;
11
+ updateTokens: (tokens: {
12
+ token?: string | null;
13
+ refreshToken?: string | null;
14
+ expiresAt?: number | null;
15
+ }) => Promise<void>;
16
+ signOut: () => Promise<void>;
17
+ refetchSession: () => Promise<SessionRecord<TUser> | null>;
18
+ }
19
+ /**
20
+ * Reactive React hook for persistent, offline-first user session management.
21
+ * The session remains authenticated even when offline, ensuring uninterrupted UX.
22
+ *
23
+ * Example:
24
+ * const { user, isAuthenticated, isOffline, signOut } = useSession(client);
25
+ */
26
+ export declare function useSession<TUser = any>(client: LocalFirstClient | null | undefined): UseSessionResult<TUser>;