local-lite 0.0.11 → 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 +44 -0
- package/dist/auth/google.d.ts +75 -0
- package/dist/auth/google.js +323 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/react/auth.d.ts +11 -0
- package/dist/react/auth.js +93 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +1 -0
- package/package.json +6 -1
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**):
|
|
@@ -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);
|
package/dist/index.d.ts
CHANGED
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
|
+
}
|
package/dist/react/index.d.ts
CHANGED
package/dist/react/index.js
CHANGED
|
@@ -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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-lite",
|
|
3
|
-
"version": "0.0.
|
|
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",
|