local-lite 0.0.19 → 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.
Files changed (69) hide show
  1. package/README.md +5 -1
  2. package/dist/actions/store.js +1 -1
  3. package/dist/auth/index.d.ts +1 -0
  4. package/dist/auth/index.js +1 -0
  5. package/dist/auth/session.d.ts +107 -0
  6. package/dist/auth/session.js +259 -0
  7. package/dist/core/client.d.ts +18 -0
  8. package/dist/core/client.js +55 -2
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.js +5 -0
  11. package/dist/keys/generator.d.ts +38 -0
  12. package/dist/keys/generator.js +136 -0
  13. package/dist/keys/index.d.ts +5 -0
  14. package/dist/keys/index.js +21 -0
  15. package/dist/keys/manager.d.ts +59 -0
  16. package/dist/keys/manager.js +127 -0
  17. package/dist/keys/signed-token.d.ts +18 -0
  18. package/dist/keys/signed-token.js +171 -0
  19. package/dist/keys/store.d.ts +23 -0
  20. package/dist/keys/store.js +109 -0
  21. package/dist/keys/types.d.ts +95 -0
  22. package/dist/keys/types.js +2 -0
  23. package/dist/kv/index.d.ts +3 -0
  24. package/dist/kv/index.js +19 -0
  25. package/dist/kv/manager.d.ts +52 -0
  26. package/dist/kv/manager.js +164 -0
  27. package/dist/kv/store.d.ts +13 -0
  28. package/dist/kv/store.js +85 -0
  29. package/dist/kv/types.d.ts +27 -0
  30. package/dist/kv/types.js +2 -0
  31. package/dist/react/index.d.ts +3 -0
  32. package/dist/react/index.js +3 -0
  33. package/dist/react/kv.d.ts +9 -0
  34. package/dist/react/kv.js +53 -0
  35. package/dist/react/search.d.ts +19 -0
  36. package/dist/react/search.js +49 -0
  37. package/dist/react/session.d.ts +26 -0
  38. package/dist/react/session.js +118 -0
  39. package/dist/reactive/event-emitter.d.ts +1 -0
  40. package/dist/reactive/event-emitter.js +9 -6
  41. package/dist/scheduler/cron.d.ts +6 -0
  42. package/dist/scheduler/cron.js +62 -0
  43. package/dist/scheduler/index.d.ts +4 -0
  44. package/dist/scheduler/index.js +20 -0
  45. package/dist/scheduler/manager.d.ts +59 -0
  46. package/dist/scheduler/manager.js +186 -0
  47. package/dist/scheduler/store.d.ts +20 -0
  48. package/dist/scheduler/store.js +134 -0
  49. package/dist/scheduler/types.d.ts +44 -0
  50. package/dist/scheduler/types.js +2 -0
  51. package/dist/search/index.d.ts +3 -0
  52. package/dist/search/index.js +19 -0
  53. package/dist/search/manager.d.ts +42 -0
  54. package/dist/search/manager.js +123 -0
  55. package/dist/search/store.d.ts +33 -0
  56. package/dist/search/store.js +137 -0
  57. package/dist/search/types.d.ts +49 -0
  58. package/dist/search/types.js +2 -0
  59. package/dist/storage/driver.d.ts +25 -0
  60. package/dist/storage/driver.js +92 -0
  61. package/dist/storage/index.d.ts +4 -0
  62. package/dist/storage/index.js +20 -0
  63. package/dist/storage/manager.d.ts +48 -0
  64. package/dist/storage/manager.js +149 -0
  65. package/dist/storage/store.d.ts +14 -0
  66. package/dist/storage/store.js +116 -0
  67. package/dist/storage/types.d.ts +44 -0
  68. package/dist/storage/types.js +2 -0
  69. package/package.json +26 -1
package/README.md CHANGED
@@ -92,7 +92,11 @@ This recovers sync state; it does not repair corrupt SQLite files or cancel hung
92
92
  * **🔄 Infinite-Loop Safe:** Incoming remote deltas update local tables and fire reactive UI listeners without echoing back into the outbox.
