local-lite 0.0.10 → 0.0.12

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/README.md CHANGED
@@ -87,6 +87,8 @@ This recovers sync state; it does not repair corrupt SQLite files or cancel hung
87
87
  * **📶 Network-Aware:** Automatically pauses polling when offline and triggers immediate sync flushes the instant connectivity is restored.
88
88
  * **🔄 Infinite-Loop Safe:** Incoming remote deltas update local tables and fire reactive UI listeners without echoing back into the outbox.
89
89
  * **☁️ Database Backups:** Export and restore SQLite snapshots independently of your live sync backend.
90
+ * **🔑 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.
91
+
90
92
 
91
93
  ---
92
94
 
@@ -244,8 +246,50 @@ export function TodoScreen() {
244
246
  }
245
247
  ```
246
248
 
249
+ ### 3. Google Sign-In (Out of the Box)
250
+
251
+ `local-lite` provides a zero-backend, PKCE-secured Google OAuth 2.0 client with automatic token refreshing, user profile retrieval, and React hooks.
252
+
253
+ ```tsx
254
+ import * as WebBrowser from 'expo-web-browser';
255
+ import * as Linking from 'expo-linking';
256
+ import { GoogleAuthClient } from 'local-lite/auth';
257
+ import { useGoogleAuth } from 'local-lite/react';
258
+
259
+ const googleAuth = new GoogleAuthClient({
260
+ clientId: 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com',
261
+ redirectUri: Linking.createURL('oauth'),
262
+ authorize: async ({ authorizationUrl, redirectUri }) => {
263
+ const res = await WebBrowser.openAuthSessionAsync(authorizationUrl, redirectUri);
264
+ if (res.type !== 'success' || !res.url) return null;
265
+ const url = new URL(res.url);
266
+ return {
267
+ code: url.searchParams.get('code') || '',
268
+ state: url.searchParams.get('state') || '',
269
+ };
270
+ },
271
+ });
272
+
273
+ export function UserProfileHeader() {
274
+ const { user, isSignedIn, loading, signIn, signOut } = useGoogleAuth(googleAuth);
275
+
276
+ if (loading) return <Text>Loading...</Text>;
277
+ if (!isSignedIn) {
278
+ return <Button title="Sign in with Google" onPress={signIn} />;
279
+ }
280
+
281
+ return (
282
+ <View>
283
+ <Text>Welcome, {user?.name} ({user?.email})</Text>
284
+ <Button title="Sign Out" onPress={signOut} />
285
+ </View>
286
+ );
287
+ }
288
+ ```
289
+
247
290
  ---
248
291
 
292
+
249
293
  ## Supabase Backend Setup & Schema
250
294
 
251
295
  To synchronize with Supabase using zero-clock-skew delta queries and `deleted: true` soft deletes, execute the following script in your Supabase project (**Dashboard → SQL Editor**):
@@ -19,3 +19,13 @@ export declare class MemoryDemoRemoteAdapter implements RemoteDBAdapter {
19
19
  emitLiveChanges(changes: DeltaChanges): void;
20
20
  clear(): void;
21
21
  }
22
+ /**
23
+ * Built-in No-Op Remote Adapter for local-only / offline operation.
24
+ * Acknowledges all local outbox pushes with 0 latency, performs no remote pulls,
25
+ * and maintains zero cloud backend connections.
26
+ */
27
+ export declare class NoopRemoteAdapter implements RemoteDBAdapter {
28
+ pushChanges(_operations: SyncOperation[]): Promise<boolean>;
29
+ fetchChanges(_sinceTimestamp: number): Promise<DeltaChanges>;
30
+ subscribeToLiveChanges(_callback: (changes: DeltaChanges) => void): () => void;
31
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MemoryDemoRemoteAdapter = void 0;
3
+ exports.NoopRemoteAdapter = exports.MemoryDemoRemoteAdapter = void 0;
4
4
  /**
5
5
  * Built-in In-Memory Demo Remote Adapter.
6
6
  * Useful for prototyping, offline testing, and previewing without a live backend.
@@ -47,3 +47,23 @@ class MemoryDemoRemoteAdapter {
47
47
  }
48
48
  }
49
49
  exports.MemoryDemoRemoteAdapter = MemoryDemoRemoteAdapter;
50
+ /**
51
+ * Built-in No-Op Remote Adapter for local-only / offline operation.
52
+ * Acknowledges all local outbox pushes with 0 latency, performs no remote pulls,
53
+ * and maintains zero cloud backend connections.
54
+ */
55
+ class NoopRemoteAdapter {
56
+ async pushChanges(_operations) {
57
+ return true;
58
+ }
59
+ async fetchChanges(_sinceTimestamp) {
60
+ return {
61
+ timestamp: Date.now(),
62
+ changes: [],
63
+ };
64
+ }
65
+ subscribeToLiveChanges(_callback) {
66
+ return () => { };
67
+ }
68
+ }
69
+ exports.NoopRemoteAdapter = NoopRemoteAdapter;
@@ -0,0 +1,75 @@
1
+ import { GoogleDriveBackupStorage } from '../backups/cloud-storage.js';
2
+ export declare const GOOGLE_AUTHORIZATION_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
3
+ export declare const GOOGLE_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
4
+ export declare const GOOGLE_USERINFO_ENDPOINT = "https://www.googleapis.com/oauth2/v3/userinfo";
5
+ export declare const DEFAULT_GOOGLE_AUTH_SCOPES: string[];
6
+ export interface GoogleUser {
7
+ id: string;
8
+ email: string;
9
+ emailVerified: boolean;
10
+ name: string;
11
+ givenName?: string;
12
+ familyName?: string;
13
+ picture?: string;
14
+ locale?: string;
15
+ }
16
+ export interface GoogleAuthTokens {
17
+ accessToken: string;
18
+ refreshToken?: string;
19
+ idToken?: string;
20
+ expiresAt: number;
21
+ tokenType?: string;
22
+ scope?: string;
23
+ }
24
+ export interface GoogleAuthTokenStore {
25
+ getTokens(): Promise<GoogleAuthTokens | null>;
26
+ setTokens(tokens: GoogleAuthTokens): Promise<void>;
27
+ removeTokens(): Promise<void>;
28
+ getUser(): Promise<GoogleUser | null>;
29
+ setUser(user: GoogleUser | null): Promise<void>;
30
+ }
31
+ export interface GoogleAuthKeyValueStorage {
32
+ getItem(key: string): Promise<unknown>;
33
+ setItem(key: string, value: unknown): Promise<void>;
34
+ removeItem(key: string): Promise<void>;
35
+ }
36
+ export interface GoogleAuthorizationRequest {
37
+ authorizationUrl: string;
38
+ redirectUri: string;
39
+ state: string;
40
+ }
41
+ export interface GoogleAuthorizationResponse {
42
+ code: string;
43
+ state: string;
44
+ }
45
+ export interface GoogleAuthClientOptions {
46
+ clientId: string;
47
+ redirectUri: string;
48
+ scopes?: string[];
49
+ authorize: (request: GoogleAuthorizationRequest) => Promise<GoogleAuthorizationResponse | null>;
50
+ tokenStore?: GoogleAuthTokenStore;
51
+ storage?: GoogleAuthKeyValueStorage;
52
+ randomBytes?: (length: number) => Uint8Array;
53
+ fetch?: typeof fetch;
54
+ prompt?: 'consent' | 'select_account' | 'none';
55
+ accessType?: 'offline' | 'online';
56
+ }
57
+ export declare function createInMemoryGoogleTokenStore(): GoogleAuthTokenStore;
58
+ export declare function createGoogleAuthTokenStore(storage: GoogleAuthKeyValueStorage, prefix?: string): GoogleAuthTokenStore;
59
+ export declare class GoogleAuthClient {
60
+ private readonly options;
61
+ private readonly tokenStore;
62
+ private readonly fetcher;
63
+ private readonly listeners;
64
+ private currentUser;
65
+ constructor(options: GoogleAuthClientOptions);
66
+ onAuthStateChange(listener: (user: GoogleUser | null) => void): () => void;
67
+ private notify;
68
+ signIn(): Promise<GoogleUser | null>;
69
+ signOut(): Promise<void>;
70
+ isSignedIn(): Promise<boolean>;
71
+ getUser(): Promise<GoogleUser | null>;
72
+ getValidAccessToken(): Promise<string>;
73
+ private fetchUserProfile;
74
+ createDriveBackupStorage(): GoogleDriveBackupStorage;
75
+ }
@@ -0,0 +1,323 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GoogleAuthClient = exports.DEFAULT_GOOGLE_AUTH_SCOPES = exports.GOOGLE_USERINFO_ENDPOINT = exports.GOOGLE_TOKEN_ENDPOINT = exports.GOOGLE_AUTHORIZATION_ENDPOINT = void 0;
4
+ exports.createInMemoryGoogleTokenStore = createInMemoryGoogleTokenStore;
5
+ exports.createGoogleAuthTokenStore = createGoogleAuthTokenStore;
6
+ const sha2_js_1 = require("@noble/hashes/sha2.js");
7
+ const utils_js_1 = require("@noble/hashes/utils.js");
8
+ const cloud_storage_js_1 = require("../backups/cloud-storage.js");
9
+ exports.GOOGLE_AUTHORIZATION_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth';
10
+ exports.GOOGLE_TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token';
11
+ exports.GOOGLE_USERINFO_ENDPOINT = 'https://www.googleapis.com/oauth2/v3/userinfo';
12
+ exports.DEFAULT_GOOGLE_AUTH_SCOPES = ['openid', 'email', 'profile'];
13
+ function secureRandomBytes(length) {
14
+ const crypto = globalThis.crypto;
15
+ if (!crypto?.getRandomValues) {
16
+ throw new Error('Secure random number generation is unavailable.');
17
+ }
18
+ return crypto.getRandomValues(new Uint8Array(length));
19
+ }
20
+ function base64UrlEncode(bytes) {
21
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
22
+ let result = '';
23
+ for (let index = 0; index < bytes.length; index += 3) {
24
+ const first = bytes[index];
25
+ const second = bytes[index + 1];
26
+ const third = bytes[index + 2];
27
+ result += alphabet[first >> 2];
28
+ result += alphabet[((first & 3) << 4) | (second === undefined ? 0 : second >> 4)];
29
+ if (second !== undefined) {
30
+ result += alphabet[((second & 15) << 2) | (third === undefined ? 0 : third >> 6)];
31
+ }
32
+ if (third !== undefined)
33
+ result += alphabet[third & 63];
34
+ }
35
+ return result;
36
+ }
37
+ function validateTokens(value) {
38
+ if (!value || typeof value !== 'object')
39
+ return false;
40
+ const tokens = value;
41
+ return (typeof tokens.accessToken === 'string' &&
42
+ typeof tokens.expiresAt === 'number' &&
43
+ (tokens.refreshToken === undefined || typeof tokens.refreshToken === 'string') &&
44
+ (tokens.idToken === undefined || typeof tokens.idToken === 'string'));
45
+ }
46
+ function validateUser(value) {
47
+ if (!value || typeof value !== 'object')
48
+ return false;
49
+ const user = value;
50
+ return (typeof user.id === 'string' &&
51
+ typeof user.email === 'string' &&
52
+ typeof user.name === 'string');
53
+ }
54
+ function createInMemoryGoogleTokenStore() {
55
+ let storedTokens = null;
56
+ let storedUser = null;
57
+ return {
58
+ getTokens: async () => storedTokens,
59
+ setTokens: async (tokens) => {
60
+ storedTokens = tokens;
61
+ },
62
+ removeTokens: async () => {
63
+ storedTokens = null;
64
+ },
65
+ getUser: async () => storedUser,
66
+ setUser: async (user) => {
67
+ storedUser = user;
68
+ },
69
+ };
70
+ }
71
+ function createGoogleAuthTokenStore(storage, prefix = 'local-lite-google-auth') {
72
+ const tokensKey = `${prefix}-tokens`;
73
+ const userKey = `${prefix}-user`;
74
+ return {
75
+ getTokens: async () => {
76
+ const raw = await storage.getItem(tokensKey);
77
+ return validateTokens(raw) ? raw : null;
78
+ },
79
+ setTokens: tokens => storage.setItem(tokensKey, tokens),
80
+ removeTokens: () => storage.removeItem(tokensKey),
81
+ getUser: async () => {
82
+ const raw = await storage.getItem(userKey);
83
+ return validateUser(raw) ? raw : null;
84
+ },
85
+ setUser: async (user) => {
86
+ if (user) {
87
+ await storage.setItem(userKey, user);
88
+ }
89
+ else {
90
+ await storage.removeItem(userKey);
91
+ }
92
+ },
93
+ };
94
+ }
95
+ async function parseTokenResponse(response) {
96
+ try {
97
+ return (await response.json());
98
+ }
99
+ catch {
100
+ throw new Error(`Google OAuth request failed (${response.status}).`);
101
+ }
102
+ }
103
+ function throwOAuthError(payload, status) {
104
+ throw new Error(payload.error_description ||
105
+ payload.error ||
106
+ `Google OAuth request failed (${status}).`);
107
+ }
108
+ class GoogleAuthClient {
109
+ options;
110
+ tokenStore;
111
+ fetcher;
112
+ listeners = new Set();
113
+ currentUser = null;
114
+ constructor(options) {
115
+ this.options = options;
116
+ if (!options.clientId?.trim()) {
117
+ throw new Error('Google OAuth client ID is required.');
118
+ }
119
+ if (!options.redirectUri?.trim()) {
120
+ throw new Error('Google OAuth redirect URI is required.');
121
+ }
122
+ this.fetcher = options.fetch ?? fetch;
123
+ if (options.tokenStore) {
124
+ this.tokenStore = options.tokenStore;
125
+ }
126
+ else if (options.storage) {
127
+ this.tokenStore = createGoogleAuthTokenStore(options.storage);
128
+ }
129
+ else {
130
+ this.tokenStore = createInMemoryGoogleTokenStore();
131
+ }
132
+ }
133
+ onAuthStateChange(listener) {
134
+ this.listeners.add(listener);
135
+ return () => {
136
+ this.listeners.delete(listener);
137
+ };
138
+ }
139
+ notify(user) {
140
+ this.currentUser = user;
141
+ for (const listener of this.listeners) {
142
+ try {
143
+ listener(user);
144
+ }
145
+ catch (err) {
146
+ console.error('Error in GoogleAuthClient auth listener:', err);
147
+ }
148
+ }
149
+ }
150
+ async signIn() {
151
+ const randomBytes = this.options.randomBytes ?? secureRandomBytes;
152
+ const verifierBytes = randomBytes(32);
153
+ const stateBytes = randomBytes(32);
154
+ if (verifierBytes.length !== 32 || stateBytes.length !== 32) {
155
+ throw new Error('The randomBytes provider returned an invalid number of bytes.');
156
+ }
157
+ const codeVerifier = base64UrlEncode(verifierBytes);
158
+ const codeChallenge = base64UrlEncode((0, sha2_js_1.sha256)((0, utils_js_1.utf8ToBytes)(codeVerifier)));
159
+ const state = base64UrlEncode(stateBytes);
160
+ const scopes = this.options.scopes && this.options.scopes.length > 0
161
+ ? this.options.scopes
162
+ : exports.DEFAULT_GOOGLE_AUTH_SCOPES;
163
+ const authorizationParams = new URLSearchParams({
164
+ client_id: this.options.clientId,
165
+ redirect_uri: this.options.redirectUri,
166
+ response_type: 'code',
167
+ scope: scopes.join(' '),
168
+ access_type: this.options.accessType ?? 'offline',
169
+ prompt: this.options.prompt ?? 'select_account',
170
+ code_challenge: codeChallenge,
171
+ code_challenge_method: 'S256',
172
+ state,
173
+ });
174
+ const authorization = await this.options.authorize({
175
+ authorizationUrl: `${exports.GOOGLE_AUTHORIZATION_ENDPOINT}?${authorizationParams.toString()}`,
176
+ redirectUri: this.options.redirectUri,
177
+ state,
178
+ });
179
+ if (!authorization) {
180
+ return null;
181
+ }
182
+ if (authorization.state !== state) {
183
+ throw new Error('Google OAuth state validation failed. Please try connecting again.');
184
+ }
185
+ if (!authorization.code) {
186
+ throw new Error('Google OAuth did not return an authorization code.');
187
+ }
188
+ const response = await this.fetcher(exports.GOOGLE_TOKEN_ENDPOINT, {
189
+ method: 'POST',
190
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
191
+ body: new URLSearchParams({
192
+ client_id: this.options.clientId,
193
+ code: authorization.code,
194
+ code_verifier: codeVerifier,
195
+ grant_type: 'authorization_code',
196
+ redirect_uri: this.options.redirectUri,
197
+ }).toString(),
198
+ });
199
+ const payload = await parseTokenResponse(response);
200
+ if (!response.ok) {
201
+ throwOAuthError(payload, response.status);
202
+ }
203
+ if (!payload.access_token) {
204
+ throw new Error('Google OAuth did not return an access token.');
205
+ }
206
+ const priorTokens = await this.tokenStore.getTokens();
207
+ const storedTokens = {
208
+ accessToken: payload.access_token,
209
+ refreshToken: payload.refresh_token ?? priorTokens?.refreshToken,
210
+ idToken: payload.id_token ?? priorTokens?.idToken,
211
+ expiresAt: Date.now() + (payload.expires_in ?? 3600) * 1000,
212
+ tokenType: payload.token_type,
213
+ scope: payload.scope,
214
+ };
215
+ await this.tokenStore.setTokens(storedTokens);
216
+ const user = await this.fetchUserProfile(storedTokens.accessToken);
217
+ if (this.tokenStore.setUser) {
218
+ await this.tokenStore.setUser(user);
219
+ }
220
+ this.notify(user);
221
+ return user;
222
+ }
223
+ async signOut() {
224
+ await this.tokenStore.removeTokens();
225
+ if (this.tokenStore.setUser) {
226
+ await this.tokenStore.setUser(null);
227
+ }
228
+ this.notify(null);
229
+ }
230
+ async isSignedIn() {
231
+ const tokens = await this.tokenStore.getTokens();
232
+ return validateTokens(tokens);
233
+ }
234
+ async getUser() {
235
+ if (this.currentUser) {
236
+ return this.currentUser;
237
+ }
238
+ if (this.tokenStore.getUser) {
239
+ const cached = await this.tokenStore.getUser();
240
+ if (cached) {
241
+ this.currentUser = cached;
242
+ return cached;
243
+ }
244
+ }
245
+ const tokens = await this.tokenStore.getTokens();
246
+ if (!validateTokens(tokens)) {
247
+ return null;
248
+ }
249
+ try {
250
+ const accessToken = await this.getValidAccessToken();
251
+ const user = await this.fetchUserProfile(accessToken);
252
+ if (this.tokenStore.setUser) {
253
+ await this.tokenStore.setUser(user);
254
+ }
255
+ this.currentUser = user;
256
+ return user;
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ }
262
+ async getValidAccessToken() {
263
+ const tokens = await this.tokenStore.getTokens();
264
+ if (!validateTokens(tokens)) {
265
+ throw new Error('No Google account signed in. Please sign in first.');
266
+ }
267
+ if (tokens.expiresAt > Date.now() + 60_000) {
268
+ return tokens.accessToken;
269
+ }
270
+ if (!tokens.refreshToken) {
271
+ throw new Error('Your Google sign-in session expired. Please sign in again.');
272
+ }
273
+ const response = await this.fetcher(exports.GOOGLE_TOKEN_ENDPOINT, {
274
+ method: 'POST',
275
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
276
+ body: new URLSearchParams({
277
+ client_id: this.options.clientId,
278
+ grant_type: 'refresh_token',
279
+ refresh_token: tokens.refreshToken,
280
+ }).toString(),
281
+ });
282
+ const payload = await parseTokenResponse(response);
283
+ if (!response.ok) {
284
+ throwOAuthError(payload, response.status);
285
+ }
286
+ if (!payload.access_token) {
287
+ throw new Error('Google OAuth token refresh did not return an access token.');
288
+ }
289
+ const updatedTokens = {
290
+ accessToken: payload.access_token,
291
+ refreshToken: payload.refresh_token ?? tokens.refreshToken,
292
+ idToken: payload.id_token ?? tokens.idToken,
293
+ expiresAt: Date.now() + (payload.expires_in ?? 3600) * 1000,
294
+ tokenType: payload.token_type ?? tokens.tokenType,
295
+ scope: payload.scope ?? tokens.scope,
296
+ };
297
+ await this.tokenStore.setTokens(updatedTokens);
298
+ return updatedTokens.accessToken;
299
+ }
300
+ async fetchUserProfile(accessToken) {
301
+ const res = await this.fetcher(exports.GOOGLE_USERINFO_ENDPOINT, {
302
+ headers: { Authorization: `Bearer ${accessToken}` },
303
+ });
304
+ if (!res.ok) {
305
+ throw new Error(`Failed to fetch Google user profile (${res.status}).`);
306
+ }
307
+ const raw = (await res.json());
308
+ return {
309
+ id: raw.sub,
310
+ email: raw.email,
311
+ emailVerified: Boolean(raw.email_verified),
312
+ name: raw.name || raw.email || 'Google User',
313
+ givenName: raw.given_name,
314
+ familyName: raw.family_name,
315
+ picture: raw.picture,
316
+ locale: raw.locale,
317
+ };
318
+ }
319
+ createDriveBackupStorage() {
320
+ return new cloud_storage_js_1.GoogleDriveBackupStorage(() => this.getValidAccessToken(), this.fetcher);
321
+ }
322
+ }
323
+ exports.GoogleAuthClient = GoogleAuthClient;
@@ -0,0 +1 @@
1
+ export * from './google.js';
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./google.js"), exports);
@@ -13,7 +13,7 @@ export { NodeSQLiteAdapter } from '../adapters/local/node-sqlite.js';
13
13
  export { SupabaseRemoteAdapter } from '../adapters/remote/supabase.js';
14
14
  export { ConvexRemoteAdapter } from '../adapters/remote/convex.js';
15
15
  export { FirestoreRemoteAdapter } from '../adapters/remote/firestore.js';
16
- export { MemoryDemoRemoteAdapter } from '../adapters/remote/demo.js';
16
+ export { MemoryDemoRemoteAdapter, NoopRemoteAdapter } from '../adapters/remote/demo.js';
17
17
  /**
18
18
  * Type-safe configuration helper function (similar to defineConfig in Vite/Tailwind).
19
19
  */
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.MemoryDemoRemoteAdapter = exports.FirestoreRemoteAdapter = exports.ConvexRemoteAdapter = exports.SupabaseRemoteAdapter = exports.NodeSQLiteAdapter = exports.SqlJsAdapter = exports.LibSQLAdapter = exports.BetterSQLite3Adapter = exports.OpSQLiteAdapter = exports.ExpoSQLiteAdapter = exports.LocalFirstClient = exports.resolveRemoteAdapter = exports.resolveLocalAdapter = void 0;
17
+ exports.NoopRemoteAdapter = exports.MemoryDemoRemoteAdapter = exports.FirestoreRemoteAdapter = exports.ConvexRemoteAdapter = exports.SupabaseRemoteAdapter = exports.NodeSQLiteAdapter = exports.SqlJsAdapter = exports.LibSQLAdapter = exports.BetterSQLite3Adapter = exports.OpSQLiteAdapter = exports.ExpoSQLiteAdapter = exports.LocalFirstClient = exports.resolveRemoteAdapter = exports.resolveLocalAdapter = void 0;
18
18
  exports.defineSyncConfig = defineSyncConfig;
19
19
  exports.createSyncClient = createSyncClient;
20
20
  const client_js_1 = require("../core/client.js");
@@ -51,6 +51,7 @@ var firestore_js_1 = require("../adapters/remote/firestore.js");
51
51
  Object.defineProperty(exports, "FirestoreRemoteAdapter", { enumerable: true, get: function () { return firestore_js_1.FirestoreRemoteAdapter; } });
52
52
  var demo_js_1 = require("../adapters/remote/demo.js");
53
53
  Object.defineProperty(exports, "MemoryDemoRemoteAdapter", { enumerable: true, get: function () { return demo_js_1.MemoryDemoRemoteAdapter; } });
54
+ Object.defineProperty(exports, "NoopRemoteAdapter", { enumerable: true, get: function () { return demo_js_1.NoopRemoteAdapter; } });
54
55
  /**
55
56
  * Type-safe configuration helper function (similar to defineConfig in Vite/Tailwind).
56
57
  */
@@ -7,6 +7,8 @@ const better_sqlite3_js_1 = require("../adapters/local/better-sqlite3.js");
7
7
  const libsql_js_1 = require("../adapters/local/libsql.js");
8
8
  const sql_js_js_1 = require("../adapters/local/sql-js.js");
9
9
  const node_sqlite_js_1 = require("../adapters/local/node-sqlite.js");
10
+ const dynamic_js_1 = require("../utils/dynamic.js");
11
+ const EXPO_SQLITE_PKG = ['expo', 'sqlite'].join('-');
10
12
  /**
11
13
  * Resolves or instantiates the appropriate LocalDBAdapter based on the provided configuration.
12
14
  * Auto-detects Expo SQLite, OP-SQLite, better-sqlite3, LibSQL, sql.js, and Node.js built-in sqlite.
@@ -51,19 +53,11 @@ async function resolveLocalAdapter(config) {
51
53
  // 9. Auto-open by name using expo-sqlite if available
52
54
  if (typeof rawDb === 'string' || config.databaseName) {
53
55
  const dbName = typeof rawDb === 'string' ? rawDb : (config.databaseName ?? 'local_lite.db');
54
- let expoSqlite;
55
- try {
56
- if (typeof require === 'function') {
57
- expoSqlite = require('expo-sqlite');
58
- }
59
- }
60
- catch {
61
- // ignore
56
+ let expoSqlite = (0, dynamic_js_1.safeDynamicRequire)(EXPO_SQLITE_PKG);
57
+ if (!expoSqlite) {
58
+ expoSqlite = await (0, dynamic_js_1.safeDynamicImport)(EXPO_SQLITE_PKG);
62
59
  }
63
60
  try {
64
- if (!expoSqlite) {
65
- expoSqlite = await import('expo-sqlite');
66
- }
67
61
  if (expoSqlite && typeof expoSqlite.openDatabaseAsync === 'function') {
68
62
  const expoDb = await expoSqlite.openDatabaseAsync(dbName);
69
63
  return new expo_sqlite_js_1.ExpoSQLiteAdapter(expoDb);
@@ -2,6 +2,6 @@ import { RemoteDBAdapter } from '../adapters/remote/index.js';
2
2
  import { LocalLiteConfig } from './types.js';
3
3
  /**
4
4
  * Resolves or instantiates the appropriate RemoteDBAdapter based on the provided configuration.
5
- * Supports Supabase, Convex, Firebase / Firestore, in-memory demo adapter, or custom adapters.
5
+ * Supports Supabase, Convex, Firebase / Firestore, in-memory demo adapter, local-only mode, or custom adapters.
6
6
  */
7
7
  export declare function resolveRemoteAdapter(config: LocalLiteConfig): Promise<RemoteDBAdapter>;
@@ -5,9 +5,13 @@ const supabase_js_1 = require("../adapters/remote/supabase.js");
5
5
  const convex_js_1 = require("../adapters/remote/convex.js");
6
6
  const firestore_js_1 = require("../adapters/remote/firestore.js");
7
7
  const demo_js_1 = require("../adapters/remote/demo.js");
8
+ const dynamic_js_1 = require("../utils/dynamic.js");
9
+ // Construct dynamically to prevent Metro, Webpack, and Rollup from treating
10
+ // optional peer dependencies as unconditional build-time dependencies.
11
+ const SUPABASE_PKG = ['@supabase', 'supabase-js'].join('/');
8
12
  /**
9
13
  * Resolves or instantiates the appropriate RemoteDBAdapter based on the provided configuration.
10
- * Supports Supabase, Convex, Firebase / Firestore, in-memory demo adapter, or custom adapters.
14
+ * Supports Supabase, Convex, Firebase / Firestore, in-memory demo adapter, local-only mode, or custom adapters.
11
15
  */
12
16
  async function resolveRemoteAdapter(config) {
13
17
  // 1. Explicit custom remote adapter
@@ -53,34 +57,27 @@ async function resolveRemoteAdapter(config) {
53
57
  }
54
58
  // 5. Supabase with URL + anonKey (lazy dynamic import)
55
59
  if (config.supabase?.url && config.supabase?.anonKey) {
56
- let supabaseModule;
57
- // In CommonJS / React Native Metro environments, synchronous require avoids async chunk split failure
58
- try {
59
- if (typeof require === 'function') {
60
- supabaseModule = require('@supabase/supabase-js');
61
- }
62
- }
63
- catch {
64
- // ignore
65
- }
60
+ let supabaseModule = (0, dynamic_js_1.safeDynamicRequire)(SUPABASE_PKG);
66
61
  if (!supabaseModule && typeof globalThis?.supabase?.createClient === 'function') {
67
62
  supabaseModule = globalThis.supabase;
68
63
  }
69
64
  if (!supabaseModule) {
70
- try {
71
- supabaseModule = await import('@supabase/supabase-js');
72
- }
73
- catch {
74
- throw new Error('To initialize Supabase from url and anonKey, please install @supabase/supabase-js or provide a pre-configured client to config.supabase.client');
75
- }
65
+ supabaseModule = await (0, dynamic_js_1.safeDynamicImport)(SUPABASE_PKG);
66
+ }
67
+ if (!supabaseModule) {
68
+ throw new Error('To initialize Supabase from url and anonKey, please install @supabase/supabase-js or provide a pre-configured client to config.supabase.client');
76
69
  }
77
70
  const createClientFn = supabaseModule.createClient || supabaseModule.default?.createClient;
78
71
  const supabaseClient = createClientFn(config.supabase.url, config.supabase.anonKey);
79
72
  return new supabase_js_1.SupabaseRemoteAdapter(supabaseClient, getSupabaseOptions());
80
73
  }
81
- // 6. Explicitly selected demo backend only; never acknowledge writes into a temporary backend implicitly.
74
+ // 6. Local-only standalone mode
75
+ if (config.localOnly === true) {
76
+ return new demo_js_1.NoopRemoteAdapter();
77
+ }
78
+ // 7. Explicitly selected demo backend only; never acknowledge writes into a temporary backend implicitly.
82
79
  if (config.inMemoryRemote === true) {
83
80
  return new demo_js_1.MemoryDemoRemoteAdapter(300);
84
81
  }
85
- throw new Error('No remote adapter configured. Please specify config.supabase, config.convex, config.firebase, or config.remote. For demos only, explicitly set config.inMemoryRemote: true.');
82
+ throw new Error('No remote adapter configured. Please specify config.supabase, config.convex, config.firebase, or config.remote. For local-only use, set config.localOnly: true. For demos, set config.inMemoryRemote: true.');
86
83
  }
@@ -120,6 +120,13 @@ export interface LocalLiteConfig {
120
120
  * Default: false
121
121
  */
122
122
  inMemoryRemote?: boolean;
123
+ /**
124
+ * Run purely local-only without requiring any remote cloud backend.
125
+ * Disables cloud sync pushes and pulls while preserving all local SQLite features,
126
+ * schema management, outbox tracking, and transactions.
127
+ * Default: false
128
+ */
129
+ localOnly?: boolean;
123
130
  /**
124
131
  * Conflict resolution strategy. Default: 'last-write-wins'
125
132
  */
@@ -7,6 +7,7 @@ const outbox_store_js_1 = require("./outbox-store.js");
7
7
  const meta_store_js_1 = require("./meta-store.js");
8
8
  const table_manager_js_1 = require("./table-manager.js");
9
9
  const logger_js_1 = require("../utils/logger.js");
10
+ const REACT_NATIVE_PKG = ['react', 'native'].join('-');
10
11
  /** Maximum outbox records sent per push cycle. */
11
12
  const PUSH_BATCH_SIZE = 50;
12
13
  /** Maximum incoming delta changes applied in a single database transaction. */
package/dist/index.d.ts CHANGED
@@ -18,3 +18,5 @@ export * from './utils/uuid.js';
18
18
  export * from './utils/sql.js';
19
19
  export * from './utils/logger.js';
20
20
  export * from './schema/index.js';
21
+ export * from './auth/index.js';
22
+ export * from './backups/index.js';
package/dist/index.js CHANGED
@@ -33,3 +33,5 @@ __exportStar(require("./utils/uuid.js"), exports);
33
33
  __exportStar(require("./utils/sql.js"), exports);
34
34
  __exportStar(require("./utils/logger.js"), exports);
35
35
  __exportStar(require("./schema/index.js"), exports);
36
+ __exportStar(require("./auth/index.js"), exports);
37
+ __exportStar(require("./backups/index.js"), exports);
@@ -0,0 +1,11 @@
1
+ import type { GoogleAuthClient, GoogleUser } from '../auth/google.js';
2
+ export interface UseGoogleAuthResult {
3
+ user: GoogleUser | null;
4
+ isSignedIn: boolean;
5
+ loading: boolean;
6
+ error: Error | null;
7
+ signIn: () => Promise<GoogleUser | null>;
8
+ signOut: () => Promise<void>;
9
+ refetchUser: () => Promise<GoogleUser | null>;
10
+ }
11
+ export declare function useGoogleAuth(client: GoogleAuthClient | null | undefined): UseGoogleAuthResult;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useGoogleAuth = useGoogleAuth;
4
+ const react_1 = require("react");
5
+ function useGoogleAuth(client) {
6
+ const [user, setUser] = (0, react_1.useState)(null);
7
+ const [loading, setLoading] = (0, react_1.useState)(true);
8
+ const [error, setError] = (0, react_1.useState)(null);
9
+ (0, react_1.useEffect)(() => {
10
+ if (!client) {
11
+ setUser(null);
12
+ setLoading(false);
13
+ return;
14
+ }
15
+ let isMounted = true;
16
+ client
17
+ .getUser()
18
+ .then(currentUser => {
19
+ if (isMounted) {
20
+ setUser(currentUser);
21
+ setLoading(false);
22
+ }
23
+ })
24
+ .catch(err => {
25
+ if (isMounted) {
26
+ setError(err instanceof Error ? err : new Error(String(err)));
27
+ setLoading(false);
28
+ }
29
+ });
30
+ const unsubscribe = client.onAuthStateChange(updatedUser => {
31
+ if (isMounted) {
32
+ setUser(updatedUser);
33
+ }
34
+ });
35
+ return () => {
36
+ isMounted = false;
37
+ unsubscribe();
38
+ };
39
+ }, [client]);
40
+ const signIn = (0, react_1.useCallback)(async () => {
41
+ if (!client)
42
+ throw new Error('GoogleAuthClient is not initialized.');
43
+ setError(null);
44
+ setLoading(true);
45
+ try {
46
+ const res = await client.signIn();
47
+ setUser(res);
48
+ return res;
49
+ }
50
+ catch (err) {
51
+ const e = err instanceof Error ? err : new Error(String(err));
52
+ setError(e);
53
+ throw e;
54
+ }
55
+ finally {
56
+ setLoading(false);
57
+ }
58
+ }, [client]);
59
+ const signOut = (0, react_1.useCallback)(async () => {
60
+ if (!client)
61
+ return;
62
+ setError(null);
63
+ setLoading(true);
64
+ try {
65
+ await client.signOut();
66
+ setUser(null);
67
+ }
68
+ catch (err) {
69
+ const e = err instanceof Error ? err : new Error(String(err));
70
+ setError(e);
71
+ throw e;
72
+ }
73
+ finally {
74
+ setLoading(false);
75
+ }
76
+ }, [client]);
77
+ const refetchUser = (0, react_1.useCallback)(async () => {
78
+ if (!client)
79
+ return null;
80
+ const res = await client.getUser();
81
+ setUser(res);
82
+ return res;
83
+ }, [client]);
84
+ return {
85
+ user,
86
+ isSignedIn: Boolean(user),
87
+ loading,
88
+ error,
89
+ signIn,
90
+ signOut,
91
+ refetchUser,
92
+ };
93
+ }
@@ -117,3 +117,4 @@ export * from './theme.js';
117
117
  export * from './BenchmarkModal.js';
118
118
  export { SyncInspector, SyncIndicator } from './SyncInspector.js';
119
119
  export type { SyncInspectorProps, SyncIndicatorProps } from './SyncInspector.js';
120
+ export * from './auth.js';
@@ -594,3 +594,4 @@ __exportStar(require("./BenchmarkModal.js"), exports);
594
594
  var SyncInspector_js_1 = require("./SyncInspector.js");
595
595
  Object.defineProperty(exports, "SyncInspector", { enumerable: true, get: function () { return SyncInspector_js_1.SyncInspector; } });
596
596
  Object.defineProperty(exports, "SyncIndicator", { enumerable: true, get: function () { return SyncInspector_js_1.SyncIndicator; } });
597
+ __exportStar(require("./auth.js"), exports);
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Safely loads an optional dependency at runtime without triggering
3
+ * ahead-of-time static analysis in bundlers like Metro, Webpack, or Rollup.
4
+ */
5
+ export declare function safeDynamicRequire(pkgName: string): any;
6
+ export declare function safeDynamicImport(pkgName: string): Promise<any>;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.safeDynamicRequire = safeDynamicRequire;
4
+ exports.safeDynamicImport = safeDynamicImport;
5
+ /**
6
+ * Safely loads an optional dependency at runtime without triggering
7
+ * ahead-of-time static analysis in bundlers like Metro, Webpack, or Rollup.
8
+ */
9
+ function safeDynamicRequire(pkgName) {
10
+ try {
11
+ const req = typeof __non_webpack_require__ !== 'undefined'
12
+ ? __non_webpack_require__
13
+ : typeof require === 'function'
14
+ ? require
15
+ : undefined;
16
+ return req ? req(pkgName) : undefined;
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ async function safeDynamicImport(pkgName) {
23
+ try {
24
+ // Metro's AST worker rejects `import(dynamicVariable)` with "Invalid call at line ...: import(...)".
25
+ // Evaluating via new Function bypasses the bundler AST parser entirely while working in Node/browser.
26
+ const dynamicImport = new Function('specifier', 'return import(specifier);');
27
+ return await dynamicImport(pkgName);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-lite",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -46,6 +46,11 @@
46
46
  "import": "./dist/adapters/remote/firestore.js",
47
47
  "default": "./dist/adapters/remote/firestore.js"
48
48
  },
49
+ "./auth": {
50
+ "types": "./dist/auth/index.d.ts",
51
+ "import": "./dist/auth/index.js",
52
+ "default": "./dist/auth/index.js"
53
+ },
49
54
  "./backups": {
50
55
  "types": "./dist/backups/index.d.ts",
51
56
  "import": "./dist/backups/index.js",