local-lite 0.0.20 → 0.0.21
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/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 +9 -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/package.json +1 -1
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,6 +212,8 @@ 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);
|
|
@@ -220,6 +224,10 @@ class LocalFirstClient {
|
|
|
220
224
|
...config.storage,
|
|
221
225
|
});
|
|
222
226
|
this.scheduler = new index_js_7.JobScheduler(this.db, config.scheduler);
|
|
227
|
+
this.session = new session_js_1.SessionManager(this.db, this.events, {
|
|
228
|
+
network: config.network,
|
|
229
|
+
...config.session,
|
|
230
|
+
});
|
|
223
231
|
this.actions = new index_js_2.ActionManager(this.db, this.events, () => Promise.resolve(actionNetwork ? actionNetwork.isOnline() : true), {
|
|
224
232
|
network: actionNetwork,
|
|
225
233
|
...config.actions,
|
|
@@ -387,6 +395,7 @@ class LocalFirstClient {
|
|
|
387
395
|
await this.kv.initialize();
|
|
388
396
|
await this.storage.initialize();
|
|
389
397
|
await this.scheduler.initialize();
|
|
398
|
+
await this.session.initialize();
|
|
390
399
|
if (this.migrations.length > 0) {
|
|
391
400
|
await this.db.transaction(async (tx) => {
|
|
392
401
|
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>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useSession = useSession;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
/**
|
|
6
|
+
* Reactive React hook for persistent, offline-first user session management.
|
|
7
|
+
* The session remains authenticated even when offline, ensuring uninterrupted UX.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* const { user, isAuthenticated, isOffline, signOut } = useSession(client);
|
|
11
|
+
*/
|
|
12
|
+
function useSession(client) {
|
|
13
|
+
const [session, setSession] = (0, react_1.useState)(null);
|
|
14
|
+
const [loading, setLoading] = (0, react_1.useState)(true);
|
|
15
|
+
const [isOffline, setIsOffline] = (0, react_1.useState)(false);
|
|
16
|
+
const loadSession = (0, react_1.useCallback)(async () => {
|
|
17
|
+
if (!client) {
|
|
18
|
+
setSession(null);
|
|
19
|
+
setLoading(false);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const active = await client.session.getActiveSession();
|
|
24
|
+
setSession(active);
|
|
25
|
+
return active;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
setLoading(false);
|
|
32
|
+
}
|
|
33
|
+
}, [client]);
|
|
34
|
+
(0, react_1.useEffect)(() => {
|
|
35
|
+
if (!client) {
|
|
36
|
+
setSession(null);
|
|
37
|
+
setLoading(false);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let isMounted = true;
|
|
41
|
+
// Check network
|
|
42
|
+
const checkNetwork = () => {
|
|
43
|
+
const net = client.syncWorker?.networkStatusProvider;
|
|
44
|
+
if (net && typeof net.isOnline === 'function') {
|
|
45
|
+
const res = net.isOnline();
|
|
46
|
+
if (typeof res === 'boolean') {
|
|
47
|
+
if (isMounted)
|
|
48
|
+
setIsOffline(!res);
|
|
49
|
+
}
|
|
50
|
+
else if (res && typeof res.then === 'function') {
|
|
51
|
+
res.then((online) => {
|
|
52
|
+
if (isMounted)
|
|
53
|
+
setIsOffline(!online);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
checkNetwork();
|
|
59
|
+
loadSession();
|
|
60
|
+
// Listen to session changes
|
|
61
|
+
const unsubscribeSession = client.events.on('_local_sessions', () => {
|
|
62
|
+
if (isMounted) {
|
|
63
|
+
loadSession();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
// Listen to network changes if available
|
|
67
|
+
const unsubscribeNetwork = client.events.on('network', (status) => {
|
|
68
|
+
if (isMounted && status && typeof status.online === 'boolean') {
|
|
69
|
+
setIsOffline(!status.online);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return () => {
|
|
73
|
+
isMounted = false;
|
|
74
|
+
unsubscribeSession();
|
|
75
|
+
if (typeof unsubscribeNetwork === 'function') {
|
|
76
|
+
unsubscribeNetwork();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}, [client, loadSession]);
|
|
80
|
+
const createSession = (0, react_1.useCallback)(async (options) => {
|
|
81
|
+
if (!client)
|
|
82
|
+
throw new Error('Client not provided to useSession.');
|
|
83
|
+
const res = await client.session.createSession(options);
|
|
84
|
+
setSession(res);
|
|
85
|
+
return res;
|
|
86
|
+
}, [client]);
|
|
87
|
+
const updateUser = (0, react_1.useCallback)(async (partialUser) => {
|
|
88
|
+
if (!client)
|
|
89
|
+
return null;
|
|
90
|
+
const res = await client.session.updateUser(partialUser);
|
|
91
|
+
setSession(res);
|
|
92
|
+
return res;
|
|
93
|
+
}, [client]);
|
|
94
|
+
const updateTokens = (0, react_1.useCallback)(async (tokens) => {
|
|
95
|
+
if (!client)
|
|
96
|
+
return;
|
|
97
|
+
await client.session.updateTokens(tokens);
|
|
98
|
+
await loadSession();
|
|
99
|
+
}, [client, loadSession]);
|
|
100
|
+
const signOut = (0, react_1.useCallback)(async () => {
|
|
101
|
+
if (!client)
|
|
102
|
+
return;
|
|
103
|
+
await client.session.clearSession();
|
|
104
|
+
setSession(null);
|
|
105
|
+
}, [client]);
|
|
106
|
+
return {
|
|
107
|
+
session,
|
|
108
|
+
user: session ? session.user : null,
|
|
109
|
+
isAuthenticated: session !== null,
|
|
110
|
+
isOffline,
|
|
111
|
+
loading,
|
|
112
|
+
createSession,
|
|
113
|
+
updateUser,
|
|
114
|
+
updateTokens,
|
|
115
|
+
signOut,
|
|
116
|
+
refetchSession: loadSession,
|
|
117
|
+
};
|
|
118
|
+
}
|
package/package.json
CHANGED