93
93
  * **☁️ Database Backups:** Export and restore SQLite snapshots independently of your live sync backend.
94
94
  * **🔑 Google Sign-In Out of the Box:** Secure, zero-backend Google OAuth 2.0 (PKCE) authentication with automatic token refresh, user profile retrieval, persistent session storage, and `useGoogleAuth()` hook.
95
- * **📨 Action Outbox & Email Queue:** Enqueue transactional emails, push notifications, and webhooks offline with idempotency keys, automatic retry backoff, crash recovery, and TTL expiration.
95
+ * **📨 Action Outbox & Email Queue:** Enqueue transactional emails, push notifications, and webhooks offline with idempotency keys, automatic retry backoff, crash recovery, and TTL expiration.
96
+ * **⚡ Fast Key-Value & Encrypted Preferences (`client.kv`):** Typed storage, TTL expiration, and AES-256-GCM zero-knowledge secret protection.
97
+ * **🔍 Instant Full-Text Search (`client.search`):** SQLite FTS5 indexer with prefix matching, BM25 rank scoring, and highlighting.
98
+ * **📁 Offline Media & Blob Storage (`client.storage`):** Decoupled binary attachments, SHA-256 verification, and cloud sync queue (S3/Supabase/Firebase).
99
+ * **⏰ Offline Job Scheduler & Background Cron (`client.scheduler`):** Persistent background task runner supporting human schedules and 5-field cron.
96
100
 
97
101
 
98
102
  ---
