@rebasepro/client 0.17.3-canary.gdd23447 → 0.18.0
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 +4 -0
- package/dist/auth.d.ts +80 -0
- package/dist/functions.d.ts +6 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.es.js +471 -94
- package/dist/index.es.js.map +1 -1
- package/dist/offline-connectivity.d.ts +12 -1
- package/dist/offline.d.ts +23 -1
- package/dist/query-contract.types.d.ts +30 -0
- package/dist/realtime-channel.d.ts +29 -1
- package/dist/sdk_query_builder.d.ts +21 -2
- package/dist/transport.d.ts +24 -0
- package/package.json +28 -15
- package/src/admin.ts +0 -90
- package/src/anonymous-client-guard.test.ts +0 -190
- package/src/api-keys.ts +0 -87
- package/src/auth-listener-errors.test.ts +0 -57
- package/src/auth-refresh-overflow.test.ts +0 -89
- package/src/auth.ts +0 -982
- package/src/backups.ts +0 -40
- package/src/client-close.test.ts +0 -80
- package/src/collection-listen-meta.test.ts +0 -105
- package/src/collection-observe.test.ts +0 -138
- package/src/collection.test.ts +0 -293
- package/src/collection.ts +0 -525
- package/src/cron.test.ts +0 -164
- package/src/cron.ts +0 -62
- package/src/data-proxy.test.ts +0 -183
- package/src/errors.ts +0 -9
- package/src/functions.ts +0 -82
- package/src/index.ts +0 -639
- package/src/like-pattern-redos.test.ts +0 -61
- package/src/offline-codec.ts +0 -79
- package/src/offline-connectivity.test.ts +0 -191
- package/src/offline-connectivity.ts +0 -255
- package/src/offline-idb-store.test.ts +0 -340
- package/src/offline-integration.test.ts +0 -180
- package/src/offline-query.test.ts +0 -431
- package/src/offline-query.ts +0 -529
- package/src/offline-store.ts +0 -357
- package/src/offline-sync-engine.test.ts +0 -857
- package/src/offline.test.ts +0 -897
- package/src/offline.ts +0 -1928
- package/src/query-contract.types.ts +0 -206
- package/src/query_builder.ts +0 -1
- package/src/realtime-channel.test.ts +0 -542
- package/src/realtime-channel.ts +0 -539
- package/src/realtime-concurrent-subscribe.test.ts +0 -102
- package/src/realtime-error-surfacing.test.ts +0 -105
- package/src/realtime-optout.test.ts +0 -279
- package/src/realtime-row-identity.test.ts +0 -254
- package/src/realtime-subscription-key.test.ts +0 -92
- package/src/reviver.ts +0 -39
- package/src/sdk_query_builder.ts +0 -206
- package/src/storage-key-encoding.test.ts +0 -65
- package/src/storage-registry.ts +0 -102
- package/src/storage.ts +0 -253
- package/src/transport-baseurl.test.ts +0 -101
- package/src/transport.ts +0 -505
- package/src/vector-search-listen.test.ts +0 -42
- package/src/vector-search-query.test.ts +0 -55
- package/src/websocket-url.test.ts +0 -97
- package/src/websocket.ts +0 -1837
package/src/auth.ts
DELETED
|
@@ -1,982 +0,0 @@
|
|
|
1
|
-
import { RebaseApiError, Transport } from "./transport";
|
|
2
|
-
import type { AuthAdapterCapabilities, AuthChangeEvent, RebaseSession, AuthTokens, DeviceSession, User } from "@rebasepro/types";
|
|
3
|
-
|
|
4
|
-
// Re-export canonical types so `import { RebaseSession } from "@rebasepro/client"` keeps working
|
|
5
|
-
export type { RebaseSession, AuthTokens, AuthChangeEvent, DeviceSession } from "@rebasepro/types";
|
|
6
|
-
|
|
7
|
-
/** Minimal, non-sensitive user profile returned by {@link findUserByEmail}. */
|
|
8
|
-
export interface PublicUserProfile {
|
|
9
|
-
uid: string;
|
|
10
|
-
displayName: string | null;
|
|
11
|
-
photoURL: string | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Map a raw user object from an auth response (`/login`, `/refresh`, `/me`) to a `User`. */
|
|
15
|
-
function mapRawUser(raw: Record<string, unknown>): User {
|
|
16
|
-
return {
|
|
17
|
-
uid: raw.uid as string,
|
|
18
|
-
email: (raw.email as string | null) ?? null,
|
|
19
|
-
displayName: (raw.displayName as string | null) ?? null,
|
|
20
|
-
photoURL: (raw.photoURL as string | null) ?? null,
|
|
21
|
-
providerId: (raw.providerId as string | undefined) ?? "password",
|
|
22
|
-
isAnonymous: (raw.isAnonymous as boolean | undefined) ?? false,
|
|
23
|
-
emailVerified: raw.emailVerified as boolean | undefined,
|
|
24
|
-
roles: raw.roles as string[] | undefined,
|
|
25
|
-
metadata: raw.metadata as Record<string, unknown> | undefined,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Placeholder user, used only as a last resort when none can be resolved. */
|
|
30
|
-
const EMPTY_USER: User = { uid: "", email: null, displayName: null, photoURL: null, providerId: "password", isAnonymous: false };
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* What `GET /auth/config` answers: the backend's auth capability document.
|
|
35
|
-
*
|
|
36
|
-
* Declared once, in `@rebasepro/types`, and re-exported here under the name the
|
|
37
|
-
* endpoint uses. The SDK used to carry its own near-copy of it, which listed an
|
|
38
|
-
* `emailServiceEnabled` flag no backend has ever sent and omitted half of what
|
|
39
|
-
* every backend does send.
|
|
40
|
-
*/
|
|
41
|
-
export type AuthConfig = AuthAdapterCapabilities;
|
|
42
|
-
|
|
43
|
-
export interface AuthStorage {
|
|
44
|
-
getItem: (key: string) => string | null;
|
|
45
|
-
setItem: (key: string, value: string) => void;
|
|
46
|
-
removeItem: (key: string) => void;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function createMemoryStorage(): AuthStorage {
|
|
50
|
-
const store: Record<string, string> = {};
|
|
51
|
-
return {
|
|
52
|
-
getItem(key) { return store[key] ?? null; },
|
|
53
|
-
setItem(key, value) { store[key] = value; },
|
|
54
|
-
removeItem(key) { delete store[key]; }
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function detectStorage(): AuthStorage {
|
|
59
|
-
try {
|
|
60
|
-
if (typeof localStorage !== "undefined") {
|
|
61
|
-
localStorage.setItem("__rebase_test__", "1");
|
|
62
|
-
localStorage.removeItem("__rebase_test__");
|
|
63
|
-
return localStorage;
|
|
64
|
-
}
|
|
65
|
-
} catch (e) { /* ignore */ }
|
|
66
|
-
return createMemoryStorage();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface CreateAuthOptions {
|
|
70
|
-
storage?: AuthStorage;
|
|
71
|
-
authPath?: string;
|
|
72
|
-
autoRefresh?: boolean;
|
|
73
|
-
persistSession?: boolean;
|
|
74
|
-
/**
|
|
75
|
-
* Authentication flow mode.
|
|
76
|
-
* - 'json' (default): Tokens are sent/received in JSON bodies. Refresh token is stored in local storage.
|
|
77
|
-
* - 'cookie': Refresh token is sent/received via httpOnly cookies. Access token remains in memory.
|
|
78
|
-
*/
|
|
79
|
-
authFlowMode?: "json" | "cookie";
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function createAuth(transport: Transport, options?: CreateAuthOptions) {
|
|
83
|
-
const opts = options || {};
|
|
84
|
-
const storage = opts.storage || detectStorage();
|
|
85
|
-
const authPath = opts.authPath || "/auth";
|
|
86
|
-
const autoRefresh = opts.autoRefresh !== false;
|
|
87
|
-
const persistSession = opts.persistSession !== false;
|
|
88
|
-
const authFlowMode = opts.authFlowMode || "json";
|
|
89
|
-
|
|
90
|
-
const STORAGE_KEY = "rebase_auth";
|
|
91
|
-
const REFRESH_BUFFER_MS = 120000;
|
|
92
|
-
/**
|
|
93
|
-
* The largest delay `setTimeout` can hold — 2^31 - 1 ms, about 24.8 days.
|
|
94
|
-
* Anything larger is silently clamped to 1ms by Node and every browser.
|
|
95
|
-
*/
|
|
96
|
-
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
97
|
-
// Auto-refresh resilience: retry transient failures with exponential backoff
|
|
98
|
-
// (1s, 2s, 4s, … capped) before giving up and signing out.
|
|
99
|
-
const MAX_REFRESH_RETRIES = 5;
|
|
100
|
-
const REFRESH_RETRY_BASE_MS = 1000;
|
|
101
|
-
const REFRESH_RETRY_MAX_MS = 30000;
|
|
102
|
-
|
|
103
|
-
let currentSession: RebaseSession | null = null;
|
|
104
|
-
const listeners = new Set<(event: AuthChangeEvent, session: RebaseSession | null) => void>();
|
|
105
|
-
let refreshTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
106
|
-
// De-dupe concurrent refreshes. On boot (esp. cookie mode + React StrictMode)
|
|
107
|
-
// multiple callers can trigger refresh at once; without this they race — the
|
|
108
|
-
// server rotates the refresh token twice and the browser can end up with a
|
|
109
|
-
// cookie the DB no longer matches. A single in-flight promise is shared.
|
|
110
|
-
let inFlightRefresh: Promise<RebaseSession> | null = null;
|
|
111
|
-
let resolveInitialized: (value: void | PromiseLike<void>) => void;
|
|
112
|
-
const isInitialized = new Promise<void>((resolve) => {
|
|
113
|
-
resolveInitialized = resolve;
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
function authUrl(endpoint: string) {
|
|
117
|
-
return transport.baseUrl + transport.apiPath + authPath + endpoint;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getFetch() {
|
|
121
|
-
return transport.fetchFn || globalThis.fetch;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function throwApiError(status: number, body: { error?: { message?: string; code?: string; details?: unknown }; message?: string; code?: string; details?: unknown } | undefined, statusText: string): never {
|
|
125
|
-
throw new RebaseApiError(
|
|
126
|
-
body?.error?.message || body?.message || statusText,
|
|
127
|
-
{
|
|
128
|
-
status,
|
|
129
|
-
code: body?.error?.code || body?.code,
|
|
130
|
-
details: body?.error?.details || body?.details
|
|
131
|
-
}
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function emit(event: AuthChangeEvent, session: RebaseSession | null) {
|
|
136
|
-
for (const fn of listeners) {
|
|
137
|
-
try {
|
|
138
|
-
fn(event, session);
|
|
139
|
-
} catch (e) {
|
|
140
|
-
// Isolated so one bad handler cannot stop the rest being told —
|
|
141
|
-
// but reported, because the throw came from the caller's own
|
|
142
|
-
// code and discarding it made a broken `onAuthStateChange`
|
|
143
|
-
// handler look like an event that never fired. The socket in
|
|
144
|
-
// this package already reports handler errors this way.
|
|
145
|
-
console.error("Error in auth state change listener:", e);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function saveSession(session: RebaseSession) {
|
|
151
|
-
if (!persistSession || authFlowMode === "cookie") return;
|
|
152
|
-
try {
|
|
153
|
-
storage.setItem(STORAGE_KEY, JSON.stringify(session));
|
|
154
|
-
} catch (e) { /* ignore */ }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function clearStoredSession() {
|
|
158
|
-
try {
|
|
159
|
-
storage.removeItem(STORAGE_KEY);
|
|
160
|
-
} catch (e) { /* ignore */ }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function loadStoredSession(): RebaseSession | null {
|
|
164
|
-
try {
|
|
165
|
-
const raw = storage.getItem(STORAGE_KEY);
|
|
166
|
-
if (raw) return JSON.parse(raw) as RebaseSession;
|
|
167
|
-
} catch (e) { /* ignore */ }
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* A refresh failure is only fatal if the refresh token itself is rejected
|
|
173
|
-
* (expired / invalid / forbidden). Network blips, timeouts, and 5xx (e.g. a
|
|
174
|
-
* backend restart mid-session) are transient and must NOT log the user out.
|
|
175
|
-
*/
|
|
176
|
-
function isFatalRefreshError(err: unknown): boolean {
|
|
177
|
-
if (!(err instanceof RebaseApiError)) return false; // network/other → transient
|
|
178
|
-
// Another tab (or a retry of our own request) rotated the token we
|
|
179
|
-
// were holding. In cookie mode the jar may ALREADY contain the
|
|
180
|
-
// replacement, so this is the one 401 that is worth retrying: giving
|
|
181
|
-
// up here is precisely the bug where opening a second tab signs you
|
|
182
|
-
// out of both.
|
|
183
|
-
if (err.code === "TOKEN_ALREADY_USED") return false;
|
|
184
|
-
if (err.code === "INVALID_TOKEN" || err.code === "TOKEN_EXPIRED") return true;
|
|
185
|
-
// 401/403 are auth failures; other statuses (incl. 5xx, 0) are transient.
|
|
186
|
-
return err.status === 401 || err.status === 403;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Drop this client's session without telling the server.
|
|
191
|
-
*
|
|
192
|
-
* `signOut()` is a user action: it POSTs /logout, which revokes the whole
|
|
193
|
-
* sign-in. That is the wrong hammer for a refresh that failed. Our token
|
|
194
|
-
* may be stale precisely because a sibling tab holds a live one, and
|
|
195
|
-
* logging out on its behalf would turn one tab's bad luck into everybody
|
|
196
|
-
* being signed out — the exact failure this work exists to remove.
|
|
197
|
-
*/
|
|
198
|
-
function abandonSessionLocally() {
|
|
199
|
-
currentSession = null;
|
|
200
|
-
clearStoredSession();
|
|
201
|
-
if (refreshTimeout) {
|
|
202
|
-
clearTimeout(refreshTimeout);
|
|
203
|
-
refreshTimeout = null;
|
|
204
|
-
}
|
|
205
|
-
transport.setToken(null);
|
|
206
|
-
emit("SIGNED_OUT", null);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Recover from a 401 on an ordinary API request.
|
|
211
|
-
*
|
|
212
|
-
* Returns `true` when the caller should retry — we minted a fresh access
|
|
213
|
-
* token. When the refresh is rejected *fatally* (the refresh token itself
|
|
214
|
-
* is invalid, expired or revoked) this client can no longer act as the
|
|
215
|
-
* user at all, so we drop the session and emit `SIGNED_OUT`. UIs gate on
|
|
216
|
-
* that event, so they show their login screen instead of leaving the user
|
|
217
|
-
* staring at "Invalid or expired token" on every view.
|
|
218
|
-
*
|
|
219
|
-
* Transient failures (offline, 5xx, backend restarting) keep the session:
|
|
220
|
-
* the scheduled refresh backs off and retries, and the token is very
|
|
221
|
-
* likely still good once the backend answers again.
|
|
222
|
-
*/
|
|
223
|
-
async function handleUnauthorized(): Promise<boolean> {
|
|
224
|
-
// No session to recover: the 401 is just an anonymous caller hitting a
|
|
225
|
-
// protected route. Emitting SIGNED_OUT here would fire sign-out
|
|
226
|
-
// handlers for a user who was never signed in.
|
|
227
|
-
if (!currentSession) return false;
|
|
228
|
-
|
|
229
|
-
// Nothing to refresh *with* — the access token is dead and there is no
|
|
230
|
-
// way back. Same end state as a rejected refresh token.
|
|
231
|
-
if (authFlowMode !== "cookie" && !currentSession.refreshToken) {
|
|
232
|
-
abandonSessionLocally();
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
await refreshSession();
|
|
238
|
-
return true;
|
|
239
|
-
} catch (err) {
|
|
240
|
-
if (isFatalRefreshError(err)) {
|
|
241
|
-
abandonSessionLocally();
|
|
242
|
-
}
|
|
243
|
-
return false;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function attemptScheduledRefresh(attempt: number) {
|
|
248
|
-
try {
|
|
249
|
-
await refreshSession();
|
|
250
|
-
// On success, refreshSession() re-schedules the next refresh itself.
|
|
251
|
-
} catch (err) {
|
|
252
|
-
if (isFatalRefreshError(err)) {
|
|
253
|
-
abandonSessionLocally();
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (attempt >= MAX_REFRESH_RETRIES) {
|
|
257
|
-
abandonSessionLocally();
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
// Transient failure — back off and retry rather than dropping the session.
|
|
261
|
-
const backoff = Math.min(REFRESH_RETRY_BASE_MS * 2 ** attempt, REFRESH_RETRY_MAX_MS);
|
|
262
|
-
refreshTimeout = setTimeout(() => { void attemptScheduledRefresh(attempt + 1); }, backoff);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function scheduleRefresh(expiresAt: number) {
|
|
267
|
-
if (refreshTimeout) clearTimeout(refreshTimeout);
|
|
268
|
-
if (!autoRefresh) return;
|
|
269
|
-
|
|
270
|
-
const delay = (expiresAt - REFRESH_BUFFER_MS) - Date.now();
|
|
271
|
-
|
|
272
|
-
if (delay <= 0) {
|
|
273
|
-
void attemptScheduledRefresh(0);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// `setTimeout` holds its delay in a 32-bit signed integer. Past
|
|
278
|
-
// ~24.8 days it does not wait — it clamps to 1ms and fires at once. The
|
|
279
|
-
// refresh would then land, receive a token expiring just as far out,
|
|
280
|
-
// schedule again and overflow again: a hot loop against
|
|
281
|
-
// `/auth/refresh`, one per open tab.
|
|
282
|
-
//
|
|
283
|
-
// `auth.accessExpiresIn` is configurable and defaults to "1h", so this
|
|
284
|
-
// is dormant on a default deployment and immediate on `"30d"` — an
|
|
285
|
-
// ordinary setting for an internal tool. Re-arm instead of refreshing:
|
|
286
|
-
// sleep the maximum, then work out again how long is left.
|
|
287
|
-
if (delay > MAX_TIMER_DELAY_MS) {
|
|
288
|
-
refreshTimeout = setTimeout(() => scheduleRefresh(expiresAt), MAX_TIMER_DELAY_MS);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
refreshTimeout = setTimeout(() => { void attemptScheduledRefresh(0); }, delay);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Stop the scheduled token refresh, leaving the session itself alone.
|
|
297
|
-
*
|
|
298
|
-
* This is teardown, not sign-out. `scheduleRefresh` arms an ordinary
|
|
299
|
-
* `setTimeout` up to a token lifetime away, and it is not `unref`'d — so on
|
|
300
|
-
* Node it holds the event loop open by itself. `client.close()` promised
|
|
301
|
-
* that "a script that does not call this will not exit on its own", which
|
|
302
|
-
* was true, while the converse it plainly implies was not: a signed-in
|
|
303
|
-
* client that closed its socket still hung, because this timer outlived it.
|
|
304
|
-
* Any script, cron handler or job that signs in hit that.
|
|
305
|
-
*
|
|
306
|
-
* Deliberately does NOT clear the session, touch storage, or emit
|
|
307
|
-
* SIGNED_OUT. Closing a client is not the user signing out — `signOut()`
|
|
308
|
-
* POSTs /logout and revokes the whole sign-in, which is the wrong hammer
|
|
309
|
-
* (see `abandonSessionLocally`) — and a persisted session must still be
|
|
310
|
-
* there for the next client to restore.
|
|
311
|
-
*/
|
|
312
|
-
function stopAutoRefresh() {
|
|
313
|
-
if (refreshTimeout) {
|
|
314
|
-
clearTimeout(refreshTimeout);
|
|
315
|
-
refreshTimeout = null;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function handleAuthResponse(data: { tokens: AuthTokens, user: Record<string, unknown> }, event?: AuthChangeEvent): RebaseSession {
|
|
320
|
-
const user: User = mapRawUser(data.user);
|
|
321
|
-
const session: RebaseSession = {
|
|
322
|
-
accessToken: data.tokens.accessToken,
|
|
323
|
-
refreshToken: data.tokens.refreshToken || (currentSession?.refreshToken) || "",
|
|
324
|
-
expiresAt: data.tokens.accessTokenExpiresAt,
|
|
325
|
-
user
|
|
326
|
-
};
|
|
327
|
-
currentSession = session;
|
|
328
|
-
saveSession(session);
|
|
329
|
-
transport.setToken(session.accessToken);
|
|
330
|
-
scheduleRefresh(session.expiresAt);
|
|
331
|
-
emit(event || "SIGNED_IN", session);
|
|
332
|
-
return session;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async function signInWithEmail(email: string, password: string) {
|
|
336
|
-
const fetchFn = getFetch();
|
|
337
|
-
const res = await fetchFn(authUrl("/login"), {
|
|
338
|
-
method: "POST",
|
|
339
|
-
headers: { "Content-Type": "application/json" },
|
|
340
|
-
body: JSON.stringify({ email,
|
|
341
|
-
password }),
|
|
342
|
-
credentials: authFlowMode === "cookie" ? "include" : undefined
|
|
343
|
-
} as RequestInit);
|
|
344
|
-
const body = await res.json().catch(() => ({}));
|
|
345
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
346
|
-
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
347
|
-
return { user: session.user,
|
|
348
|
-
accessToken: session.accessToken,
|
|
349
|
-
refreshToken: session.refreshToken };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
async function signUp(email: string, password: string, displayName?: string) {
|
|
353
|
-
const fetchFn = getFetch();
|
|
354
|
-
const payload: Record<string, string> = { email,
|
|
355
|
-
password };
|
|
356
|
-
if (displayName !== undefined) payload.displayName = displayName;
|
|
357
|
-
const res = await fetchFn(authUrl("/register"), {
|
|
358
|
-
method: "POST",
|
|
359
|
-
headers: { "Content-Type": "application/json" },
|
|
360
|
-
body: JSON.stringify(payload),
|
|
361
|
-
credentials: authFlowMode === "cookie" ? "include" : undefined
|
|
362
|
-
} as RequestInit);
|
|
363
|
-
const body = await res.json().catch(() => ({}));
|
|
364
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
365
|
-
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
366
|
-
return { user: session.user,
|
|
367
|
-
accessToken: session.accessToken,
|
|
368
|
-
refreshToken: session.refreshToken };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Sign in with Google.
|
|
373
|
-
*
|
|
374
|
-
* Supports three invocation styles:
|
|
375
|
-
* - `signInWithGoogle({ idToken })` — ID-token flow (One Tap / Sign In button)
|
|
376
|
-
* - `signInWithGoogle({ accessToken })` — Access-token flow (popup)
|
|
377
|
-
* - `signInWithGoogle({ code, redirectUri })` — Authorization code flow (most secure)
|
|
378
|
-
*/
|
|
379
|
-
async function signInWithGoogle(
|
|
380
|
-
payload: { idToken: string } | { accessToken: string } | { code: string; redirectUri: string }
|
|
381
|
-
) {
|
|
382
|
-
const fetchFn = getFetch();
|
|
383
|
-
const res = await fetchFn(authUrl("/google"), {
|
|
384
|
-
method: "POST",
|
|
385
|
-
headers: { "Content-Type": "application/json" },
|
|
386
|
-
body: JSON.stringify(payload),
|
|
387
|
-
credentials: authFlowMode === "cookie" ? "include" : undefined
|
|
388
|
-
} as RequestInit);
|
|
389
|
-
const responseBody = await res.json().catch(() => ({}));
|
|
390
|
-
if (!res.ok) throwApiError(res.status, responseBody, res.statusText);
|
|
391
|
-
const session = handleAuthResponse(responseBody, "SIGNED_IN");
|
|
392
|
-
return { user: session.user,
|
|
393
|
-
accessToken: session.accessToken,
|
|
394
|
-
refreshToken: session.refreshToken };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function signInWithLinkedin(code: string, redirectUri: string) {
|
|
398
|
-
const fetchFn = getFetch();
|
|
399
|
-
const res = await fetchFn(authUrl("/linkedin"), {
|
|
400
|
-
method: "POST",
|
|
401
|
-
headers: { "Content-Type": "application/json" },
|
|
402
|
-
body: JSON.stringify({ code,
|
|
403
|
-
redirectUri }),
|
|
404
|
-
credentials: authFlowMode === "cookie" ? "include" : undefined
|
|
405
|
-
} as RequestInit);
|
|
406
|
-
const body = await res.json().catch(() => ({}));
|
|
407
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
408
|
-
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
409
|
-
return { user: session.user,
|
|
410
|
-
accessToken: session.accessToken,
|
|
411
|
-
refreshToken: session.refreshToken };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Generic OAuth sign-in. Posts the given payload to `/auth/{providerId}`.
|
|
416
|
-
* Use this for any provider registered on the backend.
|
|
417
|
-
*/
|
|
418
|
-
async function signInWithOAuth(providerId: string, payload: Record<string, unknown>) {
|
|
419
|
-
const fetchFn = getFetch();
|
|
420
|
-
const res = await fetchFn(authUrl(`/${providerId}`), {
|
|
421
|
-
method: "POST",
|
|
422
|
-
headers: { "Content-Type": "application/json" },
|
|
423
|
-
body: JSON.stringify(payload),
|
|
424
|
-
credentials: authFlowMode === "cookie" ? "include" : undefined
|
|
425
|
-
} as RequestInit);
|
|
426
|
-
const body = await res.json().catch(() => ({}));
|
|
427
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
428
|
-
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
429
|
-
return { user: session.user,
|
|
430
|
-
accessToken: session.accessToken,
|
|
431
|
-
refreshToken: session.refreshToken };
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Convenience wrappers for all supported OAuth providers
|
|
435
|
-
|
|
436
|
-
async function signInWithGitHub(code: string, redirectUri: string) {
|
|
437
|
-
return signInWithOAuth("github", { code,
|
|
438
|
-
redirectUri });
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async function signInWithMicrosoft(code: string, redirectUri: string) {
|
|
442
|
-
return signInWithOAuth("microsoft", { code,
|
|
443
|
-
redirectUri });
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
async function signInWithApple(code: string, redirectUri: string, user?: { name?: { firstName?: string; lastName?: string }; email?: string }) {
|
|
447
|
-
return signInWithOAuth("apple", { code,
|
|
448
|
-
redirectUri,
|
|
449
|
-
user });
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async function signInWithFacebook(code: string, redirectUri: string) {
|
|
453
|
-
return signInWithOAuth("facebook", { code,
|
|
454
|
-
redirectUri });
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
async function signInWithTwitter(code: string, redirectUri: string, codeVerifier: string) {
|
|
458
|
-
return signInWithOAuth("twitter", { code,
|
|
459
|
-
redirectUri,
|
|
460
|
-
codeVerifier });
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
async function signInWithDiscord(code: string, redirectUri: string) {
|
|
464
|
-
return signInWithOAuth("discord", { code,
|
|
465
|
-
redirectUri });
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
async function signInWithGitLab(code: string, redirectUri: string) {
|
|
469
|
-
return signInWithOAuth("gitlab", { code,
|
|
470
|
-
redirectUri });
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
async function signInWithBitbucket(code: string, redirectUri: string) {
|
|
474
|
-
return signInWithOAuth("bitbucket", { code,
|
|
475
|
-
redirectUri });
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
async function signInWithSlack(code: string, redirectUri: string) {
|
|
479
|
-
return signInWithOAuth("slack", { code,
|
|
480
|
-
redirectUri });
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
async function signInWithSpotify(code: string, redirectUri: string) {
|
|
484
|
-
return signInWithOAuth("spotify", { code,
|
|
485
|
-
redirectUri });
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
async function signOut() {
|
|
489
|
-
const fetchFn = getFetch();
|
|
490
|
-
try {
|
|
491
|
-
if (authFlowMode === "cookie" || currentSession?.refreshToken) {
|
|
492
|
-
await fetchFn(authUrl("/logout"), {
|
|
493
|
-
method: "POST",
|
|
494
|
-
headers: { "Content-Type": "application/json" },
|
|
495
|
-
body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
|
|
496
|
-
credentials: authFlowMode === "cookie" ? "include" : undefined
|
|
497
|
-
} as RequestInit);
|
|
498
|
-
}
|
|
499
|
-
} catch (e) { /* ignore */ }
|
|
500
|
-
currentSession = null;
|
|
501
|
-
clearStoredSession();
|
|
502
|
-
if (refreshTimeout) {
|
|
503
|
-
clearTimeout(refreshTimeout);
|
|
504
|
-
refreshTimeout = null;
|
|
505
|
-
}
|
|
506
|
-
transport.setToken(null);
|
|
507
|
-
emit("SIGNED_OUT", null);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Serialise refreshes across TABS, not just within one.
|
|
512
|
-
*
|
|
513
|
-
* The in-flight promise below covers callers inside a single JavaScript
|
|
514
|
-
* context. It does nothing about the far more common case: two tabs of the
|
|
515
|
-
* same app booting together, each firing its own /refresh with the same
|
|
516
|
-
* cookie. The server tolerates that now (superseded tokens stay usable for
|
|
517
|
-
* a grace window), but tolerating a stampede is not the same as avoiding
|
|
518
|
-
* one, and every extra rotation is another chance to end up holding a
|
|
519
|
-
* token whose response never arrived.
|
|
520
|
-
*
|
|
521
|
-
* Web Locks are best-effort on purpose. supabase-js shipped this and then
|
|
522
|
-
* spent a year fielding deadlock reports — a lock held by a crashed or
|
|
523
|
-
* frozen tab must never be able to wedge sign-in — so a lock we cannot
|
|
524
|
-
* take within the timeout is simply not taken, and the refresh proceeds
|
|
525
|
-
* unserialised, exactly as it did before.
|
|
526
|
-
*/
|
|
527
|
-
const REFRESH_LOCK_NAME = "rebase-auth-refresh";
|
|
528
|
-
const REFRESH_LOCK_TIMEOUT_MS = 5000;
|
|
529
|
-
|
|
530
|
-
async function withRefreshLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
531
|
-
const locks = (globalThis as { navigator?: { locks?: LockManager } }).navigator?.locks;
|
|
532
|
-
if (!locks?.request) return fn();
|
|
533
|
-
|
|
534
|
-
const controller = new AbortController();
|
|
535
|
-
const giveUp = setTimeout(() => controller.abort(), REFRESH_LOCK_TIMEOUT_MS);
|
|
536
|
-
try {
|
|
537
|
-
return await locks.request(
|
|
538
|
-
REFRESH_LOCK_NAME,
|
|
539
|
-
{ signal: controller.signal },
|
|
540
|
-
async () => fn()
|
|
541
|
-
) as T;
|
|
542
|
-
} catch (e) {
|
|
543
|
-
// AbortError means only that we waited long enough for the lock.
|
|
544
|
-
// Anything the callback itself threw has to keep propagating.
|
|
545
|
-
if ((e as { name?: string })?.name !== "AbortError") throw e;
|
|
546
|
-
return fn();
|
|
547
|
-
} finally {
|
|
548
|
-
clearTimeout(giveUp);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function refreshSession(): Promise<RebaseSession> {
|
|
553
|
-
// Share a single in-flight refresh across concurrent callers.
|
|
554
|
-
if (inFlightRefresh) return inFlightRefresh;
|
|
555
|
-
inFlightRefresh = withRefreshLock(() => doRefreshSession()).finally(() => {
|
|
556
|
-
inFlightRefresh = null;
|
|
557
|
-
});
|
|
558
|
-
return inFlightRefresh;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
async function doRefreshSession(): Promise<RebaseSession> {
|
|
562
|
-
if (authFlowMode !== "cookie" && !currentSession?.refreshToken) {
|
|
563
|
-
throw new Error("No active session to refresh");
|
|
564
|
-
}
|
|
565
|
-
const fetchFn = getFetch();
|
|
566
|
-
const res = await fetchFn(authUrl("/refresh"), {
|
|
567
|
-
method: "POST",
|
|
568
|
-
headers: { "Content-Type": "application/json" },
|
|
569
|
-
body: JSON.stringify({ refreshToken: currentSession?.refreshToken }),
|
|
570
|
-
credentials: authFlowMode === "cookie" ? "include" : undefined
|
|
571
|
-
} as RequestInit);
|
|
572
|
-
const body = await res.json().catch(() => ({}));
|
|
573
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
574
|
-
|
|
575
|
-
const accessToken = body.tokens.accessToken;
|
|
576
|
-
transport.setToken(accessToken);
|
|
577
|
-
|
|
578
|
-
// Resolve the user, in order of preference:
|
|
579
|
-
// 1. the user returned by /refresh (modern backends include it),
|
|
580
|
-
// 2. the user already in memory,
|
|
581
|
-
// 3. a fetch of /me — required to restore a session from an httpOnly
|
|
582
|
-
// cookie alone (cold start in cookie mode), where there is no
|
|
583
|
-
// in-memory user and the backend didn't echo one.
|
|
584
|
-
let user = currentSession?.user;
|
|
585
|
-
if (body.user && typeof body.user.uid === "string") {
|
|
586
|
-
user = mapRawUser(body.user as Record<string, unknown>);
|
|
587
|
-
} else if (!user || !user.uid) {
|
|
588
|
-
try {
|
|
589
|
-
user = await getUser();
|
|
590
|
-
} catch { /* fall through to the empty stub below */ }
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const session: RebaseSession = {
|
|
594
|
-
accessToken,
|
|
595
|
-
refreshToken: body.tokens.refreshToken || currentSession?.refreshToken || "",
|
|
596
|
-
expiresAt: body.tokens.accessTokenExpiresAt,
|
|
597
|
-
user: user ?? EMPTY_USER
|
|
598
|
-
};
|
|
599
|
-
currentSession = session;
|
|
600
|
-
saveSession(session);
|
|
601
|
-
transport.setToken(session.accessToken);
|
|
602
|
-
scheduleRefresh(session.expiresAt);
|
|
603
|
-
emit("TOKEN_REFRESHED", session);
|
|
604
|
-
return session;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
async function getUser() {
|
|
608
|
-
const data = await transport.request<{ user: User }>(authPath + "/me", { method: "GET" });
|
|
609
|
-
return data.user;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Resolve an email to a minimal public profile (`uid`, `displayName`,
|
|
614
|
-
* `photoURL`) for invite-by-email flows. Returns `null` when no account
|
|
615
|
-
* matches. Requires the backend to opt in via `auth.allowUserLookup`;
|
|
616
|
-
* otherwise the endpoint is absent and this rejects.
|
|
617
|
-
*/
|
|
618
|
-
async function findUserByEmail(email: string): Promise<PublicUserProfile | null> {
|
|
619
|
-
const data = await transport.request<{ user: PublicUserProfile | null }>(authPath + "/find-user", {
|
|
620
|
-
method: "POST",
|
|
621
|
-
body: JSON.stringify({ email })
|
|
622
|
-
});
|
|
623
|
-
return data.user;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
async function updateUser(updates: { displayName?: string, photoURL?: string }) {
|
|
627
|
-
const data = await transport.request<{ user: User }>(authPath + "/me", {
|
|
628
|
-
method: "PATCH",
|
|
629
|
-
body: JSON.stringify(updates)
|
|
630
|
-
});
|
|
631
|
-
if (currentSession) {
|
|
632
|
-
currentSession = { ...currentSession,
|
|
633
|
-
user: data.user };
|
|
634
|
-
saveSession(currentSession);
|
|
635
|
-
emit("USER_UPDATED", currentSession);
|
|
636
|
-
}
|
|
637
|
-
return data.user;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
async function resetPasswordForEmail(email: string) {
|
|
641
|
-
const fetchFn = getFetch();
|
|
642
|
-
const res = await fetchFn(authUrl("/forgot-password"), {
|
|
643
|
-
method: "POST",
|
|
644
|
-
headers: { "Content-Type": "application/json" },
|
|
645
|
-
body: JSON.stringify({ email })
|
|
646
|
-
});
|
|
647
|
-
const body = await res.json().catch(() => ({}));
|
|
648
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
649
|
-
return body as { success: boolean; message: string; };
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
async function resetPassword(token: string, password: string) {
|
|
653
|
-
const fetchFn = getFetch();
|
|
654
|
-
const res = await fetchFn(authUrl("/reset-password"), {
|
|
655
|
-
method: "POST",
|
|
656
|
-
headers: { "Content-Type": "application/json" },
|
|
657
|
-
body: JSON.stringify({ token,
|
|
658
|
-
password })
|
|
659
|
-
});
|
|
660
|
-
const body = await res.json().catch(() => ({}));
|
|
661
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
662
|
-
return body as { success: boolean; message: string; };
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
async function changePassword(oldPassword: string, newPassword: string) {
|
|
666
|
-
return transport.request<{ success: boolean; message: string; }>(authPath + "/change-password", {
|
|
667
|
-
method: "POST",
|
|
668
|
-
body: JSON.stringify({ oldPassword,
|
|
669
|
-
newPassword })
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Link an OAuth provider to the **currently signed-in** account.
|
|
675
|
-
*
|
|
676
|
-
* Use this when `signIn*` failed with `EMAIL_NOT_VERIFIED` — an account
|
|
677
|
-
* with that email already exists under a different sign-in method — or to
|
|
678
|
-
* attach a provider whose email differs from the account's.
|
|
679
|
-
*
|
|
680
|
-
* The payload is the same one the provider's sign-in method takes, e.g.
|
|
681
|
-
* `linkProvider("google", { idToken })`.
|
|
682
|
-
*
|
|
683
|
-
* Unlike sign-in, this does not require the provider to have verified the
|
|
684
|
-
* email, and the emails need not match: the active session already proves
|
|
685
|
-
* account ownership.
|
|
686
|
-
*
|
|
687
|
-
* Throws `IDENTITY_ALREADY_LINKED` (409) if that provider identity is
|
|
688
|
-
* attached to a different user. Succeeds idempotently (`alreadyLinked:
|
|
689
|
-
* true`) if it is already attached to the current one.
|
|
690
|
-
*/
|
|
691
|
-
async function linkProvider(
|
|
692
|
-
providerId: string,
|
|
693
|
-
payload: Record<string, unknown>
|
|
694
|
-
) {
|
|
695
|
-
return transport.request<{ success: boolean; provider: string; alreadyLinked: boolean; }>(
|
|
696
|
-
authPath + "/link/" + providerId,
|
|
697
|
-
{
|
|
698
|
-
method: "POST",
|
|
699
|
-
body: JSON.stringify(payload)
|
|
700
|
-
}
|
|
701
|
-
);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
async function sendVerificationEmail() {
|
|
705
|
-
return transport.request<{ success: boolean; message: string; }>(authPath + "/send-verification", {
|
|
706
|
-
method: "POST"
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
async function verifyEmail(token: string) {
|
|
711
|
-
const fetchFn = getFetch();
|
|
712
|
-
const res = await fetchFn(authUrl("/verify-email?token=" + encodeURIComponent(token)), {
|
|
713
|
-
method: "GET",
|
|
714
|
-
headers: { "Content-Type": "application/json" }
|
|
715
|
-
});
|
|
716
|
-
const body = await res.json().catch(() => ({}));
|
|
717
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
718
|
-
return body as { success: boolean; message: string; };
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
async function sendMagicLink(email: string) {
|
|
722
|
-
const fetchFn = getFetch();
|
|
723
|
-
const res = await fetchFn(authUrl("/magic-link"), {
|
|
724
|
-
method: "POST",
|
|
725
|
-
headers: { "Content-Type": "application/json" },
|
|
726
|
-
body: JSON.stringify({ email })
|
|
727
|
-
});
|
|
728
|
-
const body = await res.json().catch(() => ({}));
|
|
729
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
730
|
-
return body as { success: boolean; message: string; };
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
async function verifyMagicLink(token: string) {
|
|
734
|
-
const fetchFn = getFetch();
|
|
735
|
-
const res = await fetchFn(authUrl("/magic-link/verify"), {
|
|
736
|
-
method: "POST",
|
|
737
|
-
headers: { "Content-Type": "application/json" },
|
|
738
|
-
body: JSON.stringify({ token }),
|
|
739
|
-
credentials: authFlowMode === "cookie" ? "include" : undefined
|
|
740
|
-
} as RequestInit);
|
|
741
|
-
const body = await res.json().catch(() => ({}));
|
|
742
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
743
|
-
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
744
|
-
return { user: session.user,
|
|
745
|
-
accessToken: session.accessToken,
|
|
746
|
-
refreshToken: session.refreshToken };
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* Ask for a six-digit sign-in code by email.
|
|
751
|
-
*
|
|
752
|
-
* Answers the same thing whether or not the address has an account — do not
|
|
753
|
-
* use the result to tell a person whether they are registered, because it
|
|
754
|
-
* does not know.
|
|
755
|
-
*/
|
|
756
|
-
async function sendEmailOtp(email: string) {
|
|
757
|
-
const fetchFn = getFetch();
|
|
758
|
-
const res = await fetchFn(authUrl("/otp"), {
|
|
759
|
-
method: "POST",
|
|
760
|
-
headers: { "Content-Type": "application/json" },
|
|
761
|
-
body: JSON.stringify({ email })
|
|
762
|
-
});
|
|
763
|
-
const body = await res.json().catch(() => ({}));
|
|
764
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
765
|
-
return body as { success: boolean; message: string; expiresInSeconds: number };
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Trade a code for a session.
|
|
770
|
-
*
|
|
771
|
-
* The address is sent with the code because the code is only valid for it:
|
|
772
|
-
* that is what keeps a six-digit guess a guess against one account rather
|
|
773
|
-
* than against every account at once.
|
|
774
|
-
*/
|
|
775
|
-
async function verifyEmailOtp(email: string, code: string) {
|
|
776
|
-
const fetchFn = getFetch();
|
|
777
|
-
const res = await fetchFn(authUrl("/otp/verify"), {
|
|
778
|
-
method: "POST",
|
|
779
|
-
headers: { "Content-Type": "application/json" },
|
|
780
|
-
body: JSON.stringify({ email, code }),
|
|
781
|
-
credentials: authFlowMode === "cookie" ? "include" : undefined
|
|
782
|
-
} as RequestInit);
|
|
783
|
-
const body = await res.json().catch(() => ({}));
|
|
784
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
785
|
-
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
786
|
-
return { user: session.user,
|
|
787
|
-
accessToken: session.accessToken,
|
|
788
|
-
refreshToken: session.refreshToken };
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
async function getSessions(): Promise<DeviceSession[]> {
|
|
792
|
-
const data = await transport.request<{ sessions: DeviceSession[] }>(authPath + "/sessions", { method: "GET" });
|
|
793
|
-
return data.sessions;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
async function revokeSession(sessionId: string) {
|
|
797
|
-
return transport.request<{ success: boolean }>(authPath + "/sessions/" + encodeURIComponent(sessionId), {
|
|
798
|
-
method: "DELETE"
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
async function revokeAllSessions() {
|
|
803
|
-
const result = await transport.request<{ success: boolean }>(authPath + "/sessions", {
|
|
804
|
-
method: "DELETE"
|
|
805
|
-
});
|
|
806
|
-
currentSession = null;
|
|
807
|
-
clearStoredSession();
|
|
808
|
-
if (refreshTimeout) {
|
|
809
|
-
clearTimeout(refreshTimeout);
|
|
810
|
-
refreshTimeout = null;
|
|
811
|
-
}
|
|
812
|
-
transport.setToken(null);
|
|
813
|
-
emit("SIGNED_OUT", null);
|
|
814
|
-
return result;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
async function getAuthConfig() {
|
|
818
|
-
const fetchFn = getFetch();
|
|
819
|
-
const res = await fetchFn(authUrl("/config"), {
|
|
820
|
-
method: "GET",
|
|
821
|
-
headers: { "Content-Type": "application/json" }
|
|
822
|
-
});
|
|
823
|
-
const body = await res.json().catch(() => ({}));
|
|
824
|
-
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
825
|
-
return body as AuthConfig;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function getSession() {
|
|
829
|
-
return currentSession;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function onAuthStateChange(callback: (event: AuthChangeEvent, session: RebaseSession | null) => void) {
|
|
833
|
-
listeners.add(callback);
|
|
834
|
-
return () => listeners.delete(callback);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (persistSession) {
|
|
838
|
-
const stored = loadStoredSession();
|
|
839
|
-
if (stored && stored.accessToken) {
|
|
840
|
-
if (stored.expiresAt > Date.now()) {
|
|
841
|
-
currentSession = stored;
|
|
842
|
-
transport.setToken(stored.accessToken);
|
|
843
|
-
scheduleRefresh(stored.expiresAt);
|
|
844
|
-
resolveInitialized!();
|
|
845
|
-
} else if (authFlowMode === "cookie" || stored.refreshToken) {
|
|
846
|
-
currentSession = stored;
|
|
847
|
-
refreshSession().then(() => {
|
|
848
|
-
resolveInitialized!();
|
|
849
|
-
}).catch(() => {
|
|
850
|
-
currentSession = null;
|
|
851
|
-
clearStoredSession();
|
|
852
|
-
transport.setToken(null);
|
|
853
|
-
resolveInitialized!();
|
|
854
|
-
});
|
|
855
|
-
} else {
|
|
856
|
-
resolveInitialized!();
|
|
857
|
-
}
|
|
858
|
-
} else if (authFlowMode === "cookie") {
|
|
859
|
-
// Silent refresh on boot to pick up httpOnly session
|
|
860
|
-
refreshSession().then(() => {
|
|
861
|
-
resolveInitialized!();
|
|
862
|
-
}).catch(() => {
|
|
863
|
-
resolveInitialized!();
|
|
864
|
-
});
|
|
865
|
-
} else {
|
|
866
|
-
resolveInitialized!();
|
|
867
|
-
}
|
|
868
|
-
} else {
|
|
869
|
-
resolveInitialized!();
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
return {
|
|
873
|
-
signInWithEmail,
|
|
874
|
-
signUp,
|
|
875
|
-
signInWithGoogle,
|
|
876
|
-
signInWithLinkedin,
|
|
877
|
-
signInWithOAuth,
|
|
878
|
-
signInWithGitHub,
|
|
879
|
-
signInWithMicrosoft,
|
|
880
|
-
signInWithApple,
|
|
881
|
-
signInWithFacebook,
|
|
882
|
-
signInWithTwitter,
|
|
883
|
-
signInWithDiscord,
|
|
884
|
-
signInWithGitLab,
|
|
885
|
-
signInWithBitbucket,
|
|
886
|
-
signInWithSlack,
|
|
887
|
-
signInWithSpotify,
|
|
888
|
-
signOut,
|
|
889
|
-
stopAutoRefresh,
|
|
890
|
-
refreshSession,
|
|
891
|
-
handleUnauthorized,
|
|
892
|
-
getUser,
|
|
893
|
-
findUserByEmail,
|
|
894
|
-
updateUser,
|
|
895
|
-
resetPasswordForEmail,
|
|
896
|
-
resetPassword,
|
|
897
|
-
changePassword,
|
|
898
|
-
linkProvider,
|
|
899
|
-
sendVerificationEmail,
|
|
900
|
-
verifyEmail,
|
|
901
|
-
sendMagicLink,
|
|
902
|
-
verifyMagicLink,
|
|
903
|
-
sendEmailOtp,
|
|
904
|
-
verifyEmailOtp,
|
|
905
|
-
getSessions,
|
|
906
|
-
revokeSession,
|
|
907
|
-
revokeAllSessions,
|
|
908
|
-
getAuthConfig,
|
|
909
|
-
getSession,
|
|
910
|
-
onAuthStateChange,
|
|
911
|
-
// A client that neither persists sessions nor uses cookie auth has
|
|
912
|
-
// nowhere to restore one from, so "no session in memory" is the final
|
|
913
|
-
// answer rather than a reason to ask the server. See the docblock on
|
|
914
|
-
// `AuthClient.canRestoreSession`.
|
|
915
|
-
canRestoreSession: () => persistSession || authFlowMode === "cookie",
|
|
916
|
-
isInitialized: () => isInitialized
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
export interface CookieStorageOptions {
|
|
921
|
-
path?: string;
|
|
922
|
-
domain?: string;
|
|
923
|
-
secure?: boolean;
|
|
924
|
-
sameSite?: "Lax" | "Strict" | "None";
|
|
925
|
-
maxAge?: number;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
export function createCookieStorage(options: CookieStorageOptions = {}): AuthStorage {
|
|
929
|
-
const defaultOptions = {
|
|
930
|
-
path: "/",
|
|
931
|
-
sameSite: "Lax" as const,
|
|
932
|
-
...options
|
|
933
|
-
};
|
|
934
|
-
|
|
935
|
-
return {
|
|
936
|
-
getItem(key: string): string | null {
|
|
937
|
-
if (typeof document === "undefined") return null;
|
|
938
|
-
const nameEQ = encodeURIComponent(key) + "=";
|
|
939
|
-
const ca = document.cookie.split(";");
|
|
940
|
-
for (let i = 0; i < ca.length; i++) {
|
|
941
|
-
let c = ca[i];
|
|
942
|
-
while (c.charAt(0) === " ") c = c.substring(1, c.length);
|
|
943
|
-
if (c.indexOf(nameEQ) === 0) {
|
|
944
|
-
return decodeURIComponent(c.substring(nameEQ.length, c.length));
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
return null;
|
|
948
|
-
},
|
|
949
|
-
setItem(key: string, value: string): void {
|
|
950
|
-
if (typeof document === "undefined") return;
|
|
951
|
-
let cookieStr = `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
952
|
-
|
|
953
|
-
if (defaultOptions.path) {
|
|
954
|
-
cookieStr += `; path=${defaultOptions.path}`;
|
|
955
|
-
}
|
|
956
|
-
if (defaultOptions.domain) {
|
|
957
|
-
cookieStr += `; domain=${defaultOptions.domain}`;
|
|
958
|
-
}
|
|
959
|
-
if (defaultOptions.maxAge !== undefined) {
|
|
960
|
-
cookieStr += `; max-age=${defaultOptions.maxAge}`;
|
|
961
|
-
} else {
|
|
962
|
-
cookieStr += `; max-age=${365 * 24 * 60 * 60}`;
|
|
963
|
-
}
|
|
964
|
-
if (defaultOptions.secure) {
|
|
965
|
-
cookieStr += "; secure";
|
|
966
|
-
}
|
|
967
|
-
if (defaultOptions.sameSite) {
|
|
968
|
-
cookieStr += `; samesite=${defaultOptions.sameSite}`;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
document.cookie = cookieStr;
|
|
972
|
-
},
|
|
973
|
-
removeItem(key: string): void {
|
|
974
|
-
if (typeof document === "undefined") return;
|
|
975
|
-
let cookieStr = `${encodeURIComponent(key)}=; path=${defaultOptions.path || "/"}; max-age=-1`;
|
|
976
|
-
if (defaultOptions.domain) {
|
|
977
|
-
cookieStr += `; domain=${defaultOptions.domain}`;
|
|
978
|
-
}
|
|
979
|
-
document.cookie = cookieStr;
|
|
980
|
-
}
|
|
981
|
-
};
|
|
982
|
-
}
|