local-lite 0.0.12 → 0.0.14

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
@@ -19,9 +19,11 @@ Built around the **Transactional Outbox Pattern**, `local-lite` ensures instant,
19
19
  * **[Configuration Reference](https://georgechitechi.github.io/local-lite/#/configuration)** - Options for `defineSyncConfig()`, retries, and loggers
20
20
  * **[Local SQLite Adapters](https://georgechitechi.github.io/local-lite/#/adapters-local)** - Expo SQLite, OP-SQLite, better-sqlite3, node:sqlite, and LibSQL
21
21
  * **[Remote Cloud Adapters](https://georgechitechi.github.io/local-lite/#/adapters-remote)** - Supabase, Convex, Cloud Firestore, and custom adapters
22
- * **[Database Backups](https://georgechitechi.github.io/local-lite/#/adapters-local?id=database-backups)** - SQLite snapshots with a Google Drive OAuth/PKCE client
23
- * **[React & Expo Hooks](https://georgechitechi.github.io/local-lite/#/react-hooks)** - `useLiveQuery()` and `useSyncStatus()`
24
- * **[Conflict Resolution](https://georgechitechi.github.io/local-lite/#/conflict-resolution)** - LWW, Client-Wins, Server-Wins, and custom resolvers
22
+ * **[Authentication (Google, Microsoft, GitHub, Apple)](https://georgechitechi.github.io/local-lite/#/auth)** - Direct PKCE & native Apple authentication clients & hooks
23
+ * **[Database Backups & Encryption](https://georgechitechi.github.io/local-lite/#/backups)** - Google Drive, OneDrive, and Zero-Knowledge AES-256-GCM snapshots
24
+ * **[Conflict Resolution & Field Merge](https://georgechitechi.github.io/local-lite/#/conflict-resolution)** - 3-Way field merging, LWW, Client-Wins, and Server-Wins
25
+ * **[Maintenance & Data Portability](https://georgechitechi.github.io/local-lite/#/maintenance-portability)** - VACUUM, WAL checkpoints, outbox pruning, and JSON/CSV export/import
26
+ * **[React & Expo Hooks](https://georgechitechi.github.io/local-lite/#/react-hooks)** - `useLiveQuery()`, `useSyncStatus()`, and auth hooks
25
27
 
26
28
  ---
27
29
 
@@ -0,0 +1,91 @@
1
+ export declare const APPLE_AUTHORIZATION_ENDPOINT = "https://appleid.apple.com/auth/authorize";
2
+ export declare const APPLE_TOKEN_ENDPOINT = "https://appleid.apple.com/auth/token";
3
+ export declare const DEFAULT_APPLE_AUTH_SCOPES: string[];
4
+ export interface AppleUser {
5
+ id: string;
6
+ email: string;
7
+ name: string;
8
+ givenName?: string;
9
+ familyName?: string;
10
+ identityToken?: string;
11
+ authorizationCode?: string;
12
+ }
13
+ export interface AppleAuthTokens {
14
+ accessToken?: string;
15
+ refreshToken?: string;
16
+ idToken: string;
17
+ expiresAt?: number;
18
+ tokenType?: string;
19
+ }
20
+ export interface AppleAuthTokenStore {
21
+ getTokens(): Promise<AppleAuthTokens | null>;
22
+ setTokens(tokens: AppleAuthTokens): Promise<void>;
23
+ removeTokens(): Promise<void>;
24
+ getUser(): Promise<AppleUser | null>;
25
+ setUser(user: AppleUser | null): Promise<void>;
26
+ }
27
+ export interface AppleAuthKeyValueStorage {
28
+ getItem(key: string): Promise<unknown>;
29
+ setItem(key: string, value: unknown): Promise<void>;
30
+ removeItem(key: string): Promise<void>;
31
+ }
32
+ export interface AppleAuthorizationRequest {
33
+ authorizationUrl: string;
34
+ redirectUri: string;
35
+ state: string;
36
+ nonce: string;
37
+ }
38
+ export interface AppleAuthorizationResponse {
39
+ code?: string;
40
+ id_token?: string;
41
+ state?: string;
42
+ user?: {
43
+ name?: {
44
+ firstName?: string;
45
+ lastName?: string;
46
+ };
47
+ email?: string;
48
+ };
49
+ }
50
+ export interface AppleNativeCredential {
51
+ user: string;
52
+ email?: string | null;
53
+ fullName?: {
54
+ givenName?: string | null;
55
+ familyName?: string | null;
56
+ } | null;
57
+ identityToken?: string | null;
58
+ authorizationCode?: string | null;
59
+ }
60
+ export interface AppleAuthClientOptions {
61
+ clientId: string;
62
+ redirectUri?: string;
63
+ scopes?: ('name' | 'email' | string)[];
64
+ authorize?: (request: AppleAuthorizationRequest) => Promise<AppleAuthorizationResponse | null>;
65
+ tokenStore?: AppleAuthTokenStore;
66
+ storage?: AppleAuthKeyValueStorage;
67
+ randomBytes?: (length: number) => Uint8Array;
68
+ fetch?: typeof fetch;
69
+ }
70
+ export declare function createInMemoryAppleTokenStore(): AppleAuthTokenStore;
71
+ export declare function createAppleAuthTokenStore(storage: AppleAuthKeyValueStorage, prefix?: string): AppleAuthTokenStore;
72
+ export declare class AppleAuthClient {
73
+ private readonly options;
74
+ private readonly tokenStore;
75
+ private readonly listeners;
76
+ private currentUser;
77
+ constructor(options: AppleAuthClientOptions);
78
+ onAuthStateChange(listener: (user: AppleUser | null) => void): () => void;
79
+ private notify;
80
+ /**
81
+ * Signs in with Apple.
82
+ * If a native credential object (from expo-apple-authentication or native ASAuthorizationAppleIDCredential)
83
+ * is passed, processes and verifies it directly.
84
+ * Otherwise, launches the PKCE web/browser authorization flow.
85
+ */
86
+ signIn(nativeCredential?: AppleNativeCredential): Promise<AppleUser | null>;
87
+ private processNativeCredential;
88
+ signOut(): Promise<void>;
89
+ isSignedIn(): Promise<boolean>;
90
+ getUser(): Promise<AppleUser | null>;
91
+ }
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AppleAuthClient = exports.DEFAULT_APPLE_AUTH_SCOPES = exports.APPLE_TOKEN_ENDPOINT = exports.APPLE_AUTHORIZATION_ENDPOINT = void 0;
4
+ exports.createInMemoryAppleTokenStore = createInMemoryAppleTokenStore;
5
+ exports.createAppleAuthTokenStore = createAppleAuthTokenStore;
6
+ exports.APPLE_AUTHORIZATION_ENDPOINT = 'https://appleid.apple.com/auth/authorize';
7
+ exports.APPLE_TOKEN_ENDPOINT = 'https://appleid.apple.com/auth/token';
8
+ exports.DEFAULT_APPLE_AUTH_SCOPES = ['name', 'email'];
9
+ function secureRandomBytes(length) {
10
+ const crypto = globalThis.crypto;
11
+ if (!crypto?.getRandomValues) {
12
+ throw new Error('Secure random number generation is unavailable.');
13
+ }
14
+ return crypto.getRandomValues(new Uint8Array(length));
15
+ }
16
+ function base64UrlEncode(bytes) {
17
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
18
+ let result = '';
19
+ for (let index = 0; index < bytes.length; index += 3) {
20
+ const first = bytes[index];
21
+ const second = bytes[index + 1];
22
+ const third = bytes[index + 2];
23
+ result += alphabet[first >> 2];
24
+ result += alphabet[((first & 3) << 4) | (second === undefined ? 0 : second >> 4)];
25
+ if (second !== undefined) {
26
+ result += alphabet[((second & 15) << 2) | (third === undefined ? 0 : third >> 6)];
27
+ }
28
+ if (third !== undefined)
29
+ result += alphabet[third & 63];
30
+ }
31
+ return result;
32
+ }
33
+ function parseJwtClaims(jwt) {
34
+ try {
35
+ const parts = jwt.split('.');
36
+ if (parts.length < 2)
37
+ return {};
38
+ let base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
39
+ while (base64.length % 4) {
40
+ base64 += '=';
41
+ }
42
+ const json = typeof atob === 'function'
43
+ ? atob(base64)
44
+ : Buffer.from(base64, 'base64').toString('utf8');
45
+ return JSON.parse(json);
46
+ }
47
+ catch {
48
+ return {};
49
+ }
50
+ }
51
+ function validateTokens(value) {
52
+ if (!value || typeof value !== 'object')
53
+ return false;
54
+ const tokens = value;
55
+ return typeof tokens.idToken === 'string';
56
+ }
57
+ function validateUser(value) {
58
+ if (!value || typeof value !== 'object')
59
+ return false;
60
+ const user = value;
61
+ return typeof user.id === 'string' && typeof user.email === 'string';
62
+ }
63
+ function createInMemoryAppleTokenStore() {
64
+ let storedTokens = null;
65
+ let storedUser = null;
66
+ return {
67
+ getTokens: async () => storedTokens,
68
+ setTokens: async (tokens) => {
69
+ storedTokens = tokens;
70
+ },
71
+ removeTokens: async () => {
72
+ storedTokens = null;
73
+ },
74
+ getUser: async () => storedUser,
75
+ setUser: async (user) => {
76
+ storedUser = user;
77
+ },
78
+ };
79
+ }
80
+ function createAppleAuthTokenStore(storage, prefix = 'local-lite-apple-auth') {
81
+ const tokensKey = `${prefix}-tokens`;
82
+ const userKey = `${prefix}-user`;
83
+ return {
84
+ getTokens: async () => {
85
+ const raw = await storage.getItem(tokensKey);
86
+ return validateTokens(raw) ? raw : null;
87
+ },
88
+ setTokens: tokens => storage.setItem(tokensKey, tokens),
89
+ removeTokens: () => storage.removeItem(tokensKey),
90
+ getUser: async () => {
91
+ const raw = await storage.getItem(userKey);
92
+ return validateUser(raw) ? raw : null;
93
+ },
94
+ setUser: async (user) => {
95
+ if (user) {
96
+ await storage.setItem(userKey, user);
97
+ }
98
+ else {
99
+ await storage.removeItem(userKey);
100
+ }
101
+ },
102
+ };
103
+ }
104
+ class AppleAuthClient {
105
+ options;
106
+ tokenStore;
107
+ listeners = new Set();
108
+ currentUser = null;
109
+ constructor(options) {
110
+ this.options = options;
111
+ if (!options.clientId?.trim()) {
112
+ throw new Error('Apple OAuth client ID (Service ID or App Bundle ID) is required.');
113
+ }
114
+ if (options.tokenStore) {
115
+ this.tokenStore = options.tokenStore;
116
+ }
117
+ else if (options.storage) {
118
+ this.tokenStore = createAppleAuthTokenStore(options.storage);
119
+ }
120
+ else {
121
+ this.tokenStore = createInMemoryAppleTokenStore();
122
+ }
123
+ }
124
+ onAuthStateChange(listener) {
125
+ this.listeners.add(listener);
126
+ return () => {
127
+ this.listeners.delete(listener);
128
+ };
129
+ }
130
+ notify(user) {
131
+ this.currentUser = user;
132
+ for (const listener of this.listeners) {
133
+ try {
134
+ listener(user);
135
+ }
136
+ catch (err) {
137
+ console.error('Error in AppleAuthClient listener:', err);
138
+ }
139
+ }
140
+ }
141
+ /**
142
+ * Signs in with Apple.
143
+ * If a native credential object (from expo-apple-authentication or native ASAuthorizationAppleIDCredential)
144
+ * is passed, processes and verifies it directly.
145
+ * Otherwise, launches the PKCE web/browser authorization flow.
146
+ */
147
+ async signIn(nativeCredential) {
148
+ if (nativeCredential) {
149
+ return this.processNativeCredential(nativeCredential);
150
+ }
151
+ if (!this.options.authorize) {
152
+ throw new Error('AppleAuthClient requires an authorize callback for web/browser authorization, or a native credential passed to signIn().');
153
+ }
154
+ if (!this.options.redirectUri) {
155
+ throw new Error('Apple OAuth redirectUri is required for web authorization flow.');
156
+ }
157
+ const randomBytes = this.options.randomBytes ?? secureRandomBytes;
158
+ const state = base64UrlEncode(randomBytes(32));
159
+ const nonce = base64UrlEncode(randomBytes(32));
160
+ const scopes = this.options.scopes && this.options.scopes.length > 0
161
+ ? this.options.scopes.join(' ')
162
+ : exports.DEFAULT_APPLE_AUTH_SCOPES.join(' ');
163
+ const params = new URLSearchParams({
164
+ client_id: this.options.clientId,
165
+ redirect_uri: this.options.redirectUri,
166
+ response_type: 'code id_token',
167
+ response_mode: 'form_post',
168
+ scope: scopes,
169
+ state,
170
+ nonce,
171
+ });
172
+ const authorization = await this.options.authorize({
173
+ authorizationUrl: `${exports.APPLE_AUTHORIZATION_ENDPOINT}?${params.toString()}`,
174
+ redirectUri: this.options.redirectUri,
175
+ state,
176
+ nonce,
177
+ });
178
+ if (!authorization) {
179
+ return null;
180
+ }
181
+ if (authorization.state && authorization.state !== state) {
182
+ throw new Error('Apple OAuth state validation failed. Please try connecting again.');
183
+ }
184
+ if (!authorization.id_token) {
185
+ throw new Error('Apple OAuth did not return an id_token.');
186
+ }
187
+ const claims = parseJwtClaims(authorization.id_token);
188
+ const id = claims.sub || 'apple-user';
189
+ const email = claims.email || authorization.user?.email || `${id}@privaterelay.appleid.com`;
190
+ const givenName = authorization.user?.name?.firstName;
191
+ const familyName = authorization.user?.name?.lastName;
192
+ const nameParts = [givenName, familyName].filter(Boolean);
193
+ const name = nameParts.length > 0 ? nameParts.join(' ') : email.split('@')[0];
194
+ const tokens = {
195
+ idToken: authorization.id_token,
196
+ expiresAt: claims.exp ? claims.exp * 1000 : undefined,
197
+ };
198
+ await this.tokenStore.setTokens(tokens);
199
+ const user = {
200
+ id,
201
+ email,
202
+ name,
203
+ givenName,
204
+ familyName,
205
+ identityToken: authorization.id_token,
206
+ authorizationCode: authorization.code,
207
+ };
208
+ await this.tokenStore.setUser(user);
209
+ this.notify(user);
210
+ return user;
211
+ }
212
+ async processNativeCredential(cred) {
213
+ const claims = cred.identityToken ? parseJwtClaims(cred.identityToken) : {};
214
+ const id = cred.user || claims.sub;
215
+ const email = cred.email || claims.email || `${id}@privaterelay.appleid.com`;
216
+ const givenName = cred.fullName?.givenName || undefined;
217
+ const familyName = cred.fullName?.familyName || undefined;
218
+ const nameParts = [givenName, familyName].filter(Boolean);
219
+ const name = nameParts.length > 0 ? nameParts.join(' ') : email.split('@')[0];
220
+ const priorUser = await this.tokenStore.getUser();
221
+ // Apple only returns full name and email on the very first sign in; preserve prior if present
222
+ const resolvedGivenName = givenName || priorUser?.givenName;
223
+ const resolvedFamilyName = familyName || priorUser?.familyName;
224
+ const resolvedName = name !== email.split('@')[0] ? name : priorUser?.name || name;
225
+ const user = {
226
+ id,
227
+ email: cred.email || priorUser?.email || email,
228
+ name: resolvedName,
229
+ givenName: resolvedGivenName,
230
+ familyName: resolvedFamilyName,
231
+ identityToken: cred.identityToken || undefined,
232
+ authorizationCode: cred.authorizationCode || undefined,
233
+ };
234
+ if (cred.identityToken) {
235
+ await this.tokenStore.setTokens({
236
+ idToken: cred.identityToken,
237
+ expiresAt: claims.exp ? claims.exp * 1000 : undefined,
238
+ });
239
+ }
240
+ await this.tokenStore.setUser(user);
241
+ this.notify(user);
242
+ return user;
243
+ }
244
+ async signOut() {
245
+ await this.tokenStore.removeTokens();
246
+ await this.tokenStore.setUser(null);
247
+ this.notify(null);
248
+ }
249
+ async isSignedIn() {
250
+ const tokens = await this.tokenStore.getTokens();
251
+ return validateTokens(tokens);
252
+ }
253
+ async getUser() {
254
+ if (this.currentUser)
255
+ return this.currentUser;
256
+ const cached = await this.tokenStore.getUser();
257
+ if (cached) {
258
+ this.currentUser = cached;
259
+ return cached;
260
+ }
261
+ return null;
262
+ }
263
+ }
264
+ exports.AppleAuthClient = AppleAuthClient;
@@ -0,0 +1,72 @@
1
+ export declare const GITHUB_AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize";
2
+ export declare const GITHUB_TOKEN_ENDPOINT = "https://github.com/login/oauth/access_token";
3
+ export declare const GITHUB_API_ENDPOINT = "https://api.github.com";
4
+ export declare const DEFAULT_GITHUB_AUTH_SCOPES: string[];
5
+ export interface GitHubUser {
6
+ id: string;
7
+ login: string;
8
+ name: string;
9
+ email: string;
10
+ avatarUrl?: string;
11
+ bio?: string;
12
+ company?: string;
13
+ location?: string;
14
+ }
15
+ export interface GitHubAuthTokens {
16
+ accessToken: string;
17
+ tokenType?: string;
18
+ scope?: string;
19
+ expiresAt?: number;
20
+ }
21
+ export interface GitHubAuthTokenStore {
22
+ getTokens(): Promise<GitHubAuthTokens | null>;
23
+ setTokens(tokens: GitHubAuthTokens): Promise<void>;
24
+ removeTokens(): Promise<void>;
25
+ getUser(): Promise<GitHubUser | null>;
26
+ setUser(user: GitHubUser | null): Promise<void>;
27
+ }
28
+ export interface GitHubAuthKeyValueStorage {
29
+ getItem(key: string): Promise<unknown>;
30
+ setItem(key: string, value: unknown): Promise<void>;
31
+ removeItem(key: string): Promise<void>;
32
+ }
33
+ export interface GitHubAuthorizationRequest {
34
+ authorizationUrl: string;
35
+ redirectUri: string;
36
+ state: string;
37
+ }
38
+ export interface GitHubAuthorizationResponse {
39
+ code: string;
40
+ state: string;
41
+ }
42
+ export interface GitHubAuthClientOptions {
43
+ clientId: string;
44
+ clientSecret?: string;
45
+ tokenExchangeUrl?: string;
46
+ redirectUri: string;
47
+ scopes?: string[];
48
+ authorize: (request: GitHubAuthorizationRequest) => Promise<GitHubAuthorizationResponse | null>;
49
+ tokenStore?: GitHubAuthTokenStore;
50
+ storage?: GitHubAuthKeyValueStorage;
51
+ randomBytes?: (length: number) => Uint8Array;
52
+ fetch?: typeof fetch;
53
+ allowSignup?: boolean;
54
+ }
55
+ export declare function createInMemoryGitHubTokenStore(): GitHubAuthTokenStore;
56
+ export declare function createGitHubAuthTokenStore(storage: GitHubAuthKeyValueStorage, prefix?: string): GitHubAuthTokenStore;
57
+ export declare class GitHubAuthClient {
58
+ private readonly options;
59
+ private readonly tokenStore;
60
+ private readonly fetcher;
61
+ private readonly listeners;
62
+ private currentUser;
63
+ constructor(options: GitHubAuthClientOptions);
64
+ onAuthStateChange(listener: (user: GitHubUser | null) => void): () => void;
65
+ private notify;
66
+ signIn(): Promise<GitHubUser | null>;
67
+ signOut(): Promise<void>;
68
+ isSignedIn(): Promise<boolean>;
69
+ getUser(): Promise<GitHubUser | null>;
70
+ getValidAccessToken(): Promise<string>;
71
+ private fetchUserProfile;
72
+ }