@@ -169,7 +169,7 @@ class ActionStore {
169
169
  * Cancels a pending action if it has not yet executed.
170
170
  */
171
171
  static async cancel(db, tableName = exports.DEFAULT_ACTIONS_TABLE, id) {
172
- const rows = await db.execute(`UPDATE ${tableName} SET status = 'CANCELLED' WHERE id = ? AND status = 'PENDING';`, [id]);
172
+ await db.execute(`UPDATE ${tableName} SET status = 'CANCELLED' WHERE id = ? AND status = 'PENDING';`, [id]);
173
173
  // In drivers that return affected rows or execute successfully
174
174
  const record = await this.findById(db, tableName, id);
175
175
  return record?.status === 'CANCELLED';
@@ -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;
@@ -10,6 +10,12 @@ import { QueryBuilder, AggregateSpec, AggregateResult } from './query-builder.js
10
10
  import { type ExportJsonOptions, type ImportJsonOptions, type ExportCsvOptions, type ImportCsvOptions, type ImportSummary } from '../utils/portability.js';
11
11
  import { type BackupStorage, type CreateDatabaseBackupOptions, type EncryptedBackupOptions, type RestoreEncryptedBackupOptions } from '../backups/index.js';
12
12
  import { ActionManager, type ActionManagerOptions } from '../actions/index.js';
13
+ import { ApiKeyManager, type ApiKeyManagerOptions } from '../keys/index.js';
14
+ import { KVManager, type KVStoreOptions } from '../kv/index.js';
15
+ import { SearchManager } from '../search/index.js';
16
+ import { BlobStorageManager, type BlobStorageManagerOptions } from '../storage/index.js';
17
+ import { JobScheduler, type JobSchedulerOptions } from '../scheduler/index.js';
18
+ import { SessionManager, type SessionManagerOptions } from '../auth/session.js';
13
19
  export interface LocalFirstClientConfig {
14
20
  local: LocalDBAdapter;
15
21
  remote: RemoteDBAdapter;
@@ -41,6 +47,11 @@ export interface LocalFirstClientConfig {
41
47
  */
42
48
  touchUpdatedAt?: boolean | string;
43
49
  actions?: ActionManagerOptions;
50
+ keys?: ApiKeyManagerOptions;
51
+ kv?: KVStoreOptions;
52
+ storage?: BlobStorageManagerOptions;
53
+ scheduler?: JobSchedulerOptions;
54
+ session?: SessionManagerOptions;
44
55
  }
45
56
  export declare class TransactionalClient {
46
57
  private parent;
@@ -89,6 +100,12 @@ export declare class LocalFirstClient {
89
100
  touchColumn: string | null;
90
101
  readonly maintenance: DatabaseMaintenance;
91
102
  readonly actions: ActionManager;
103
+ readonly keys: ApiKeyManager;
104
+ readonly kv: KVManager;
105
+ readonly search: SearchManager;
106
+ readonly storage: BlobStorageManager;
107
+ readonly scheduler: JobScheduler;
108
+ readonly session: SessionManager;
92
109
  private primaryKey;
93
110
  private migrations;
94
111
  private autoStart;
@@ -328,6 +345,7 @@ export declare class LocalFirstClient {
328
345
  */
329
346
  reset(options?: {
330
347
  dropTables?: boolean;
348
+ includeInternal?: boolean;
331
349
  }): Promise<void>;
332
350
  private getAllTableNames;
333
351
  /**
@@ -15,6 +15,12 @@ const query_builder_js_1 = require("./query-builder.js");
15
15
  const portability_js_1 = require("../utils/portability.js");
16
16
  const index_js_1 = require("../backups/index.js");
17
17
  const index_js_2 = require("../actions/index.js");
18
+ const index_js_3 = require("../keys/index.js");
19
+ const index_js_4 = require("../kv/index.js");
20
+ const index_js_5 = require("../search/index.js");
21
+ const index_js_6 = require("../storage/index.js");
22
+ const index_js_7 = require("../scheduler/index.js");
23
+ const session_js_1 = require("../auth/session.js");
18
24
  function hasFunctionFilter(value) {
19
25
  if (typeof value === 'function')
20
26
  return true;
@@ -168,6 +174,12 @@ class LocalFirstClient {
168
174
  touchColumn;
169
175
  maintenance;
170
176
  actions;
177
+ keys;
178
+ kv;
179
+ search;
180
+ storage;
181
+ scheduler;
182
+ session;
171
183
  primaryKey;
172
184
  migrations;
173
185
  autoStart;
@@ -192,8 +204,30 @@ class LocalFirstClient {
192
204
  ? touch
193
205
  : this.remote?.updatedAtColumn || 'updated_at';
194
206
  this.schemaManager = new schema_manager_js_1.SchemaManager();
195
- this.maintenance = new maintenance_js_1.DatabaseMaintenance(this.db, () => this.tables);
207
+ this.maintenance = new maintenance_js_1.DatabaseMaintenance(this.db, () => [
208
+ ...this.tables,
209
+ '_local_kv',
210
+ '_local_blobs',
211
+ '_local_scheduled_jobs',
212
+ '_local_job_runs',
213
+ '_local_api_keys',
214
+ '_local_actions',
215
+ '_local_sessions',
216
+ '_local_sessions',
217
+ ]);
196
218
  const actionNetwork = config.actions?.network ?? config.network;
219
+ this.keys = new index_js_3.ApiKeyManager(this.db, config.keys);
220
+ this.kv = new index_js_4.KVManager(this.db, this.events, config.kv);
221
+ this.search = new index_js_5.SearchManager(this.db, this.events, (table, id) => this.findById(table, id));
222
+ this.storage = new index_js_6.BlobStorageManager(this.db, {
223
+ network: config.network,
224
+ ...config.storage,
225
+ });
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
+ });
197
231
  this.actions = new index_js_2.ActionManager(this.db, this.events, () => Promise.resolve(actionNetwork ? actionNetwork.isOnline() : true), {
198
232
  network: actionNetwork,
199
233
  ...config.actions,
@@ -357,6 +391,11 @@ class LocalFirstClient {
357
391
  await meta_store_js_1.MetaStore.ensureSchema(this.db);
358
392
  await outbox_store_js_1.OutboxStore.resetStaleProcessing(this.db);
359
393
  await this.actions.initialize();
394
+ await this.keys.initialize();
395
+ await this.kv.initialize();
396
+ await this.storage.initialize();
397
+ await this.scheduler.initialize();
398
+ await this.session.initialize();
360
399
  if (this.migrations.length > 0) {
361
400
  await this.db.transaction(async (tx) => {
362
401
  for (const query of this.migrations) {
@@ -871,6 +910,7 @@ class LocalFirstClient {
871
910
  stop() {
872
911
  this.syncWorker.stop();
873
912
  this.actions.stop();
913
+ this.scheduler.stop();
874
914
  }
875
915
  /**
876
916
  * Stops background sync workers, cleans up remote listeners,
@@ -891,7 +931,20 @@ class LocalFirstClient {
891
931
  */
892
932
  async reset(options = {}) {
893
933
  this.stop();
894
- const tablesToReset = Array.from(new Set([...this.tables, 'sync_outbox', 'sync_meta']));
934
+ const internalTables = [
935
+ '_local_kv',
936
+ '_local_blobs',
937
+ '_local_scheduled_jobs',
938
+ '_local_job_runs',
939
+ '_local_api_keys',
940
+ '_local_actions',
941
+ ];
942
+ const tablesToReset = Array.from(new Set([
943
+ ...this.tables,
944
+ 'sync_outbox',
945
+ 'sync_meta',
946
+ ...(options.includeInternal ? internalTables : []),
947
+ ]));
895
948
  for (const table of tablesToReset) {
896
949
  try {
897
950
  if (options.dropTables) {
package/dist/index.d.ts CHANGED
@@ -23,3 +23,8 @@ export * from './schema/index.js';
23
23
  export * from './auth/index.js';
24
24
  export * from './backups/index.js';
25
25
  export * from './actions/index.js';
26
+ export * from './keys/index.js';
27
+ export * from './kv/index.js';
28
+ export * from './search/index.js';
29
+ export * from './storage/index.js';
30
+ export * from './scheduler/index.js';
package/dist/index.js CHANGED
@@ -39,3 +39,8 @@ __exportStar(require("./schema/index.js"), exports);
39
39
  __exportStar(require("./auth/index.js"), exports);
40
40
  __exportStar(require("./backups/index.js"), exports);
41
41
  __exportStar(require("./actions/index.js"), exports);
42
+ __exportStar(require("./keys/index.js"), exports);
43
+ __exportStar(require("./kv/index.js"), exports);
44
+ __exportStar(require("./search/index.js"), exports);
45
+ __exportStar(require("./storage/index.js"), exports);
46
+ __exportStar(require("./scheduler/index.js"), exports);
@@ -0,0 +1,38 @@
1
+ import { GenerateApiKeyOptions, GeneratedApiKey } from './types.js';
2
+ /**
3
+ * Generates cryptographically secure random bytes across Web, Expo/React Native, and Node.js.
4
+ */
5
+ export declare function getRandomBytes(length: number): Uint8Array;
6
+ /**
7
+ * Encodes raw bytes into a base62 string.
8
+ */
9
+ export declare function bytesToBase62(bytes: Uint8Array): string;
10
+ /**
11
+ * Calculates a 4-byte (8-char hex) checksum over the prefix and random entropy.
12
+ */
13
+ export declare function calculateKeyChecksum(content: string): string;
14
+ /**
15
+ * Constant-time string comparison to prevent timing attacks.
16
+ */
17
+ export declare function timingSafeEqual(a: string, b: string): boolean;
18
+ /**
19
+ * Hashes an API key using SHA-256 for secure storage.
20
+ */
21
+ export declare function hashApiKey(key: string): string;
22
+ /**
23
+ * Generates an industry-standard, cryptographically secure API key.
24
+ * Format: [prefix]_[entropy]_[checksum]
25
+ *
26
+ * Example:
27
+ * prop_a7F9x2K1m8P0qW4z_c4f910e2
28
+ */
29
+ export declare function generateApiKey(options?: GenerateApiKeyOptions): GeneratedApiKey;
30
+ /**
31
+ * Verifies whether a key string is syntactically valid and has an authentic checksum.
32
+ * Does not require a database lookup.
33
+ */
34
+ export declare function verifyApiKeyFormat(key: string, expectedPrefix?: string): boolean;
35
+ /**
36
+ * Validates a raw API key against a stored SHA-256 hash in constant time.
37
+ */
38
+ export declare function verifyApiKey(rawKey: string, storedHash: string, expectedPrefix?: string): boolean;