local-lite 0.0.11 → 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 +49 -3
- package/dist/auth/apple.d.ts +91 -0
- package/dist/auth/apple.js +264 -0
- package/dist/auth/github.d.ts +72 -0
- package/dist/auth/github.js +284 -0
- package/dist/auth/google.d.ts +75 -0
- package/dist/auth/google.js +323 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.js +20 -0
- package/dist/auth/microsoft.d.ts +76 -0
- package/dist/auth/microsoft.js +310 -0
- package/dist/backups/encryption.d.ts +18 -0
- package/dist/backups/encryption.js +122 -0
- package/dist/backups/index.d.ts +2 -0
- package/dist/backups/index.js +11 -1
- package/dist/backups/onedrive.d.ts +22 -0
- package/dist/backups/onedrive.js +114 -0
- package/dist/core/client.d.ts +2 -0
- package/dist/core/client.js +3 -0
- package/dist/core/conflict.d.ts +8 -1
- package/dist/core/conflict.js +75 -0
- package/dist/core/maintenance.d.ts +66 -0
- package/dist/core/maintenance.js +157 -0
- package/dist/core/sync-worker.js +1 -1
- package/dist/core/types.d.ts +34 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/react/auth.d.ts +46 -0
- package/dist/react/auth.js +365 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +1 -0
- package/dist/utils/portability.d.ts +73 -0
- package/dist/utils/portability.js +276 -0
- package/package.json +6 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GitHubAuthClient = exports.DEFAULT_GITHUB_AUTH_SCOPES = exports.GITHUB_API_ENDPOINT = exports.GITHUB_TOKEN_ENDPOINT = exports.GITHUB_AUTHORIZATION_ENDPOINT = void 0;
|
|
4
|
+
exports.createInMemoryGitHubTokenStore = createInMemoryGitHubTokenStore;
|
|
5
|
+
exports.createGitHubAuthTokenStore = createGitHubAuthTokenStore;
|
|
6
|
+
exports.GITHUB_AUTHORIZATION_ENDPOINT = 'https://github.com/login/oauth/authorize';
|
|
7
|
+
exports.GITHUB_TOKEN_ENDPOINT = 'https://github.com/login/oauth/access_token';
|
|
8
|
+
exports.GITHUB_API_ENDPOINT = 'https://api.github.com';
|
|
9
|
+
exports.DEFAULT_GITHUB_AUTH_SCOPES = ['read:user', 'user:email'];
|
|
10
|
+
function secureRandomBytes(length) {
|
|
11
|
+
const crypto = globalThis.crypto;
|
|
12
|
+
if (!crypto?.getRandomValues) {
|
|
13
|
+
throw new Error('Secure random number generation is unavailable.');
|
|
14
|
+
}
|
|
15
|
+
return crypto.getRandomValues(new Uint8Array(length));
|
|
16
|
+
}
|
|
17
|
+
function base64UrlEncode(bytes) {
|
|
18
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
19
|
+
let result = '';
|
|
20
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
21
|
+
const first = bytes[index];
|
|
22
|
+
const second = bytes[index + 1];
|
|
23
|
+
const third = bytes[index + 2];
|
|
24
|
+
result += alphabet[first >> 2];
|
|
25
|
+
result += alphabet[((first & 3) << 4) | (second === undefined ? 0 : second >> 4)];
|
|
26
|
+
if (second !== undefined) {
|
|
27
|
+
result += alphabet[((second & 15) << 2) | (third === undefined ? 0 : third >> 6)];
|
|
28
|
+
}
|
|
29
|
+
if (third !== undefined)
|
|
30
|
+
result += alphabet[third & 63];
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function validateTokens(value) {
|
|
35
|
+
if (!value || typeof value !== 'object')
|
|
36
|
+
return false;
|
|
37
|
+
const tokens = value;
|
|
38
|
+
return typeof tokens.accessToken === 'string';
|
|
39
|
+
}
|
|
40
|
+
function validateUser(value) {
|
|
41
|
+
if (!value || typeof value !== 'object')
|
|
42
|
+
return false;
|
|
43
|
+
const user = value;
|
|
44
|
+
return (typeof user.id === 'string' &&
|
|
45
|
+
typeof user.login === 'string' &&
|
|
46
|
+
typeof user.name === 'string');
|
|
47
|
+
}
|
|
48
|
+
function createInMemoryGitHubTokenStore() {
|
|
49
|
+
let storedTokens = null;
|
|
50
|
+
let storedUser = null;
|
|
51
|
+
return {
|
|
52
|
+
getTokens: async () => storedTokens,
|
|
53
|
+
setTokens: async (tokens) => {
|
|
54
|
+
storedTokens = tokens;
|
|
55
|
+
},
|
|
56
|
+
removeTokens: async () => {
|
|
57
|
+
storedTokens = null;
|
|
58
|
+
},
|
|
59
|
+
getUser: async () => storedUser,
|
|
60
|
+
setUser: async (user) => {
|
|
61
|
+
storedUser = user;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function createGitHubAuthTokenStore(storage, prefix = 'local-lite-github-auth') {
|
|
66
|
+
const tokensKey = `${prefix}-tokens`;
|
|
67
|
+
const userKey = `${prefix}-user`;
|
|
68
|
+
return {
|
|
69
|
+
getTokens: async () => {
|
|
70
|
+
const raw = await storage.getItem(tokensKey);
|
|
71
|
+
return validateTokens(raw) ? raw : null;
|
|
72
|
+
},
|
|
73
|
+
setTokens: tokens => storage.setItem(tokensKey, tokens),
|
|
74
|
+
removeTokens: () => storage.removeItem(tokensKey),
|
|
75
|
+
getUser: async () => {
|
|
76
|
+
const raw = await storage.getItem(userKey);
|
|
77
|
+
return validateUser(raw) ? raw : null;
|
|
78
|
+
},
|
|
79
|
+
setUser: async (user) => {
|
|
80
|
+
if (user) {
|
|
81
|
+
await storage.setItem(userKey, user);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
await storage.removeItem(userKey);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
class GitHubAuthClient {
|
|
90
|
+
options;
|
|
91
|
+
tokenStore;
|
|
92
|
+
fetcher;
|
|
93
|
+
listeners = new Set();
|
|
94
|
+
currentUser = null;
|
|
95
|
+
constructor(options) {
|
|
96
|
+
this.options = options;
|
|
97
|
+
if (!options.clientId?.trim()) {
|
|
98
|
+
throw new Error('GitHub OAuth client ID is required.');
|
|
99
|
+
}
|
|
100
|
+
if (!options.redirectUri?.trim()) {
|
|
101
|
+
throw new Error('GitHub OAuth redirect URI is required.');
|
|
102
|
+
}
|
|
103
|
+
this.fetcher = options.fetch ?? fetch;
|
|
104
|
+
if (options.tokenStore) {
|
|
105
|
+
this.tokenStore = options.tokenStore;
|
|
106
|
+
}
|
|
107
|
+
else if (options.storage) {
|
|
108
|
+
this.tokenStore = createGitHubAuthTokenStore(options.storage);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
this.tokenStore = createInMemoryGitHubTokenStore();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
onAuthStateChange(listener) {
|
|
115
|
+
this.listeners.add(listener);
|
|
116
|
+
return () => {
|
|
117
|
+
this.listeners.delete(listener);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
notify(user) {
|
|
121
|
+
this.currentUser = user;
|
|
122
|
+
for (const listener of this.listeners) {
|
|
123
|
+
try {
|
|
124
|
+
listener(user);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error('Error in GitHubAuthClient listener:', err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async signIn() {
|
|
132
|
+
const randomBytes = this.options.randomBytes ?? secureRandomBytes;
|
|
133
|
+
const stateBytes = randomBytes(32);
|
|
134
|
+
if (stateBytes.length !== 32) {
|
|
135
|
+
throw new Error('The randomBytes provider returned an invalid number of bytes.');
|
|
136
|
+
}
|
|
137
|
+
const state = base64UrlEncode(stateBytes);
|
|
138
|
+
const scopes = this.options.scopes && this.options.scopes.length > 0
|
|
139
|
+
? this.options.scopes
|
|
140
|
+
: exports.DEFAULT_GITHUB_AUTH_SCOPES;
|
|
141
|
+
const authorizationParams = new URLSearchParams({
|
|
142
|
+
client_id: this.options.clientId,
|
|
143
|
+
redirect_uri: this.options.redirectUri,
|
|
144
|
+
scope: scopes.join(' '),
|
|
145
|
+
state,
|
|
146
|
+
allow_signup: String(this.options.allowSignup ?? true),
|
|
147
|
+
});
|
|
148
|
+
const authorization = await this.options.authorize({
|
|
149
|
+
authorizationUrl: `${exports.GITHUB_AUTHORIZATION_ENDPOINT}?${authorizationParams.toString()}`,
|
|
150
|
+
redirectUri: this.options.redirectUri,
|
|
151
|
+
state,
|
|
152
|
+
});
|
|
153
|
+
if (!authorization) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (authorization.state !== state) {
|
|
157
|
+
throw new Error('GitHub OAuth state validation failed. Please try connecting again.');
|
|
158
|
+
}
|
|
159
|
+
if (!authorization.code) {
|
|
160
|
+
throw new Error('GitHub OAuth did not return an authorization code.');
|
|
161
|
+
}
|
|
162
|
+
const tokenEndpoint = this.options.tokenExchangeUrl || exports.GITHUB_TOKEN_ENDPOINT;
|
|
163
|
+
const requestPayload = {
|
|
164
|
+
client_id: this.options.clientId,
|
|
165
|
+
code: authorization.code,
|
|
166
|
+
redirect_uri: this.options.redirectUri,
|
|
167
|
+
state,
|
|
168
|
+
};
|
|
169
|
+
if (this.options.clientSecret) {
|
|
170
|
+
requestPayload.client_secret = this.options.clientSecret;
|
|
171
|
+
}
|
|
172
|
+
const response = await this.fetcher(tokenEndpoint, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: {
|
|
175
|
+
'Content-Type': 'application/json',
|
|
176
|
+
Accept: 'application/json',
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify(requestPayload),
|
|
179
|
+
});
|
|
180
|
+
const payload = (await response.json());
|
|
181
|
+
if (!response.ok || payload.error) {
|
|
182
|
+
throw new Error(payload.error_description || payload.error || `GitHub OAuth token exchange failed (${response.status}).`);
|
|
183
|
+
}
|
|
184
|
+
if (!payload.access_token) {
|
|
185
|
+
throw new Error('GitHub OAuth did not return an access token.');
|
|
186
|
+
}
|
|
187
|
+
const storedTokens = {
|
|
188
|
+
accessToken: payload.access_token,
|
|
189
|
+
tokenType: payload.token_type,
|
|
190
|
+
scope: payload.scope,
|
|
191
|
+
expiresAt: payload.expires_in ? Date.now() + payload.expires_in * 1000 : undefined,
|
|
192
|
+
};
|
|
193
|
+
await this.tokenStore.setTokens(storedTokens);
|
|
194
|
+
const user = await this.fetchUserProfile(storedTokens.accessToken);
|
|
195
|
+
await this.tokenStore.setUser(user);
|
|
196
|
+
this.notify(user);
|
|
197
|
+
return user;
|
|
198
|
+
}
|
|
199
|
+
async signOut() {
|
|
200
|
+
await this.tokenStore.removeTokens();
|
|
201
|
+
await this.tokenStore.setUser(null);
|
|
202
|
+
this.notify(null);
|
|
203
|
+
}
|
|
204
|
+
async isSignedIn() {
|
|
205
|
+
const tokens = await this.tokenStore.getTokens();
|
|
206
|
+
return validateTokens(tokens);
|
|
207
|
+
}
|
|
208
|
+
async getUser() {
|
|
209
|
+
if (this.currentUser) {
|
|
210
|
+
return this.currentUser;
|
|
211
|
+
}
|
|
212
|
+
const cached = await this.tokenStore.getUser();
|
|
213
|
+
if (cached) {
|
|
214
|
+
this.currentUser = cached;
|
|
215
|
+
return cached;
|
|
216
|
+
}
|
|
217
|
+
const tokens = await this.tokenStore.getTokens();
|
|
218
|
+
if (!validateTokens(tokens)) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const user = await this.fetchUserProfile(tokens.accessToken);
|
|
223
|
+
await this.tokenStore.setUser(user);
|
|
224
|
+
this.currentUser = user;
|
|
225
|
+
return user;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async getValidAccessToken() {
|
|
232
|
+
const tokens = await this.tokenStore.getTokens();
|
|
233
|
+
if (!validateTokens(tokens)) {
|
|
234
|
+
throw new Error('No GitHub account signed in. Please sign in first.');
|
|
235
|
+
}
|
|
236
|
+
return tokens.accessToken;
|
|
237
|
+
}
|
|
238
|
+
async fetchUserProfile(accessToken) {
|
|
239
|
+
const res = await this.fetcher(`${exports.GITHUB_API_ENDPOINT}/user`, {
|
|
240
|
+
headers: {
|
|
241
|
+
Authorization: `Bearer ${accessToken}`,
|
|
242
|
+
Accept: 'application/vnd.github.v3+json',
|
|
243
|
+
'User-Agent': 'local-lite-auth',
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
throw new Error(`Failed to fetch GitHub user profile (${res.status}).`);
|
|
248
|
+
}
|
|
249
|
+
const raw = (await res.json());
|
|
250
|
+
let email = raw.email;
|
|
251
|
+
// If email is private on profile, fetch from /user/emails endpoint
|
|
252
|
+
if (!email) {
|
|
253
|
+
try {
|
|
254
|
+
const emailsRes = await this.fetcher(`${exports.GITHUB_API_ENDPOINT}/user/emails`, {
|
|
255
|
+
headers: {
|
|
256
|
+
Authorization: `Bearer ${accessToken}`,
|
|
257
|
+
Accept: 'application/vnd.github.v3+json',
|
|
258
|
+
'User-Agent': 'local-lite-auth',
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
if (emailsRes.ok) {
|
|
262
|
+
const emails = (await emailsRes.json());
|
|
263
|
+
const primary = emails.find(e => e.primary && e.verified) || emails[0];
|
|
264
|
+
if (primary)
|
|
265
|
+
email = primary.email;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Fallback to username generated placeholder if emails restricted
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
id: String(raw.id),
|
|
274
|
+
login: raw.login,
|
|
275
|
+
name: raw.name || raw.login,
|
|
276
|
+
email: email || `${raw.login}@users.noreply.github.com`,
|
|
277
|
+
avatarUrl: raw.avatar_url,
|
|
278
|
+
bio: raw.bio,
|
|
279
|
+
company: raw.company,
|
|
280
|
+
location: raw.location,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
exports.GitHubAuthClient = GitHubAuthClient;
|
|
@@ -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;
|