@rebasepro/client 0.0.1-canary.09e5ec5

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/src/auth.ts ADDED
@@ -0,0 +1,512 @@
1
+ import { RebaseApiError, Transport } from "./transport";
2
+
3
+ export interface RebaseUser {
4
+ uid: string;
5
+ email: string | null;
6
+ displayName: string | null;
7
+ photoURL: string | null;
8
+ emailVerified?: boolean;
9
+ roles?: string[];
10
+ providerId: string;
11
+ isAnonymous: boolean;
12
+ }
13
+
14
+ export interface RebaseTokens {
15
+ accessToken: string;
16
+ refreshToken: string;
17
+ accessTokenExpiresAt: number;
18
+ }
19
+
20
+ export interface RebaseSession {
21
+ accessToken: string;
22
+ refreshToken: string;
23
+ expiresAt: number;
24
+ user: RebaseUser;
25
+ }
26
+
27
+ export type AuthChangeEvent = "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | "USER_UPDATED";
28
+
29
+ export interface AuthConfig {
30
+ needsSetup: boolean;
31
+ registrationEnabled: boolean;
32
+ googleEnabled: boolean;
33
+ emailServiceEnabled: boolean;
34
+ }
35
+
36
+ export interface AuthStorage {
37
+ getItem: (key: string) => string | null;
38
+ setItem: (key: string, value: string) => void;
39
+ removeItem: (key: string) => void;
40
+ }
41
+
42
+ export function createMemoryStorage(): AuthStorage {
43
+ const store: Record<string, string> = {};
44
+ return {
45
+ getItem(key) { return store[key] ?? null; },
46
+ setItem(key, value) { store[key] = value; },
47
+ removeItem(key) { delete store[key]; }
48
+ };
49
+ }
50
+
51
+ function detectStorage(): AuthStorage {
52
+ try {
53
+ if (typeof localStorage !== "undefined") {
54
+ localStorage.setItem("__rebase_test__", "1");
55
+ localStorage.removeItem("__rebase_test__");
56
+ return localStorage;
57
+ }
58
+ } catch (e) { /* ignore */ }
59
+ return createMemoryStorage();
60
+ }
61
+
62
+ export interface CreateAuthOptions {
63
+ storage?: AuthStorage;
64
+ authPath?: string;
65
+ autoRefresh?: boolean;
66
+ persistSession?: boolean;
67
+ }
68
+
69
+ export function createAuth(transport: Transport, options?: CreateAuthOptions) {
70
+ const opts = options || {};
71
+ const storage = opts.storage || detectStorage();
72
+ const authPath = opts.authPath || "/auth";
73
+ const autoRefresh = opts.autoRefresh !== false;
74
+ const persistSession = opts.persistSession !== false;
75
+
76
+ const STORAGE_KEY = "rebase_auth";
77
+ const REFRESH_BUFFER_MS = 120000;
78
+
79
+ let currentSession: RebaseSession | null = null;
80
+ const listeners = new Set<(event: AuthChangeEvent, session: RebaseSession | null) => void>();
81
+ let refreshTimeout: ReturnType<typeof setTimeout> | null = null;
82
+
83
+ function authUrl(endpoint: string) {
84
+ return transport.baseUrl + transport.apiPath + authPath + endpoint;
85
+ }
86
+
87
+ function getFetch() {
88
+ return transport.fetchFn || globalThis.fetch;
89
+ }
90
+
91
+ function throwApiError(status: number, body: { error?: { message?: string; code?: string; details?: unknown }; message?: string; code?: string; details?: unknown } | undefined, statusText: string): never {
92
+ throw new RebaseApiError(
93
+ status,
94
+ body?.error?.message || body?.message || statusText,
95
+ body?.error?.code || body?.code,
96
+ body?.error?.details || body?.details
97
+ );
98
+ }
99
+
100
+ function emit(event: AuthChangeEvent, session: RebaseSession | null) {
101
+ for (const fn of listeners) {
102
+ try { fn(event, session); } catch (e) { /* ignore */ }
103
+ }
104
+ }
105
+
106
+ function saveSession(session: RebaseSession) {
107
+ if (!persistSession) return;
108
+ try {
109
+ storage.setItem(STORAGE_KEY, JSON.stringify(session));
110
+ } catch (e) { /* ignore */ }
111
+ }
112
+
113
+ function clearStoredSession() {
114
+ try {
115
+ storage.removeItem(STORAGE_KEY);
116
+ } catch (e) { /* ignore */ }
117
+ }
118
+
119
+ function loadStoredSession(): RebaseSession | null {
120
+ try {
121
+ const raw = storage.getItem(STORAGE_KEY);
122
+ if (raw) return JSON.parse(raw) as RebaseSession;
123
+ } catch (e) { /* ignore */ }
124
+ return null;
125
+ }
126
+
127
+ function scheduleRefresh(expiresAt: number) {
128
+ if (refreshTimeout) clearTimeout(refreshTimeout);
129
+ if (!autoRefresh) return;
130
+
131
+ const delay = (expiresAt - REFRESH_BUFFER_MS) - Date.now();
132
+
133
+ if (delay <= 0) {
134
+ refreshSession().catch(() => signOut());
135
+ return;
136
+ }
137
+
138
+ refreshTimeout = setTimeout(async () => {
139
+ try {
140
+ await refreshSession();
141
+ } catch (e) {
142
+ signOut();
143
+ }
144
+ }, delay);
145
+ }
146
+
147
+ function handleAuthResponse(data: { tokens: RebaseTokens, user: RebaseUser }, event?: AuthChangeEvent): RebaseSession {
148
+ const session: RebaseSession = {
149
+ accessToken: data.tokens.accessToken,
150
+ refreshToken: data.tokens.refreshToken,
151
+ expiresAt: data.tokens.accessTokenExpiresAt,
152
+ user: data.user
153
+ };
154
+ currentSession = session;
155
+ saveSession(session);
156
+ transport.setToken(session.accessToken);
157
+ scheduleRefresh(session.expiresAt);
158
+ emit(event || "SIGNED_IN", session);
159
+ return session;
160
+ }
161
+
162
+ async function signInWithEmail(email: string, password: string) {
163
+ const fetchFn = getFetch();
164
+ const res = await fetchFn(authUrl("/login"), {
165
+ method: "POST",
166
+ headers: { "Content-Type": "application/json" },
167
+ body: JSON.stringify({ email,
168
+ password })
169
+ });
170
+ const body = await res.json().catch(() => ({}));
171
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
172
+ const session = handleAuthResponse(body, "SIGNED_IN");
173
+ return { user: session.user,
174
+ accessToken: session.accessToken,
175
+ refreshToken: session.refreshToken };
176
+ }
177
+
178
+ async function signUp(email: string, password: string, displayName?: string) {
179
+ const fetchFn = getFetch();
180
+ const payload: Record<string, string> = { email,
181
+ password };
182
+ if (displayName !== undefined) payload.displayName = displayName;
183
+ const res = await fetchFn(authUrl("/register"), {
184
+ method: "POST",
185
+ headers: { "Content-Type": "application/json" },
186
+ body: JSON.stringify(payload)
187
+ });
188
+ const body = await res.json().catch(() => ({}));
189
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
190
+ const session = handleAuthResponse(body, "SIGNED_IN");
191
+ return { user: session.user,
192
+ accessToken: session.accessToken,
193
+ refreshToken: session.refreshToken };
194
+ }
195
+
196
+ async function signInWithGoogle(idToken: string) {
197
+ const fetchFn = getFetch();
198
+ const res = await fetchFn(authUrl("/google"), {
199
+ method: "POST",
200
+ headers: { "Content-Type": "application/json" },
201
+ body: JSON.stringify({ idToken })
202
+ });
203
+ const body = await res.json().catch(() => ({}));
204
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
205
+ const session = handleAuthResponse(body, "SIGNED_IN");
206
+ return { user: session.user,
207
+ accessToken: session.accessToken,
208
+ refreshToken: session.refreshToken };
209
+ }
210
+
211
+ async function signInWithLinkedin(code: string, redirectUri: string) {
212
+ const fetchFn = getFetch();
213
+ const res = await fetchFn(authUrl("/linkedin"), {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify({ code,
217
+ redirectUri })
218
+ });
219
+ const body = await res.json().catch(() => ({}));
220
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
221
+ const session = handleAuthResponse(body, "SIGNED_IN");
222
+ return { user: session.user,
223
+ accessToken: session.accessToken,
224
+ refreshToken: session.refreshToken };
225
+ }
226
+
227
+ /**
228
+ * Generic OAuth sign-in. Posts the given payload to `/auth/{providerId}`.
229
+ * Use this for any provider registered on the backend.
230
+ */
231
+ async function signInWithOAuth(providerId: string, payload: Record<string, unknown>) {
232
+ const fetchFn = getFetch();
233
+ const res = await fetchFn(authUrl(`/${providerId}`), {
234
+ method: "POST",
235
+ headers: { "Content-Type": "application/json" },
236
+ body: JSON.stringify(payload)
237
+ });
238
+ const body = await res.json().catch(() => ({}));
239
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
240
+ const session = handleAuthResponse(body, "SIGNED_IN");
241
+ return { user: session.user,
242
+ accessToken: session.accessToken,
243
+ refreshToken: session.refreshToken };
244
+ }
245
+
246
+ // Convenience wrappers for all supported OAuth providers
247
+
248
+ async function signInWithGitHub(code: string, redirectUri: string) {
249
+ return signInWithOAuth("github", { code,
250
+ redirectUri });
251
+ }
252
+
253
+ async function signInWithMicrosoft(code: string, redirectUri: string) {
254
+ return signInWithOAuth("microsoft", { code,
255
+ redirectUri });
256
+ }
257
+
258
+ async function signInWithApple(code: string, redirectUri: string, user?: { name?: { firstName?: string; lastName?: string }; email?: string }) {
259
+ return signInWithOAuth("apple", { code,
260
+ redirectUri,
261
+ user });
262
+ }
263
+
264
+ async function signInWithFacebook(code: string, redirectUri: string) {
265
+ return signInWithOAuth("facebook", { code,
266
+ redirectUri });
267
+ }
268
+
269
+ async function signInWithTwitter(code: string, redirectUri: string, codeVerifier: string) {
270
+ return signInWithOAuth("twitter", { code,
271
+ redirectUri,
272
+ codeVerifier });
273
+ }
274
+
275
+ async function signInWithDiscord(code: string, redirectUri: string) {
276
+ return signInWithOAuth("discord", { code,
277
+ redirectUri });
278
+ }
279
+
280
+ async function signInWithGitLab(code: string, redirectUri: string) {
281
+ return signInWithOAuth("gitlab", { code,
282
+ redirectUri });
283
+ }
284
+
285
+ async function signInWithBitbucket(code: string, redirectUri: string) {
286
+ return signInWithOAuth("bitbucket", { code,
287
+ redirectUri });
288
+ }
289
+
290
+ async function signInWithSlack(code: string, redirectUri: string) {
291
+ return signInWithOAuth("slack", { code,
292
+ redirectUri });
293
+ }
294
+
295
+ async function signInWithSpotify(code: string, redirectUri: string) {
296
+ return signInWithOAuth("spotify", { code,
297
+ redirectUri });
298
+ }
299
+
300
+ async function signOut() {
301
+ const fetchFn = getFetch();
302
+ try {
303
+ if (currentSession?.refreshToken) {
304
+ await fetchFn(authUrl("/logout"), {
305
+ method: "POST",
306
+ headers: { "Content-Type": "application/json" },
307
+ body: JSON.stringify({ refreshToken: currentSession.refreshToken })
308
+ });
309
+ }
310
+ } catch (e) { /* ignore */ }
311
+ currentSession = null;
312
+ clearStoredSession();
313
+ if (refreshTimeout) {
314
+ clearTimeout(refreshTimeout);
315
+ refreshTimeout = null;
316
+ }
317
+ transport.setToken(null);
318
+ emit("SIGNED_OUT", null);
319
+ }
320
+
321
+ async function refreshSession() {
322
+ if (!currentSession?.refreshToken) {
323
+ throw new Error("No active session to refresh");
324
+ }
325
+ const fetchFn = getFetch();
326
+ const res = await fetchFn(authUrl("/refresh"), {
327
+ method: "POST",
328
+ headers: { "Content-Type": "application/json" },
329
+ body: JSON.stringify({ refreshToken: currentSession.refreshToken })
330
+ });
331
+ const body = await res.json().catch(() => ({}));
332
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
333
+ const session: RebaseSession = {
334
+ accessToken: body.tokens.accessToken,
335
+ refreshToken: body.tokens.refreshToken,
336
+ expiresAt: body.tokens.accessTokenExpiresAt,
337
+ user: currentSession.user
338
+ };
339
+ currentSession = session;
340
+ saveSession(session);
341
+ transport.setToken(session.accessToken);
342
+ scheduleRefresh(session.expiresAt);
343
+ emit("TOKEN_REFRESHED", session);
344
+ return session;
345
+ }
346
+
347
+ async function getUser() {
348
+ const data = await transport.request<{ user: RebaseUser }>(authPath + "/me", { method: "GET" });
349
+ return data.user;
350
+ }
351
+
352
+ async function updateUser(updates: { displayName?: string, photoURL?: string }) {
353
+ const data = await transport.request<{ user: RebaseUser }>(authPath + "/me", {
354
+ method: "PATCH",
355
+ body: JSON.stringify(updates)
356
+ });
357
+ if (currentSession) {
358
+ currentSession = { ...currentSession,
359
+ user: data.user };
360
+ saveSession(currentSession);
361
+ emit("USER_UPDATED", currentSession);
362
+ }
363
+ return data.user;
364
+ }
365
+
366
+ async function resetPasswordForEmail(email: string) {
367
+ const fetchFn = getFetch();
368
+ const res = await fetchFn(authUrl("/forgot-password"), {
369
+ method: "POST",
370
+ headers: { "Content-Type": "application/json" },
371
+ body: JSON.stringify({ email })
372
+ });
373
+ const body = await res.json().catch(() => ({}));
374
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
375
+ return body as { success: boolean; message: string; };
376
+ }
377
+
378
+ async function resetPassword(token: string, password: string) {
379
+ const fetchFn = getFetch();
380
+ const res = await fetchFn(authUrl("/reset-password"), {
381
+ method: "POST",
382
+ headers: { "Content-Type": "application/json" },
383
+ body: JSON.stringify({ token,
384
+ password })
385
+ });
386
+ const body = await res.json().catch(() => ({}));
387
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
388
+ return body as { success: boolean; message: string; };
389
+ }
390
+
391
+ async function changePassword(oldPassword: string, newPassword: string) {
392
+ return transport.request<{ success: boolean; message: string; }>(authPath + "/change-password", {
393
+ method: "POST",
394
+ body: JSON.stringify({ oldPassword,
395
+ newPassword })
396
+ });
397
+ }
398
+
399
+ async function sendVerificationEmail() {
400
+ return transport.request<{ success: boolean; message: string; }>(authPath + "/send-verification", {
401
+ method: "POST"
402
+ });
403
+ }
404
+
405
+ async function verifyEmail(token: string) {
406
+ const fetchFn = getFetch();
407
+ const res = await fetchFn(authUrl("/verify-email?token=" + encodeURIComponent(token)), {
408
+ method: "GET",
409
+ headers: { "Content-Type": "application/json" }
410
+ });
411
+ const body = await res.json().catch(() => ({}));
412
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
413
+ return body as { success: boolean; message: string; };
414
+ }
415
+
416
+ async function getSessions() {
417
+ const data = await transport.request<{ sessions: Record<string, unknown>[] }>(authPath + "/sessions", { method: "GET" });
418
+ return data.sessions;
419
+ }
420
+
421
+ async function revokeSession(sessionId: string) {
422
+ return transport.request<{ success: boolean }>(authPath + "/sessions/" + encodeURIComponent(sessionId), {
423
+ method: "DELETE"
424
+ });
425
+ }
426
+
427
+ async function revokeAllSessions() {
428
+ const result = await transport.request<{ success: boolean }>(authPath + "/sessions", {
429
+ method: "DELETE"
430
+ });
431
+ currentSession = null;
432
+ clearStoredSession();
433
+ if (refreshTimeout) {
434
+ clearTimeout(refreshTimeout);
435
+ refreshTimeout = null;
436
+ }
437
+ transport.setToken(null);
438
+ emit("SIGNED_OUT", null);
439
+ return result;
440
+ }
441
+
442
+ async function getAuthConfig() {
443
+ const fetchFn = getFetch();
444
+ const res = await fetchFn(authUrl("/config"), {
445
+ method: "GET",
446
+ headers: { "Content-Type": "application/json" }
447
+ });
448
+ const body = await res.json().catch(() => ({}));
449
+ if (!res.ok) throwApiError(res.status, body, res.statusText);
450
+ return body as AuthConfig;
451
+ }
452
+
453
+ function getSession() {
454
+ return currentSession;
455
+ }
456
+
457
+ function onAuthStateChange(callback: (event: AuthChangeEvent, session: RebaseSession | null) => void) {
458
+ listeners.add(callback);
459
+ return () => listeners.delete(callback);
460
+ }
461
+
462
+ if (persistSession) {
463
+ const stored = loadStoredSession();
464
+ if (stored && stored.accessToken && stored.refreshToken) {
465
+ if (stored.expiresAt > Date.now()) {
466
+ currentSession = stored;
467
+ transport.setToken(stored.accessToken);
468
+ scheduleRefresh(stored.expiresAt);
469
+ } else if (stored.refreshToken) {
470
+ currentSession = stored;
471
+ refreshSession().catch(() => {
472
+ currentSession = null;
473
+ clearStoredSession();
474
+ transport.setToken(null);
475
+ });
476
+ }
477
+ }
478
+ }
479
+
480
+ return {
481
+ signInWithEmail,
482
+ signUp,
483
+ signInWithGoogle,
484
+ signInWithLinkedin,
485
+ signInWithOAuth,
486
+ signInWithGitHub,
487
+ signInWithMicrosoft,
488
+ signInWithApple,
489
+ signInWithFacebook,
490
+ signInWithTwitter,
491
+ signInWithDiscord,
492
+ signInWithGitLab,
493
+ signInWithBitbucket,
494
+ signInWithSlack,
495
+ signInWithSpotify,
496
+ signOut,
497
+ refreshSession,
498
+ getUser,
499
+ updateUser,
500
+ resetPasswordForEmail,
501
+ resetPassword,
502
+ changePassword,
503
+ sendVerificationEmail,
504
+ verifyEmail,
505
+ getSessions,
506
+ revokeSession,
507
+ revokeAllSessions,
508
+ getAuthConfig,
509
+ getSession,
510
+ onAuthStateChange
511
+ };
512
+ }