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.
- package/dist/adapters/remote/convex.d.ts +7 -0
- package/dist/adapters/remote/convex.js +10 -1
- package/dist/adapters/remote/firestore.d.ts +7 -0
- package/dist/adapters/remote/firestore.js +23 -1
- package/dist/adapters/remote/supabase.d.ts +7 -0
- package/dist/adapters/remote/supabase.js +13 -2
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/session.d.ts +107 -0
- package/dist/auth/session.js +259 -0
- package/dist/core/client.d.ts +3 -0
- package/dist/core/client.js +21 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +1 -0
- package/dist/react/session.d.ts +26 -0
- package/dist/react/session.js +118 -0
- package/dist/storage/convex.d.ts +43 -0
- package/dist/storage/convex.js +112 -0
- package/dist/storage/expo.d.ts +132 -0
- package/dist/storage/expo.js +408 -0
- package/dist/storage/factory.d.ts +16 -0
- package/dist/storage/factory.js +22 -0
- package/dist/storage/firebase.d.ts +44 -0
- package/dist/storage/firebase.js +135 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/manager.d.ts +20 -0
- package/dist/storage/manager.js +144 -0
- package/dist/storage/supabase.d.ts +44 -0
- package/dist/storage/supabase.js +83 -0
- package/dist/storage/types.d.ts +29 -3
- package/package.json +6 -2
|
@@ -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;
|
package/dist/auth/index.d.ts
CHANGED
package/dist/auth/index.js
CHANGED
|
@@ -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;
|
package/dist/core/client.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/client.js
CHANGED
|
@@ -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) {
|
package/dist/react/index.d.ts
CHANGED
package/dist/react/index.js
CHANGED
|
@@ -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>;
|