@depup/supabase__auth-js 2.103.0-depup.0 → 2.110.8-depup.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/AGENTS.md +11 -0
- package/README.md +2 -2
- package/changes.json +1 -1
- package/dist/main/GoTrueAdminApi.d.ts +45 -8
- package/dist/main/GoTrueAdminApi.d.ts.map +1 -1
- package/dist/main/GoTrueAdminApi.js +67 -7
- package/dist/main/GoTrueAdminApi.js.map +1 -1
- package/dist/main/GoTrueClient.d.ts +239 -42
- package/dist/main/GoTrueClient.d.ts.map +1 -1
- package/dist/main/GoTrueClient.js +946 -164
- package/dist/main/GoTrueClient.js.map +1 -1
- package/dist/main/lib/constants.d.ts +8 -0
- package/dist/main/lib/constants.d.ts.map +1 -1
- package/dist/main/lib/constants.js +9 -1
- package/dist/main/lib/constants.js.map +1 -1
- package/dist/main/lib/errors.d.ts +42 -3
- package/dist/main/lib/errors.d.ts.map +1 -1
- package/dist/main/lib/errors.js +44 -13
- package/dist/main/lib/errors.js.map +1 -1
- package/dist/main/lib/fetch.d.ts +28 -8
- package/dist/main/lib/fetch.d.ts.map +1 -1
- package/dist/main/lib/fetch.js +28 -6
- package/dist/main/lib/fetch.js.map +1 -1
- package/dist/main/lib/helpers.d.ts +4 -1
- package/dist/main/lib/helpers.d.ts.map +1 -1
- package/dist/main/lib/helpers.js +15 -4
- package/dist/main/lib/helpers.js.map +1 -1
- package/dist/main/lib/locks.d.ts +29 -35
- package/dist/main/lib/locks.d.ts.map +1 -1
- package/dist/main/lib/locks.js +37 -38
- package/dist/main/lib/locks.js.map +1 -1
- package/dist/main/lib/types.d.ts +305 -64
- package/dist/main/lib/types.d.ts.map +1 -1
- package/dist/main/lib/types.js.map +1 -1
- package/dist/main/lib/version.d.ts +1 -1
- package/dist/main/lib/version.js +1 -1
- package/dist/main/lib/webauthn.d.ts +8 -0
- package/dist/main/lib/webauthn.d.ts.map +1 -1
- package/dist/main/lib/webauthn.dom.d.ts +3 -3
- package/dist/main/lib/webauthn.dom.d.ts.map +1 -1
- package/dist/main/lib/webauthn.errors.d.ts +5 -0
- package/dist/main/lib/webauthn.errors.d.ts.map +1 -1
- package/dist/main/lib/webauthn.errors.js +7 -0
- package/dist/main/lib/webauthn.errors.js.map +1 -1
- package/dist/main/lib/webauthn.js +1 -0
- package/dist/main/lib/webauthn.js.map +1 -1
- package/dist/module/GoTrueAdminApi.d.ts +45 -8
- package/dist/module/GoTrueAdminApi.d.ts.map +1 -1
- package/dist/module/GoTrueAdminApi.js +68 -8
- package/dist/module/GoTrueAdminApi.js.map +1 -1
- package/dist/module/GoTrueClient.d.ts +239 -42
- package/dist/module/GoTrueClient.d.ts.map +1 -1
- package/dist/module/GoTrueClient.js +951 -169
- package/dist/module/GoTrueClient.js.map +1 -1
- package/dist/module/lib/constants.d.ts +8 -0
- package/dist/module/lib/constants.d.ts.map +1 -1
- package/dist/module/lib/constants.js +8 -0
- package/dist/module/lib/constants.js.map +1 -1
- package/dist/module/lib/errors.d.ts +42 -3
- package/dist/module/lib/errors.d.ts.map +1 -1
- package/dist/module/lib/errors.js +41 -12
- package/dist/module/lib/errors.js.map +1 -1
- package/dist/module/lib/fetch.d.ts +28 -8
- package/dist/module/lib/fetch.d.ts.map +1 -1
- package/dist/module/lib/fetch.js +28 -6
- package/dist/module/lib/fetch.js.map +1 -1
- package/dist/module/lib/helpers.d.ts +4 -1
- package/dist/module/lib/helpers.d.ts.map +1 -1
- package/dist/module/lib/helpers.js +14 -4
- package/dist/module/lib/helpers.js.map +1 -1
- package/dist/module/lib/locks.d.ts +29 -35
- package/dist/module/lib/locks.d.ts.map +1 -1
- package/dist/module/lib/locks.js +37 -38
- package/dist/module/lib/locks.js.map +1 -1
- package/dist/module/lib/types.d.ts +305 -64
- package/dist/module/lib/types.d.ts.map +1 -1
- package/dist/module/lib/types.js.map +1 -1
- package/dist/module/lib/version.d.ts +1 -1
- package/dist/module/lib/version.js +1 -1
- package/dist/module/lib/webauthn.d.ts +8 -0
- package/dist/module/lib/webauthn.d.ts.map +1 -1
- package/dist/module/lib/webauthn.dom.d.ts +3 -3
- package/dist/module/lib/webauthn.dom.d.ts.map +1 -1
- package/dist/module/lib/webauthn.errors.d.ts +5 -0
- package/dist/module/lib/webauthn.errors.d.ts.map +1 -1
- package/dist/module/lib/webauthn.errors.js +7 -0
- package/dist/module/lib/webauthn.errors.js.map +1 -1
- package/dist/module/lib/webauthn.js +1 -1
- package/dist/module/lib/webauthn.js.map +1 -1
- package/dist/tsconfig.module.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migrations/README.md +25 -0
- package/migrations/lockless-coordination.md +155 -0
- package/package.json +25 -12
- package/src/GoTrueAdminApi.ts +99 -7
- package/src/GoTrueClient.ts +1117 -181
- package/src/lib/constants.ts +9 -0
- package/src/lib/errors.ts +76 -9
- package/src/lib/fetch.ts +66 -24
- package/src/lib/helpers.ts +19 -6
- package/src/lib/locks.ts +44 -42
- package/src/lib/types.ts +374 -82
- package/src/lib/version.ts +1 -1
- package/src/lib/webauthn.dom.ts +3 -3
- package/src/lib/webauthn.errors.ts +12 -0
- package/src/lib/webauthn.ts +1 -1
package/src/GoTrueClient.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
EXPIRY_MARGIN_MS,
|
|
7
7
|
GOTRUE_URL,
|
|
8
8
|
JWKS_TTL,
|
|
9
|
+
REFRESH_FAILURE_COOLDOWN_MS,
|
|
9
10
|
STORAGE_KEY,
|
|
10
11
|
} from './lib/constants'
|
|
11
12
|
import {
|
|
@@ -16,11 +17,13 @@ import {
|
|
|
16
17
|
AuthInvalidTokenResponseError,
|
|
17
18
|
AuthPKCECodeVerifierMissingError,
|
|
18
19
|
AuthPKCEGrantCodeExchangeError,
|
|
20
|
+
AuthRefreshDiscardedError,
|
|
19
21
|
AuthSessionMissingError,
|
|
20
22
|
AuthUnknownError,
|
|
21
23
|
isAuthApiError,
|
|
22
24
|
isAuthError,
|
|
23
25
|
isAuthImplicitGrantRedirectError,
|
|
26
|
+
isAuthRefreshDiscardedError,
|
|
24
27
|
isAuthRetryableFetchError,
|
|
25
28
|
isAuthSessionMissingError,
|
|
26
29
|
} from './lib/errors'
|
|
@@ -33,6 +36,7 @@ import {
|
|
|
33
36
|
_userResponse,
|
|
34
37
|
} from './lib/fetch'
|
|
35
38
|
import {
|
|
39
|
+
assertPasskeyExperimentalEnabled,
|
|
36
40
|
decodeJWT,
|
|
37
41
|
deepClone,
|
|
38
42
|
Deferred,
|
|
@@ -136,6 +140,22 @@ import type {
|
|
|
136
140
|
UserResponse,
|
|
137
141
|
VerifyOtpParams,
|
|
138
142
|
Web3Credentials,
|
|
143
|
+
AuthPasskeyApi,
|
|
144
|
+
ExperimentalFeatureFlags,
|
|
145
|
+
SignInWithPasskeyCredentials,
|
|
146
|
+
RegisterPasskeyCredentials,
|
|
147
|
+
VerifyPasskeyRegistrationParams,
|
|
148
|
+
StartPasskeyAuthenticationParams,
|
|
149
|
+
VerifyPasskeyAuthenticationParams,
|
|
150
|
+
PasskeyUpdateParams,
|
|
151
|
+
PasskeyDeleteParams,
|
|
152
|
+
AuthPasskeyRegistrationOptionsResponse,
|
|
153
|
+
AuthPasskeyRegistrationVerifyResponse,
|
|
154
|
+
AuthPasskeyAuthenticationOptionsResponse,
|
|
155
|
+
AuthPasskeyAuthenticationVerifyResponse,
|
|
156
|
+
AuthPasskeyListResponse,
|
|
157
|
+
AuthPasskeyUpdateResponse,
|
|
158
|
+
AuthPasskeyDeleteResponse,
|
|
139
159
|
} from './lib/types'
|
|
140
160
|
import {
|
|
141
161
|
createSiweMessage,
|
|
@@ -146,10 +166,14 @@ import {
|
|
|
146
166
|
toHex,
|
|
147
167
|
} from './lib/web3/ethereum'
|
|
148
168
|
import {
|
|
169
|
+
createCredential,
|
|
149
170
|
deserializeCredentialCreationOptions,
|
|
150
171
|
deserializeCredentialRequestOptions,
|
|
172
|
+
getCredential,
|
|
151
173
|
serializeCredentialCreationResponse,
|
|
152
174
|
serializeCredentialRequestResponse,
|
|
175
|
+
browserSupportsWebAuthn,
|
|
176
|
+
webAuthnAbortService,
|
|
153
177
|
WebAuthnApi,
|
|
154
178
|
} from './lib/webauthn'
|
|
155
179
|
import {
|
|
@@ -174,10 +198,17 @@ const DEFAULT_OPTIONS: Omit<
|
|
|
174
198
|
debug: false,
|
|
175
199
|
hasCustomAuthorizationHeader: false,
|
|
176
200
|
throwOnError: false,
|
|
177
|
-
lockAcquireTimeout: 5000, // 5 seconds
|
|
201
|
+
lockAcquireTimeout: 5000, // 5 seconds. Only used when a custom `lock` is supplied. TODO(v3): remove.
|
|
178
202
|
skipAutoInitialize: false,
|
|
203
|
+
experimental: {},
|
|
179
204
|
}
|
|
180
205
|
|
|
206
|
+
/**
|
|
207
|
+
* No-op lock used internally as a placeholder. Kept so older test setups that
|
|
208
|
+
* inject this exact reference do not break; new code never sees it because
|
|
209
|
+
* `this.lock` stays `null` when no custom lock is supplied (lockless path).
|
|
210
|
+
* TODO(v3): remove with the legacy lock path.
|
|
211
|
+
*/
|
|
181
212
|
async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
|
|
182
213
|
return await fn()
|
|
183
214
|
}
|
|
@@ -212,6 +243,13 @@ export default class GoTrueClient {
|
|
|
212
243
|
* Used to implement the authorization code flow on the consent page.
|
|
213
244
|
*/
|
|
214
245
|
oauth: AuthOAuthServerApi
|
|
246
|
+
/**
|
|
247
|
+
* Namespace for passkey methods.
|
|
248
|
+
* Includes lower-level two-step registration/authentication and passkey management.
|
|
249
|
+
*
|
|
250
|
+
* Requires `auth.experimental.passkey: true`; otherwise all methods throw.
|
|
251
|
+
*/
|
|
252
|
+
passkey: AuthPasskeyApi
|
|
215
253
|
/**
|
|
216
254
|
* The storage key used to identify the values saved in localStorage
|
|
217
255
|
*/
|
|
@@ -251,6 +289,33 @@ export default class GoTrueClient {
|
|
|
251
289
|
protected autoRefreshTickTimeout: ReturnType<typeof setTimeout> | null = null
|
|
252
290
|
protected visibilityChangedCallback: (() => Promise<any>) | null = null
|
|
253
291
|
protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
|
|
292
|
+
/**
|
|
293
|
+
* Cache of the most recent refresh failure, keyed by the refresh token
|
|
294
|
+
* that failed. Serial callers passing the *same* token within
|
|
295
|
+
* `REFRESH_FAILURE_COOLDOWN_MS` (including subsequent auto-refresh ticks)
|
|
296
|
+
* receive this cached result instead of firing another `/token` request.
|
|
297
|
+
* Callers passing a *different* token (token rotation pickup, explicit
|
|
298
|
+
* `setSession`/`refreshSession({ refresh_token })`, multi-account switch)
|
|
299
|
+
* bypass the cache and attempt a fresh refresh as they should.
|
|
300
|
+
* Cleared on any successful refresh (locally or via BroadcastChannel from
|
|
301
|
+
* another tab) and on `_removeSession`.
|
|
302
|
+
*
|
|
303
|
+
* Pairs with `refreshingDeferred`: concurrent callers share the in-flight
|
|
304
|
+
* promise, serial callers within the cooldown share the failure result.
|
|
305
|
+
*/
|
|
306
|
+
protected lastRefreshFailure: {
|
|
307
|
+
refreshToken: string
|
|
308
|
+
result: CallRefreshTokenResult
|
|
309
|
+
expiresAt: number
|
|
310
|
+
} | null = null
|
|
311
|
+
/**
|
|
312
|
+
* Monotonic counter incremented at the top of `_removeSession`, before any
|
|
313
|
+
* `await`. The commit guard inside `_callRefreshToken` captures this value
|
|
314
|
+
* before `_saveSession` and re-checks it after, so a `signOut` that
|
|
315
|
+
* interleaves inside `_saveSession`'s storage-write awaits is still caught
|
|
316
|
+
* (the post-fetch storage snapshot alone misses that window).
|
|
317
|
+
*/
|
|
318
|
+
protected _sessionRemovalEpoch = 0
|
|
254
319
|
/**
|
|
255
320
|
* Keeps track of the async client initialization.
|
|
256
321
|
* When null or not yet resolved the auth state is `unknown`
|
|
@@ -258,6 +323,23 @@ export default class GoTrueClient {
|
|
|
258
323
|
* Keep extra care to never reject or throw uncaught errors
|
|
259
324
|
*/
|
|
260
325
|
protected initializePromise: Promise<InitializeResult> | null = null
|
|
326
|
+
/**
|
|
327
|
+
* Non-null only while `initialize()` is running. While open,
|
|
328
|
+
* `_notifyAllSubscribers` enqueues init-chain notifications (those fired with
|
|
329
|
+
* `broadcast = true`, i.e. from `_recoverAndRefresh`) into this array instead
|
|
330
|
+
* of firing directly, so that `initializePromise` is guaranteed to be
|
|
331
|
+
* resolved before any subscriber callback runs. Callbacks that call
|
|
332
|
+
* `getSession()` / `getUser()` etc. would otherwise deadlock because those
|
|
333
|
+
* methods await `initializePromise`. Notifications from the incoming
|
|
334
|
+
* BroadcastChannel handler (`broadcast = false`) are not enqueued — they fire
|
|
335
|
+
* immediately. Flushed (in order) by `initialize()` after `initializePromise`
|
|
336
|
+
* settles.
|
|
337
|
+
*/
|
|
338
|
+
private _pendingInitNotifications: Array<{
|
|
339
|
+
event: AuthChangeEvent
|
|
340
|
+
session: Session | null
|
|
341
|
+
broadcast: boolean
|
|
342
|
+
}> | null = null
|
|
261
343
|
protected detectSessionInUrl:
|
|
262
344
|
| boolean
|
|
263
345
|
| ((url: URL, params: { [parameter: string]: string }) => boolean) = true
|
|
@@ -268,11 +350,25 @@ export default class GoTrueClient {
|
|
|
268
350
|
protected hasCustomAuthorizationHeader = false
|
|
269
351
|
protected suppressGetSessionWarning = false
|
|
270
352
|
protected fetch: Fetch
|
|
271
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Custom lock function passed via `settings.lock`. When non-null, every auth
|
|
355
|
+
* operation runs inside `_acquireLock`. When null (the default), the client
|
|
356
|
+
* uses its lockless coordination (refresh single-flight + commit guard).
|
|
357
|
+
* TODO(v3): remove along with the legacy lock path.
|
|
358
|
+
*/
|
|
359
|
+
protected lock: LockFunc | null = null
|
|
272
360
|
protected lockAcquired = false
|
|
273
361
|
protected pendingInLock: Promise<any>[] = []
|
|
274
362
|
protected throwOnError: boolean
|
|
363
|
+
/**
|
|
364
|
+
* Only consulted when a custom `lock` is supplied. TODO(v3): remove.
|
|
365
|
+
*/
|
|
275
366
|
protected lockAcquireTimeout: number
|
|
367
|
+
/**
|
|
368
|
+
* Opt-in flags for experimental features. Defaults to an empty object.
|
|
369
|
+
* See `GoTrueClientOptions.experimental`.
|
|
370
|
+
*/
|
|
371
|
+
protected experimental: ExperimentalFeatureFlags
|
|
276
372
|
|
|
277
373
|
/**
|
|
278
374
|
* Used to broadcast state change events to other tabs listening.
|
|
@@ -285,13 +381,21 @@ export default class GoTrueClient {
|
|
|
285
381
|
/**
|
|
286
382
|
* Create a new client for use in the browser.
|
|
287
383
|
*
|
|
288
|
-
* @example
|
|
384
|
+
* @example Using supabase-js (recommended)
|
|
385
|
+
* ```ts
|
|
386
|
+
* import { createClient } from '@supabase/supabase-js'
|
|
387
|
+
*
|
|
388
|
+
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
|
|
389
|
+
* const { data, error } = await supabase.auth.getUser()
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @example Standalone import for bundle-sensitive environments
|
|
289
393
|
* ```ts
|
|
290
394
|
* import { GoTrueClient } from '@supabase/auth-js'
|
|
291
395
|
*
|
|
292
396
|
* const auth = new GoTrueClient({
|
|
293
397
|
* url: 'https://xyzcompany.supabase.co/auth/v1',
|
|
294
|
-
* headers: { apikey: '
|
|
398
|
+
* headers: { apikey: 'your-publishable-key' },
|
|
295
399
|
* storageKey: 'supabase-auth',
|
|
296
400
|
* })
|
|
297
401
|
* ```
|
|
@@ -318,28 +422,34 @@ export default class GoTrueClient {
|
|
|
318
422
|
|
|
319
423
|
this.persistSession = settings.persistSession
|
|
320
424
|
this.autoRefreshToken = settings.autoRefreshToken
|
|
425
|
+
this.experimental = settings.experimental ?? {}
|
|
321
426
|
this.admin = new GoTrueAdminApi({
|
|
322
427
|
url: settings.url,
|
|
323
428
|
headers: settings.headers,
|
|
324
429
|
fetch: settings.fetch,
|
|
430
|
+
experimental: this.experimental,
|
|
325
431
|
})
|
|
326
432
|
|
|
327
433
|
this.url = settings.url
|
|
328
434
|
this.headers = settings.headers
|
|
329
435
|
this.fetch = resolveFetch(settings.fetch)
|
|
330
|
-
this.lock = settings.lock || lockNoOp
|
|
331
436
|
this.detectSessionInUrl = settings.detectSessionInUrl
|
|
332
437
|
this.flowType = settings.flowType
|
|
333
438
|
this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader
|
|
334
439
|
this.throwOnError = settings.throwOnError
|
|
440
|
+
|
|
441
|
+
// Always wire `lockAcquireTimeout` even on the lockless path: consumers
|
|
442
|
+
// (including supabase-js tests) read it off the client to verify option
|
|
443
|
+
// flow-through.
|
|
335
444
|
this.lockAcquireTimeout = settings.lockAcquireTimeout
|
|
336
445
|
|
|
337
|
-
|
|
446
|
+
// TODO(v3): remove. Legacy opt-in path preserved for backwards
|
|
447
|
+
// compatibility with callers passing a custom `lock` (typically React
|
|
448
|
+
// Native `processLock` or Node multi-process setups). When `settings.lock`
|
|
449
|
+
// is null the client uses its lockless coordination — no `navigator.locks`
|
|
450
|
+
// by default, no implicit `processLock`.
|
|
451
|
+
if (settings.lock != null) {
|
|
338
452
|
this.lock = settings.lock
|
|
339
|
-
} else if (this.persistSession && isBrowser() && globalThis?.navigator?.locks) {
|
|
340
|
-
this.lock = navigatorLock
|
|
341
|
-
} else {
|
|
342
|
-
this.lock = lockNoOp
|
|
343
453
|
}
|
|
344
454
|
|
|
345
455
|
if (!this.jwks) {
|
|
@@ -366,6 +476,16 @@ export default class GoTrueClient {
|
|
|
366
476
|
revokeGrant: this._revokeOAuthGrant.bind(this),
|
|
367
477
|
}
|
|
368
478
|
|
|
479
|
+
this.passkey = {
|
|
480
|
+
startRegistration: this._startPasskeyRegistration.bind(this),
|
|
481
|
+
verifyRegistration: this._verifyPasskeyRegistration.bind(this),
|
|
482
|
+
startAuthentication: this._startPasskeyAuthentication.bind(this),
|
|
483
|
+
verifyAuthentication: this._verifyPasskeyAuthentication.bind(this),
|
|
484
|
+
list: this._listPasskeys.bind(this),
|
|
485
|
+
update: this._updatePasskey.bind(this),
|
|
486
|
+
delete: this._deletePasskey.bind(this),
|
|
487
|
+
}
|
|
488
|
+
|
|
369
489
|
if (this.persistSession) {
|
|
370
490
|
if (settings.storage) {
|
|
371
491
|
this.storage = settings.storage
|
|
@@ -389,7 +509,7 @@ export default class GoTrueClient {
|
|
|
389
509
|
if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) {
|
|
390
510
|
try {
|
|
391
511
|
this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey)
|
|
392
|
-
} catch (e
|
|
512
|
+
} catch (e) {
|
|
393
513
|
console.error(
|
|
394
514
|
'Failed to create a new BroadcastChannel, multi-tab state changes will not be available',
|
|
395
515
|
e
|
|
@@ -399,6 +519,13 @@ export default class GoTrueClient {
|
|
|
399
519
|
this.broadcastChannel?.addEventListener('message', async (event) => {
|
|
400
520
|
this._debug('received broadcast notification from other tab or client', event)
|
|
401
521
|
|
|
522
|
+
// Another tab successfully refreshed or signed in — any cached
|
|
523
|
+
// failure in this tab is stale and should not block the next
|
|
524
|
+
// refresh attempt.
|
|
525
|
+
if (event.data.event === 'TOKEN_REFRESHED' || event.data.event === 'SIGNED_IN') {
|
|
526
|
+
this.lastRefreshFailure = null
|
|
527
|
+
}
|
|
528
|
+
|
|
402
529
|
try {
|
|
403
530
|
await this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
|
|
404
531
|
} catch (error) {
|
|
@@ -452,9 +579,20 @@ export default class GoTrueClient {
|
|
|
452
579
|
}
|
|
453
580
|
|
|
454
581
|
/**
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
582
|
+
* Initialize the auth client by loading the session from storage or
|
|
583
|
+
* detecting it from the URL after an OAuth, magic-link, or password-recovery
|
|
584
|
+
* redirect.
|
|
585
|
+
*
|
|
586
|
+
* **Most callers do not need to invoke this directly.** The client calls it
|
|
587
|
+
* automatically during construction, and to react to sign-in events (including
|
|
588
|
+
* post-redirect events) you should subscribe to `onAuthStateChange` rather
|
|
589
|
+
* than awaiting `initialize()`.
|
|
590
|
+
*
|
|
591
|
+
* You only need to call it manually when you have opted out of the automatic
|
|
592
|
+
* call by passing `skipAutoInitialize: true` — for example, in an SSR context
|
|
593
|
+
* where you need to control initialization timing. In that case, awaiting
|
|
594
|
+
* `initialize()` returns the resolved session result (or any error encountered
|
|
595
|
+
* while detecting it from the URL).
|
|
458
596
|
*
|
|
459
597
|
* @category Auth
|
|
460
598
|
*/
|
|
@@ -463,13 +601,37 @@ export default class GoTrueClient {
|
|
|
463
601
|
return await this.initializePromise
|
|
464
602
|
}
|
|
465
603
|
|
|
604
|
+
// Open the notification queue before _initialize() runs so that every
|
|
605
|
+
// _notifyAllSubscribers call inside the init chain enqueues instead of
|
|
606
|
+
// firing. Without this, a callback receiving SIGNED_IN (or TOKEN_REFRESHED
|
|
607
|
+
// / SIGNED_OUT) during _recoverAndRefresh would deadlock if it called
|
|
608
|
+
// getSession() / getUser() — those methods await initializePromise, which
|
|
609
|
+
// can only resolve after the callback returns, which can only return after
|
|
610
|
+
// getSession() resolves. The queue is flushed below after initializePromise
|
|
611
|
+
// has settled, so callbacks run with a fully resolved initializePromise.
|
|
612
|
+
this._pendingInitNotifications = []
|
|
613
|
+
|
|
466
614
|
this.initializePromise = (async () => {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
615
|
+
if (this.lock != null) {
|
|
616
|
+
// TODO(v3): remove legacy lock path
|
|
617
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
618
|
+
return await this._initialize()
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
return await this._initialize()
|
|
470
622
|
})()
|
|
471
623
|
|
|
472
|
-
|
|
624
|
+
const result = await this.initializePromise
|
|
625
|
+
|
|
626
|
+
// initializePromise is now resolved — flush queued notifications in order.
|
|
627
|
+
// Callbacks can safely call getSession() / getUser() / signOut() etc.
|
|
628
|
+
const queue = this._pendingInitNotifications ?? []
|
|
629
|
+
this._pendingInitNotifications = null
|
|
630
|
+
for (const n of queue) {
|
|
631
|
+
await this._notifyAllSubscribers(n.event, n.session, n.broadcast)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return result
|
|
473
635
|
}
|
|
474
636
|
|
|
475
637
|
/**
|
|
@@ -1029,6 +1191,21 @@ export default class GoTrueClient {
|
|
|
1029
1191
|
* password: 'some-password',
|
|
1030
1192
|
* })
|
|
1031
1193
|
* ```
|
|
1194
|
+
*
|
|
1195
|
+
* @exampleDescription Handling errors
|
|
1196
|
+
* Log the full `error` object so fields like `code`, `status`, and `name` aren't hidden. The `error.code` (e.g. `'invalid_credentials'`, `'email_not_confirmed'`) is often more useful for branching than `error.message`, and the full object surfaces both.
|
|
1197
|
+
*
|
|
1198
|
+
* @example Handling errors
|
|
1199
|
+
* ```js
|
|
1200
|
+
* const { data, error } = await supabase.auth.signInWithPassword({
|
|
1201
|
+
* email: 'example@email.com',
|
|
1202
|
+
* password: 'example-password',
|
|
1203
|
+
* })
|
|
1204
|
+
* if (error) {
|
|
1205
|
+
* console.error(error)
|
|
1206
|
+
* return
|
|
1207
|
+
* }
|
|
1208
|
+
* ```
|
|
1032
1209
|
*/
|
|
1033
1210
|
async signInWithPassword(
|
|
1034
1211
|
credentials: SignInWithPasswordCredentials
|
|
@@ -1351,9 +1528,14 @@ export default class GoTrueClient {
|
|
|
1351
1528
|
async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
|
|
1352
1529
|
await this.initializePromise
|
|
1353
1530
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1531
|
+
if (this.lock != null) {
|
|
1532
|
+
// TODO(v3): remove legacy lock path
|
|
1533
|
+
return this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
1534
|
+
return this._exchangeCodeForSession(authCode)
|
|
1535
|
+
})
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
return this._exchangeCodeForSession(authCode)
|
|
1357
1539
|
}
|
|
1358
1540
|
|
|
1359
1541
|
/**
|
|
@@ -1827,7 +2009,10 @@ export default class GoTrueClient {
|
|
|
1827
2009
|
}
|
|
1828
2010
|
if (data.session) {
|
|
1829
2011
|
await this._saveSession(data.session)
|
|
1830
|
-
await this._notifyAllSubscribers(
|
|
2012
|
+
await this._notifyAllSubscribers(
|
|
2013
|
+
redirectType === 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN',
|
|
2014
|
+
data.session
|
|
2015
|
+
)
|
|
1831
2016
|
}
|
|
1832
2017
|
return this._returnResult({ data: { ...data, redirectType: redirectType ?? null }, error })
|
|
1833
2018
|
} catch (error) {
|
|
@@ -2248,7 +2433,7 @@ export default class GoTrueClient {
|
|
|
2248
2433
|
}
|
|
2249
2434
|
|
|
2250
2435
|
const session: Session | null = data.session
|
|
2251
|
-
const user: User = data.user
|
|
2436
|
+
const user: User | null = data.user
|
|
2252
2437
|
|
|
2253
2438
|
if (session?.access_token) {
|
|
2254
2439
|
await this._saveSession(session as Session)
|
|
@@ -2387,9 +2572,14 @@ export default class GoTrueClient {
|
|
|
2387
2572
|
async reauthenticate(): Promise<AuthResponse> {
|
|
2388
2573
|
await this.initializePromise
|
|
2389
2574
|
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2575
|
+
if (this.lock != null) {
|
|
2576
|
+
// TODO(v3): remove legacy lock path
|
|
2577
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2578
|
+
return await this._reauthenticate()
|
|
2579
|
+
})
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
return await this._reauthenticate()
|
|
2393
2583
|
}
|
|
2394
2584
|
|
|
2395
2585
|
private async _reauthenticate(): Promise<AuthResponse> {
|
|
@@ -2480,15 +2670,28 @@ export default class GoTrueClient {
|
|
|
2480
2670
|
const endpoint = `${this.url}/resend`
|
|
2481
2671
|
if ('email' in credentials) {
|
|
2482
2672
|
const { email, type, options } = credentials
|
|
2673
|
+
let codeChallenge: string | null = null
|
|
2674
|
+
let codeChallengeMethod: string | null = null
|
|
2675
|
+
if (this.flowType === 'pkce') {
|
|
2676
|
+
;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
|
|
2677
|
+
this.storage,
|
|
2678
|
+
this.storageKey
|
|
2679
|
+
)
|
|
2680
|
+
}
|
|
2483
2681
|
const { error } = await _request(this.fetch, 'POST', endpoint, {
|
|
2484
2682
|
headers: this.headers,
|
|
2485
2683
|
body: {
|
|
2486
2684
|
email,
|
|
2487
2685
|
type,
|
|
2488
2686
|
gotrue_meta_security: { captcha_token: options?.captchaToken },
|
|
2687
|
+
code_challenge: codeChallenge,
|
|
2688
|
+
code_challenge_method: codeChallengeMethod,
|
|
2489
2689
|
},
|
|
2490
2690
|
redirectTo: options?.emailRedirectTo,
|
|
2491
2691
|
})
|
|
2692
|
+
if (error) {
|
|
2693
|
+
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
|
|
2694
|
+
}
|
|
2492
2695
|
return this._returnResult({ data: { user: null, session: null }, error })
|
|
2493
2696
|
} else if ('phone' in credentials) {
|
|
2494
2697
|
const { phone, type, options } = credentials
|
|
@@ -2509,6 +2712,7 @@ export default class GoTrueClient {
|
|
|
2509
2712
|
'You must provide either an email or phone number and a type'
|
|
2510
2713
|
)
|
|
2511
2714
|
} catch (error) {
|
|
2715
|
+
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
|
|
2512
2716
|
if (isAuthError(error)) {
|
|
2513
2717
|
return this._returnResult({ data: { user: null, session: null }, error })
|
|
2514
2718
|
}
|
|
@@ -2525,7 +2729,7 @@ export default class GoTrueClient {
|
|
|
2525
2729
|
* to the client. If that storage is based on request cookies for example,
|
|
2526
2730
|
* the values in it may not be authentic and therefore it's strongly advised
|
|
2527
2731
|
* against using this method and its results in such circumstances. A warning
|
|
2528
|
-
* will be emitted if this is detected. Use {@link
|
|
2732
|
+
* will be emitted if this is detected. Use {@link GoTrueClient.getUser} instead.
|
|
2529
2733
|
*
|
|
2530
2734
|
* @category Auth
|
|
2531
2735
|
*
|
|
@@ -2536,7 +2740,7 @@ export default class GoTrueClient {
|
|
|
2536
2740
|
* - If the session's access token is expired or is about to expire, this method will use the refresh token to refresh the session.
|
|
2537
2741
|
* - When using in a browser, or you've called `startAutoRefresh()` in your environment (React Native, etc.) this function always returns a valid access token without refreshing the session itself, as this is done in the background. This function returns very fast.
|
|
2538
2742
|
* - **IMPORTANT SECURITY NOTICE:** If using an insecure storage medium, such as cookies or request headers, the user object returned by this function **must not be trusted**. Always verify the JWT using `getClaims()` or your own JWT verification library to securely establish the user's identity and access. You can also use `getUser()` to fetch the user object directly from the Auth server for this purpose.
|
|
2539
|
-
* -
|
|
2743
|
+
* - Cross-tab refresh races are handled by the GoTrue server (the rotated token from the first tab is returned to subsequent tabs via the parent-of-active mechanism), so no client-side serialization is needed.
|
|
2540
2744
|
*
|
|
2541
2745
|
* @example Get the session data
|
|
2542
2746
|
* ```js
|
|
@@ -2604,17 +2808,26 @@ export default class GoTrueClient {
|
|
|
2604
2808
|
async getSession() {
|
|
2605
2809
|
await this.initializePromise
|
|
2606
2810
|
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2811
|
+
if (this.lock != null) {
|
|
2812
|
+
// TODO(v3): remove legacy lock path
|
|
2813
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2814
|
+
return this._useSession(async (result) => {
|
|
2815
|
+
return result
|
|
2816
|
+
})
|
|
2610
2817
|
})
|
|
2611
|
-
}
|
|
2818
|
+
}
|
|
2612
2819
|
|
|
2613
|
-
return result
|
|
2820
|
+
return await this._useSession(async (result) => {
|
|
2821
|
+
return result
|
|
2822
|
+
})
|
|
2614
2823
|
}
|
|
2615
2824
|
|
|
2616
2825
|
/**
|
|
2617
2826
|
* Acquires a global lock based on the storage key.
|
|
2827
|
+
*
|
|
2828
|
+
* TODO(v3): remove along with the legacy lock path. Only called when
|
|
2829
|
+
* `this.lock` is non-null (custom lock supplied via constructor). The
|
|
2830
|
+
* default lockless path bypasses this entirely.
|
|
2618
2831
|
*/
|
|
2619
2832
|
private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
|
|
2620
2833
|
this._debug('#_acquireLock', 'begin', acquireTimeout)
|
|
@@ -2634,7 +2847,7 @@ export default class GoTrueClient {
|
|
|
2634
2847
|
(async () => {
|
|
2635
2848
|
try {
|
|
2636
2849
|
await result
|
|
2637
|
-
} catch (
|
|
2850
|
+
} catch (_e) {
|
|
2638
2851
|
// we just care if it finished
|
|
2639
2852
|
}
|
|
2640
2853
|
})()
|
|
@@ -2643,7 +2856,7 @@ export default class GoTrueClient {
|
|
|
2643
2856
|
return result
|
|
2644
2857
|
}
|
|
2645
2858
|
|
|
2646
|
-
return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
|
|
2859
|
+
return await this.lock!(`lock:${this.storageKey}`, acquireTimeout, async () => {
|
|
2647
2860
|
this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
|
|
2648
2861
|
|
|
2649
2862
|
try {
|
|
@@ -2685,10 +2898,9 @@ export default class GoTrueClient {
|
|
|
2685
2898
|
}
|
|
2686
2899
|
|
|
2687
2900
|
/**
|
|
2688
|
-
* Use instead of {@link
|
|
2689
|
-
*
|
|
2690
|
-
*
|
|
2691
|
-
* session at once across multiple tabs or processes.
|
|
2901
|
+
* Use instead of {@link GoTrueClient.getSession} inside the library. Loads the session
|
|
2902
|
+
* via `__loadSession` (which may trigger a refresh if the access token is
|
|
2903
|
+
* within the expiry margin) and runs `fn` with the result.
|
|
2692
2904
|
*/
|
|
2693
2905
|
private async _useSession<R>(
|
|
2694
2906
|
fn: (
|
|
@@ -2716,7 +2928,10 @@ export default class GoTrueClient {
|
|
|
2716
2928
|
this._debug('#_useSession', 'begin')
|
|
2717
2929
|
|
|
2718
2930
|
try {
|
|
2719
|
-
//
|
|
2931
|
+
// Concurrent callers may both reach __loadSession; storage reads are
|
|
2932
|
+
// idempotent, and the only write path inside it (refresh) is
|
|
2933
|
+
// single-flighted downstream by `refreshingDeferred` in
|
|
2934
|
+
// `_callRefreshToken`. No serialization is needed at this layer.
|
|
2720
2935
|
const result = await this.__loadSession()
|
|
2721
2936
|
|
|
2722
2937
|
return await fn(result)
|
|
@@ -2728,7 +2943,7 @@ export default class GoTrueClient {
|
|
|
2728
2943
|
/**
|
|
2729
2944
|
* NEVER USE DIRECTLY!
|
|
2730
2945
|
*
|
|
2731
|
-
* Always use
|
|
2946
|
+
* Always use `_useSession`.
|
|
2732
2947
|
*/
|
|
2733
2948
|
private async __loadSession(): Promise<
|
|
2734
2949
|
| {
|
|
@@ -2752,7 +2967,8 @@ export default class GoTrueClient {
|
|
|
2752
2967
|
> {
|
|
2753
2968
|
this._debug('#__loadSession()', 'begin')
|
|
2754
2969
|
|
|
2755
|
-
if (!this.lockAcquired) {
|
|
2970
|
+
if (this.lock != null && !this.lockAcquired) {
|
|
2971
|
+
// TODO(v3): remove. Only meaningful on the legacy lock path.
|
|
2756
2972
|
this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
|
|
2757
2973
|
}
|
|
2758
2974
|
|
|
@@ -2827,6 +3043,27 @@ export default class GoTrueClient {
|
|
|
2827
3043
|
|
|
2828
3044
|
const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
|
|
2829
3045
|
if (error) {
|
|
3046
|
+
// Proactive-preserve mirror: `_callRefreshToken` keeps the session
|
|
3047
|
+
// in storage when refresh fails non-retryably but the access token
|
|
3048
|
+
// is still inside its real expiry window. Hand the caller the
|
|
3049
|
+
// still-valid session instead of translating the refresh error
|
|
3050
|
+
// into `session: null`. If the access token has actually expired,
|
|
3051
|
+
// the session is genuinely dead and the error stands. Explicit
|
|
3052
|
+
// refresh entry points (`refreshSession`, `setSession`)
|
|
3053
|
+
// intentionally bypass this fallback — they want to know the
|
|
3054
|
+
// refresh failed.
|
|
3055
|
+
const accessTokenStillValid = !!(
|
|
3056
|
+
currentSession.expires_at && currentSession.expires_at * 1000 > Date.now()
|
|
3057
|
+
)
|
|
3058
|
+
if (accessTokenStillValid) {
|
|
3059
|
+
// Race guard: a concurrent `signOut` may have cleared storage
|
|
3060
|
+
// during the refresh attempt. Don't hand back a session that no
|
|
3061
|
+
// longer exists on disk.
|
|
3062
|
+
const stillStored = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
3063
|
+
if (stillStored && stillStored.refresh_token === currentSession.refresh_token) {
|
|
3064
|
+
return this._returnResult({ data: { session: currentSession }, error: null })
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
2830
3067
|
return this._returnResult({ data: { session: null }, error })
|
|
2831
3068
|
}
|
|
2832
3069
|
|
|
@@ -2919,9 +3156,15 @@ export default class GoTrueClient {
|
|
|
2919
3156
|
|
|
2920
3157
|
await this.initializePromise
|
|
2921
3158
|
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
3159
|
+
let result: UserResponse
|
|
3160
|
+
if (this.lock != null) {
|
|
3161
|
+
// TODO(v3): remove legacy lock path
|
|
3162
|
+
result = await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3163
|
+
return await this._getUser()
|
|
3164
|
+
})
|
|
3165
|
+
} else {
|
|
3166
|
+
result = await this._getUser()
|
|
3167
|
+
}
|
|
2925
3168
|
|
|
2926
3169
|
if (result.data.user) {
|
|
2927
3170
|
this.suppressGetSessionWarning = true
|
|
@@ -3096,9 +3339,14 @@ export default class GoTrueClient {
|
|
|
3096
3339
|
): Promise<UserResponse> {
|
|
3097
3340
|
await this.initializePromise
|
|
3098
3341
|
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3342
|
+
if (this.lock != null) {
|
|
3343
|
+
// TODO(v3): remove legacy lock path
|
|
3344
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3345
|
+
return await this._updateUser(attributes, options)
|
|
3346
|
+
})
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
return await this._updateUser(attributes, options)
|
|
3102
3350
|
}
|
|
3103
3351
|
|
|
3104
3352
|
protected async _updateUser(
|
|
@@ -3285,9 +3533,14 @@ export default class GoTrueClient {
|
|
|
3285
3533
|
}): Promise<AuthResponse> {
|
|
3286
3534
|
await this.initializePromise
|
|
3287
3535
|
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3536
|
+
if (this.lock != null) {
|
|
3537
|
+
// TODO(v3): remove legacy lock path
|
|
3538
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3539
|
+
return await this._setSession(currentSession)
|
|
3540
|
+
})
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
return await this._setSession(currentSession)
|
|
3291
3544
|
}
|
|
3292
3545
|
|
|
3293
3546
|
protected async _setSession(currentSession: {
|
|
@@ -3476,9 +3729,14 @@ export default class GoTrueClient {
|
|
|
3476
3729
|
async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
|
|
3477
3730
|
await this.initializePromise
|
|
3478
3731
|
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3732
|
+
if (this.lock != null) {
|
|
3733
|
+
// TODO(v3): remove legacy lock path
|
|
3734
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3735
|
+
return await this._refreshSession(currentSession)
|
|
3736
|
+
})
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
return await this._refreshSession(currentSession)
|
|
3482
3740
|
}
|
|
3483
3741
|
|
|
3484
3742
|
protected async _refreshSession(currentSession?: {
|
|
@@ -3576,7 +3834,10 @@ export default class GoTrueClient {
|
|
|
3576
3834
|
|
|
3577
3835
|
window.history.replaceState(window.history.state, '', url.toString())
|
|
3578
3836
|
|
|
3579
|
-
return {
|
|
3837
|
+
return {
|
|
3838
|
+
data: { session: data.session, redirectType: data.redirectType ?? null },
|
|
3839
|
+
error: null,
|
|
3840
|
+
}
|
|
3580
3841
|
}
|
|
3581
3842
|
|
|
3582
3843
|
const {
|
|
@@ -3664,7 +3925,9 @@ export default class GoTrueClient {
|
|
|
3664
3925
|
if (typeof this.detectSessionInUrl === 'function') {
|
|
3665
3926
|
return this.detectSessionInUrl(new URL(window.location.href), params)
|
|
3666
3927
|
}
|
|
3667
|
-
return Boolean(
|
|
3928
|
+
return Boolean(
|
|
3929
|
+
params.access_token || params.error || params.error_description || params.error_code
|
|
3930
|
+
)
|
|
3668
3931
|
}
|
|
3669
3932
|
|
|
3670
3933
|
/**
|
|
@@ -3687,24 +3950,35 @@ export default class GoTrueClient {
|
|
|
3687
3950
|
*
|
|
3688
3951
|
* If using `others` scope, no `SIGNED_OUT` event is fired!
|
|
3689
3952
|
*
|
|
3953
|
+
* **Warning:** the default `scope` is `'global'`. This signs the user out of
|
|
3954
|
+
* **every device they are currently signed in on**, not just the current
|
|
3955
|
+
* tab/session. If you only want to sign the user out of the current session
|
|
3956
|
+
* (the behavior most other auth libraries default to), pass
|
|
3957
|
+
* `{ scope: 'local' }` explicitly.
|
|
3958
|
+
*
|
|
3690
3959
|
* @category Auth
|
|
3691
3960
|
*
|
|
3692
3961
|
* @remarks
|
|
3693
3962
|
* - In order to use the `signOut()` method, the user needs to be signed in first.
|
|
3694
|
-
* - By default, `signOut()` uses the global scope, which signs out
|
|
3963
|
+
* - By default, `signOut()` uses the **global** scope, which signs out the user
|
|
3964
|
+
* on every device they are signed in on (not just the current one). Pass
|
|
3965
|
+
* `{ scope: 'local' }` to only sign out the current session. This is
|
|
3966
|
+
* usually what apps want on a "Sign out" button, especially when users
|
|
3967
|
+
* sign in from multiple devices and do not expect signing out of one to
|
|
3968
|
+
* terminate the others.
|
|
3695
3969
|
* - Since Supabase Auth uses JWTs for authentication, the access token JWT will be valid until it's expired. When the user signs out, Supabase revokes the refresh token and deletes the JWT from the client-side. This does not revoke the JWT and it will still be valid until it expires.
|
|
3696
3970
|
*
|
|
3697
|
-
* @example Sign out (
|
|
3971
|
+
* @example Sign out of every device (global – default)
|
|
3698
3972
|
* ```js
|
|
3699
3973
|
* const { error } = await supabase.auth.signOut()
|
|
3700
3974
|
* ```
|
|
3701
3975
|
*
|
|
3702
|
-
* @example Sign out
|
|
3976
|
+
* @example Sign out only the current session (recommended for most apps)
|
|
3703
3977
|
* ```js
|
|
3704
3978
|
* const { error } = await supabase.auth.signOut({ scope: 'local' })
|
|
3705
3979
|
* ```
|
|
3706
3980
|
*
|
|
3707
|
-
* @example Sign out
|
|
3981
|
+
* @example Sign out of all other sessions, keep the current one
|
|
3708
3982
|
* ```js
|
|
3709
3983
|
* const { error } = await supabase.auth.signOut({ scope: 'others' })
|
|
3710
3984
|
* ```
|
|
@@ -3712,15 +3986,24 @@ export default class GoTrueClient {
|
|
|
3712
3986
|
async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
|
|
3713
3987
|
await this.initializePromise
|
|
3714
3988
|
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3989
|
+
if (this.lock != null) {
|
|
3990
|
+
// TODO(v3): remove legacy lock path
|
|
3991
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3992
|
+
return await this._signOut(options)
|
|
3993
|
+
})
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
return await this._signOut(options)
|
|
3718
3997
|
}
|
|
3719
3998
|
|
|
3720
3999
|
protected async _signOut(
|
|
3721
4000
|
{ scope }: SignOut = { scope: 'global' }
|
|
3722
4001
|
): Promise<{ error: AuthError | null }> {
|
|
3723
4002
|
return await this._useSession(async (result) => {
|
|
4003
|
+
const removeCurrentSession = async () => {
|
|
4004
|
+
await this._removeSession()
|
|
4005
|
+
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
|
|
4006
|
+
}
|
|
3724
4007
|
const { data, error: sessionError } = result
|
|
3725
4008
|
if (sessionError && !isAuthSessionMissingError(sessionError)) {
|
|
3726
4009
|
return this._returnResult({ error: sessionError })
|
|
@@ -3738,13 +4021,15 @@ export default class GoTrueClient {
|
|
|
3738
4021
|
isAuthSessionMissingError(error)
|
|
3739
4022
|
)
|
|
3740
4023
|
) {
|
|
4024
|
+
if (scope !== 'others') {
|
|
4025
|
+
await removeCurrentSession()
|
|
4026
|
+
}
|
|
3741
4027
|
return this._returnResult({ error })
|
|
3742
4028
|
}
|
|
3743
4029
|
}
|
|
3744
4030
|
}
|
|
3745
4031
|
if (scope !== 'others') {
|
|
3746
|
-
await
|
|
3747
|
-
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
|
|
4032
|
+
await removeCurrentSession()
|
|
3748
4033
|
}
|
|
3749
4034
|
return this._returnResult({ error: null })
|
|
3750
4035
|
})
|
|
@@ -3761,16 +4046,19 @@ export default class GoTrueClient {
|
|
|
3761
4046
|
}
|
|
3762
4047
|
|
|
3763
4048
|
/**
|
|
3764
|
-
*
|
|
3765
|
-
*
|
|
3766
|
-
*
|
|
3767
|
-
*
|
|
3768
|
-
*
|
|
3769
|
-
*
|
|
3770
|
-
*
|
|
4049
|
+
* Receive a notification every time an auth event happens. Common reentry
|
|
4050
|
+
* patterns (`getUser`, `setSession`, reading the session from inside a
|
|
4051
|
+
* handler) complete normally. One hazard remains: calling `refreshSession`
|
|
4052
|
+
* (or anything that routes through `_callRefreshToken`) from inside a
|
|
4053
|
+
* `TOKEN_REFRESHED` handler. `refreshingDeferred` resolves only after
|
|
4054
|
+
* `_notifyAllSubscribers` returns, so the inner refresh dedupes onto the
|
|
4055
|
+
* outer's unresolved promise and the two wait on each other.
|
|
3771
4056
|
*
|
|
3772
4057
|
* @param callback A callback function to be invoked when an auth event happens.
|
|
3773
|
-
*
|
|
4058
|
+
*
|
|
4059
|
+
* @deprecated Async callbacks can deadlock when they trigger a nested
|
|
4060
|
+
* refresh from a `TOKEN_REFRESHED` event. Prefer the sync overload, or move
|
|
4061
|
+
* refresh-triggering work outside the callback.
|
|
3774
4062
|
*/
|
|
3775
4063
|
onAuthStateChange(callback: (event: AuthChangeEvent, session: Session | null) => Promise<void>): {
|
|
3776
4064
|
data: { subscription: Subscription }
|
|
@@ -3783,18 +4071,8 @@ export default class GoTrueClient {
|
|
|
3783
4071
|
* - Subscribes to important events occurring on the user's session.
|
|
3784
4072
|
* - Use on the frontend/client. It is less useful on the server.
|
|
3785
4073
|
* - Events are emitted across tabs to keep your application's UI up-to-date. Some events can fire very frequently, based on the number of tabs open. Use a quick and efficient callback function, and defer or debounce as many operations as you can to be performed outside of the callback.
|
|
3786
|
-
* -
|
|
3787
|
-
*
|
|
3788
|
-
* - Limit the number of `await` calls in `async` callbacks.
|
|
3789
|
-
* - Do not use other Supabase functions in the callback function. If you must, dispatch the functions once the callback has finished executing. Use this as a quick way to achieve this:
|
|
3790
|
-
* ```js
|
|
3791
|
-
* supabase.auth.onAuthStateChange((event, session) => {
|
|
3792
|
-
* setTimeout(async () => {
|
|
3793
|
-
* // await on other Supabase function here
|
|
3794
|
-
* // this runs right after the callback has finished
|
|
3795
|
-
* }, 0)
|
|
3796
|
-
* })
|
|
3797
|
-
* ```
|
|
4074
|
+
* - Callbacks can be `async` and can safely call other Supabase auth methods (`getUser`, `setSession`, etc.) from inside the callback.
|
|
4075
|
+
* - Keep callbacks quick. Events are awaited in order, so a slow callback delays subsequent events to subscribers in this tab.
|
|
3798
4076
|
* - Emitted events:
|
|
3799
4077
|
* - `INITIAL_SESSION`
|
|
3800
4078
|
* - Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
|
|
@@ -3985,9 +4263,14 @@ export default class GoTrueClient {
|
|
|
3985
4263
|
;(async () => {
|
|
3986
4264
|
await this.initializePromise
|
|
3987
4265
|
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
4266
|
+
if (this.lock != null) {
|
|
4267
|
+
// TODO(v3): remove legacy lock path
|
|
4268
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
4269
|
+
this._emitInitialSession(id)
|
|
4270
|
+
})
|
|
4271
|
+
} else {
|
|
4272
|
+
await this._emitInitialSession(id)
|
|
4273
|
+
}
|
|
3991
4274
|
})()
|
|
3992
4275
|
|
|
3993
4276
|
return { data: { subscription } }
|
|
@@ -4007,7 +4290,10 @@ export default class GoTrueClient {
|
|
|
4007
4290
|
} catch (err) {
|
|
4008
4291
|
await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
|
|
4009
4292
|
this._debug('INITIAL_SESSION', 'callback id', id, 'error', err)
|
|
4010
|
-
if (isAuthSessionMissingError(err)) {
|
|
4293
|
+
if (isAuthSessionMissingError(err) || isAuthRetryableFetchError(err)) {
|
|
4294
|
+
// A missing session or a transient/aborted network failure (e.g. a
|
|
4295
|
+
// superseded page navigation cancelling the in-flight request) is
|
|
4296
|
+
// not an application error — warn rather than surface it raw.
|
|
4011
4297
|
console.warn(err)
|
|
4012
4298
|
} else {
|
|
4013
4299
|
console.error(err)
|
|
@@ -4382,7 +4668,10 @@ export default class GoTrueClient {
|
|
|
4382
4668
|
* @param refreshToken A valid refresh token that was returned on login.
|
|
4383
4669
|
*/
|
|
4384
4670
|
private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
|
|
4385
|
-
|
|
4671
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4672
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4673
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4674
|
+
const debugName = `#_refreshAccessToken()`
|
|
4386
4675
|
this._debug(debugName, 'begin')
|
|
4387
4676
|
|
|
4388
4677
|
try {
|
|
@@ -4535,15 +4824,16 @@ export default class GoTrueClient {
|
|
|
4535
4824
|
const { error } = await this._callRefreshToken(currentSession.refresh_token)
|
|
4536
4825
|
|
|
4537
4826
|
if (error) {
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
)
|
|
4546
|
-
|
|
4827
|
+
// `_callRefreshToken` is the single source of truth for refresh
|
|
4828
|
+
// outcomes: it removes the session itself when the access token
|
|
4829
|
+
// is actually expired, and preserves it when the token is still
|
|
4830
|
+
// valid (proactive-preserve). Don't second-guess that here — a
|
|
4831
|
+
// local `_removeSession` would emit a duplicate `SIGNED_OUT` on
|
|
4832
|
+
// genuine failures and undo the proactive-preserve at init time.
|
|
4833
|
+
if (isAuthRefreshDiscardedError(error)) {
|
|
4834
|
+
this._debug(debugName, 'refresh discarded by commit guard', error)
|
|
4835
|
+
} else {
|
|
4836
|
+
this._debug(debugName, 'refresh failed', error)
|
|
4547
4837
|
}
|
|
4548
4838
|
}
|
|
4549
4839
|
}
|
|
@@ -4579,7 +4869,15 @@ export default class GoTrueClient {
|
|
|
4579
4869
|
} catch (err) {
|
|
4580
4870
|
this._debug(debugName, 'error', err)
|
|
4581
4871
|
|
|
4582
|
-
|
|
4872
|
+
if (isAuthRetryableFetchError(err)) {
|
|
4873
|
+
// Transient/aborted network failure during session recovery (e.g. a
|
|
4874
|
+
// superseded page navigation cancelling the in-flight request). Warn
|
|
4875
|
+
// rather than surface it raw; the session is untouched and recovery
|
|
4876
|
+
// will run again on the next load.
|
|
4877
|
+
console.warn(err)
|
|
4878
|
+
} else {
|
|
4879
|
+
console.error(err)
|
|
4880
|
+
}
|
|
4583
4881
|
return
|
|
4584
4882
|
} finally {
|
|
4585
4883
|
this._debug(debugName, 'end')
|
|
@@ -4596,22 +4894,113 @@ export default class GoTrueClient {
|
|
|
4596
4894
|
return this.refreshingDeferred.promise
|
|
4597
4895
|
}
|
|
4598
4896
|
|
|
4599
|
-
|
|
4897
|
+
// Serial failure cooldown: callers passing the *same* refresh token
|
|
4898
|
+
// after a recent failure receive the cached result instead of firing
|
|
4899
|
+
// another `/token` request. This caps the proactive-refresh storm
|
|
4900
|
+
// where every `getSession()` call inside the 90s EXPIRY_MARGIN_MS
|
|
4901
|
+
// window kept re-firing against the same broken refresh token during
|
|
4902
|
+
// outages. Concurrent callers already share `refreshingDeferred`; this
|
|
4903
|
+
// cache covers serial callers spaced across cooldown windows.
|
|
4904
|
+
//
|
|
4905
|
+
// Token-keyed so callers with a fresh refresh token (rotation pickup
|
|
4906
|
+
// from another tab, explicit `setSession`/`refreshSession({ refresh_token })`,
|
|
4907
|
+
// multi-account switch) bypass the cache and attempt a real refresh.
|
|
4908
|
+
if (
|
|
4909
|
+
this.lastRefreshFailure &&
|
|
4910
|
+
this.lastRefreshFailure.refreshToken === refreshToken &&
|
|
4911
|
+
Date.now() < this.lastRefreshFailure.expiresAt
|
|
4912
|
+
) {
|
|
4913
|
+
this._debug('#_callRefreshToken()', 'returning cached failure (cooldown active)')
|
|
4914
|
+
return this.lastRefreshFailure.result
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4918
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4919
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4920
|
+
const debugName = `#_callRefreshToken()`
|
|
4600
4921
|
|
|
4601
4922
|
this._debug(debugName, 'begin')
|
|
4602
4923
|
|
|
4603
4924
|
try {
|
|
4604
4925
|
this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
|
|
4605
4926
|
|
|
4927
|
+
// Snapshot storage before the fetch. The commit guard discards the
|
|
4928
|
+
// rotated tokens only when a non-null pre-fetch snapshot changed under
|
|
4929
|
+
// us — typical case: a concurrent `signOut` ran `_removeSession`, or
|
|
4930
|
+
// another tab's refresh rewrote the slot. Callers passing
|
|
4931
|
+
// externally-sourced tokens (SSR cookie handoff, multi-account
|
|
4932
|
+
// switching, `setSession`/`refreshSession({ refresh_token })`) may
|
|
4933
|
+
// start from a null snapshot OR from a non-null snapshot whose
|
|
4934
|
+
// refresh_token differs from the one they're hydrating; in both
|
|
4935
|
+
// cases the guard fires only when storage was *modified between
|
|
4936
|
+
// snapshots*, not when the input token disagrees with what's stored.
|
|
4937
|
+
const storedAtStart = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
4938
|
+
|
|
4606
4939
|
const { data, error } = await this._refreshAccessToken(refreshToken)
|
|
4607
4940
|
if (error) throw error
|
|
4608
4941
|
if (!data.session) throw new AuthSessionMissingError()
|
|
4609
4942
|
|
|
4943
|
+
const storedAfter = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
4944
|
+
const storageChangedUnderUs =
|
|
4945
|
+
storedAtStart !== null &&
|
|
4946
|
+
(storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token)
|
|
4947
|
+
|
|
4948
|
+
if (storageChangedUnderUs) {
|
|
4949
|
+
this._debug(
|
|
4950
|
+
debugName,
|
|
4951
|
+
'commit guard: storage changed since refresh started, discarding rotated tokens',
|
|
4952
|
+
{
|
|
4953
|
+
// Presence indicators only — never log refresh token fragments,
|
|
4954
|
+
// even partial. Logs may be forwarded to third-party services.
|
|
4955
|
+
startedWith: 'present',
|
|
4956
|
+
nowHolds: storedAfter ? 'replaced' : 'cleared',
|
|
4957
|
+
}
|
|
4958
|
+
)
|
|
4959
|
+
const discarded: CallRefreshTokenResult = {
|
|
4960
|
+
data: null,
|
|
4961
|
+
error: new AuthRefreshDiscardedError(),
|
|
4962
|
+
}
|
|
4963
|
+
this.refreshingDeferred.resolve(discarded)
|
|
4964
|
+
return discarded
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
// Second leg of the commit guard: close the TOCTOU window between the
|
|
4968
|
+
// synchronous `storageChangedUnderUs` check and the actual storage
|
|
4969
|
+
// writes inside `_saveSession`. A concurrent `signOut → _removeSession`
|
|
4970
|
+
// can land inside `_saveSession`'s `await setItemAsync(...)` yields and
|
|
4971
|
+
// clear storage just before we overwrite it. Capture the epoch BEFORE
|
|
4972
|
+
// the save and re-check after; if it advanced, undo the write directly
|
|
4973
|
+
// (do NOT call `_removeSession` — that would emit a duplicate
|
|
4974
|
+
// SIGNED_OUT for the concurrent signOut that already fired one).
|
|
4975
|
+
const epochBeforeSave = this._sessionRemovalEpoch
|
|
4976
|
+
|
|
4610
4977
|
await this._saveSession(data.session)
|
|
4978
|
+
|
|
4979
|
+
if (this._sessionRemovalEpoch !== epochBeforeSave) {
|
|
4980
|
+
this._debug(
|
|
4981
|
+
debugName,
|
|
4982
|
+
'commit guard (post-save): _removeSession ran during _saveSession, undoing write'
|
|
4983
|
+
)
|
|
4984
|
+
await removeItemAsync(this.storage, this.storageKey)
|
|
4985
|
+
if (this.userStorage) {
|
|
4986
|
+
await removeItemAsync(this.userStorage, this.storageKey + '-user')
|
|
4987
|
+
}
|
|
4988
|
+
const discarded: CallRefreshTokenResult = {
|
|
4989
|
+
data: null,
|
|
4990
|
+
error: new AuthRefreshDiscardedError(),
|
|
4991
|
+
}
|
|
4992
|
+
this.refreshingDeferred.resolve(discarded)
|
|
4993
|
+
return discarded
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4611
4996
|
await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
|
|
4612
4997
|
|
|
4613
4998
|
const result = { data: data.session, error: null }
|
|
4614
4999
|
|
|
5000
|
+
// Refresh succeeded — clear any cached failure so the next caller
|
|
5001
|
+
// (including the auto-refresh ticker) attempts a real refresh again.
|
|
5002
|
+
this.lastRefreshFailure = null
|
|
5003
|
+
|
|
4615
5004
|
this.refreshingDeferred.resolve(result)
|
|
4616
5005
|
|
|
4617
5006
|
return result
|
|
@@ -4622,7 +5011,40 @@ export default class GoTrueClient {
|
|
|
4622
5011
|
const result = { data: null, error }
|
|
4623
5012
|
|
|
4624
5013
|
if (!isAuthRetryableFetchError(error)) {
|
|
4625
|
-
|
|
5014
|
+
// Proactive vs reactive distinction: a refresh fires whenever
|
|
5015
|
+
// the access token is within EXPIRY_MARGIN_MS of expiry. If the
|
|
5016
|
+
// access token is *still valid* at this moment, the refresh was
|
|
5017
|
+
// proactive and the existing session is still usable until its
|
|
5018
|
+
// real expiry — destroying it now would log out a user whose
|
|
5019
|
+
// access token works. If the access token has actually expired,
|
|
5020
|
+
// the refresh token is the only credential left and it just got
|
|
5021
|
+
// rejected — the session is genuinely dead. `__loadSession`
|
|
5022
|
+
// mirrors this distinction on the read path so callers see the
|
|
5023
|
+
// preserved session instead of `session: null`.
|
|
5024
|
+
const storedNow = (await getItemAsync(this.storage, this.storageKey)) as Session | null
|
|
5025
|
+
const accessTokenStillValid = !!(
|
|
5026
|
+
storedNow?.expires_at && storedNow.expires_at * 1000 > Date.now()
|
|
5027
|
+
)
|
|
5028
|
+
|
|
5029
|
+
if (accessTokenStillValid) {
|
|
5030
|
+
this._debug(
|
|
5031
|
+
debugName,
|
|
5032
|
+
'proactive refresh failed, access token still valid — preserving session'
|
|
5033
|
+
)
|
|
5034
|
+
} else {
|
|
5035
|
+
await this._removeSession()
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
// Cache the failure so serial callers (and the next auto-refresh
|
|
5040
|
+
// tick) passing the same refresh token within the cooldown window
|
|
5041
|
+
// receive it synchronously instead of firing another `/token`
|
|
5042
|
+
// call. Set after the optional `_removeSession` above (which
|
|
5043
|
+
// clears the cache as part of teardown) so the cache survives.
|
|
5044
|
+
this.lastRefreshFailure = {
|
|
5045
|
+
refreshToken,
|
|
5046
|
+
result,
|
|
5047
|
+
expiresAt: Date.now() + REFRESH_FAILURE_COOLDOWN_MS,
|
|
4626
5048
|
}
|
|
4627
5049
|
|
|
4628
5050
|
this.refreshingDeferred?.resolve(result)
|
|
@@ -4643,6 +5065,20 @@ export default class GoTrueClient {
|
|
|
4643
5065
|
session: Session | null,
|
|
4644
5066
|
broadcast = true
|
|
4645
5067
|
) {
|
|
5068
|
+
if (this._pendingInitNotifications !== null && broadcast) {
|
|
5069
|
+
// We're inside initialize() before initializePromise has resolved, and
|
|
5070
|
+
// this notification originates from the init chain (_recoverAndRefresh),
|
|
5071
|
+
// which always fires with broadcast = true. Enqueue instead of firing so
|
|
5072
|
+
// that callbacks can safely call getSession() / getUser() without
|
|
5073
|
+
// deadlocking on initializePromise.
|
|
5074
|
+
//
|
|
5075
|
+
// Notifications with broadcast = false come from the incoming
|
|
5076
|
+
// BroadcastChannel handler (a cross-tab event), not the init chain, so
|
|
5077
|
+
// they are fired immediately to preserve multi-tab ordering.
|
|
5078
|
+
this._pendingInitNotifications.push({ event, session, broadcast })
|
|
5079
|
+
return
|
|
5080
|
+
}
|
|
5081
|
+
|
|
4646
5082
|
const debugName = `#_notifyAllSubscribers(${event})`
|
|
4647
5083
|
this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
|
|
4648
5084
|
|
|
@@ -4651,11 +5087,11 @@ export default class GoTrueClient {
|
|
|
4651
5087
|
this.broadcastChannel.postMessage({ event, session })
|
|
4652
5088
|
}
|
|
4653
5089
|
|
|
4654
|
-
const errors:
|
|
5090
|
+
const errors: unknown[] = []
|
|
4655
5091
|
const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => {
|
|
4656
5092
|
try {
|
|
4657
5093
|
await x.callback(event, session)
|
|
4658
|
-
} catch (e
|
|
5094
|
+
} catch (e) {
|
|
4659
5095
|
errors.push(e)
|
|
4660
5096
|
}
|
|
4661
5097
|
})
|
|
@@ -4683,7 +5119,6 @@ export default class GoTrueClient {
|
|
|
4683
5119
|
// _saveSession is always called whenever a new session has been acquired
|
|
4684
5120
|
// so we can safely suppress the warning returned by future getSession calls
|
|
4685
5121
|
this.suppressGetSessionWarning = true
|
|
4686
|
-
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
|
|
4687
5122
|
// Create a shallow copy to work with, to avoid mutating the original session object if it's used elsewhere
|
|
4688
5123
|
const sessionToProcess = { ...session }
|
|
4689
5124
|
|
|
@@ -4719,8 +5154,17 @@ export default class GoTrueClient {
|
|
|
4719
5154
|
}
|
|
4720
5155
|
|
|
4721
5156
|
private async _removeSession() {
|
|
5157
|
+
// Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
|
|
5158
|
+
// post-save check sees the increment whenever this method has started —
|
|
5159
|
+
// even if it hasn't finished. Pairs with the epoch check in
|
|
5160
|
+
// `_callRefreshToken`. See `_sessionRemovalEpoch` field doc.
|
|
5161
|
+
this._sessionRemovalEpoch += 1
|
|
4722
5162
|
this._debug('#_removeSession()')
|
|
4723
5163
|
|
|
5164
|
+
// The session is gone — no point holding on to a cached refresh failure
|
|
5165
|
+
// for a token that no longer exists. Synchronous, before any `await`.
|
|
5166
|
+
this.lastRefreshFailure = null
|
|
5167
|
+
|
|
4724
5168
|
this.suppressGetSessionWarning = false
|
|
4725
5169
|
|
|
4726
5170
|
await removeItemAsync(this.storage, this.storageKey)
|
|
@@ -4737,8 +5181,8 @@ export default class GoTrueClient {
|
|
|
4737
5181
|
/**
|
|
4738
5182
|
* Removes any registered visibilitychange callback.
|
|
4739
5183
|
*
|
|
4740
|
-
* {@
|
|
4741
|
-
* {@
|
|
5184
|
+
* {@link GoTrueClient.startAutoRefresh}
|
|
5185
|
+
* {@link GoTrueClient.stopAutoRefresh}
|
|
4742
5186
|
*/
|
|
4743
5187
|
private _removeVisibilityChangedCallback() {
|
|
4744
5188
|
this._debug('#_removeVisibilityChangedCallback()')
|
|
@@ -4756,7 +5200,7 @@ export default class GoTrueClient {
|
|
|
4756
5200
|
}
|
|
4757
5201
|
|
|
4758
5202
|
/**
|
|
4759
|
-
* This is the private implementation of {@link
|
|
5203
|
+
* This is the private implementation of {@link GoTrueClient.startAutoRefresh}. Use this
|
|
4760
5204
|
* within the library.
|
|
4761
5205
|
*/
|
|
4762
5206
|
private async _startAutoRefresh() {
|
|
@@ -4802,7 +5246,7 @@ export default class GoTrueClient {
|
|
|
4802
5246
|
}
|
|
4803
5247
|
|
|
4804
5248
|
/**
|
|
4805
|
-
* This is the private implementation of {@link
|
|
5249
|
+
* This is the private implementation of {@link GoTrueClient.stopAutoRefresh}. Use this
|
|
4806
5250
|
* within the library.
|
|
4807
5251
|
*/
|
|
4808
5252
|
private async _stopAutoRefresh() {
|
|
@@ -4843,7 +5287,7 @@ export default class GoTrueClient {
|
|
|
4843
5287
|
* platform's foreground indication mechanism and call these methods
|
|
4844
5288
|
* appropriately to conserve resources.
|
|
4845
5289
|
*
|
|
4846
|
-
* {@
|
|
5290
|
+
* {@link GoTrueClient.stopAutoRefresh}
|
|
4847
5291
|
*
|
|
4848
5292
|
* @category Auth
|
|
4849
5293
|
*
|
|
@@ -4878,7 +5322,7 @@ export default class GoTrueClient {
|
|
|
4878
5322
|
* If you call this method any managed visibility change callback will be
|
|
4879
5323
|
* removed and you must manage visibility changes on your own.
|
|
4880
5324
|
*
|
|
4881
|
-
* See {@link
|
|
5325
|
+
* See {@link GoTrueClient.startAutoRefresh} for more details.
|
|
4882
5326
|
*
|
|
4883
5327
|
* @category Auth
|
|
4884
5328
|
*
|
|
@@ -4907,58 +5351,144 @@ export default class GoTrueClient {
|
|
|
4907
5351
|
await this._stopAutoRefresh()
|
|
4908
5352
|
}
|
|
4909
5353
|
|
|
5354
|
+
/**
|
|
5355
|
+
* Tears down the client's background work: stops the auto-refresh interval,
|
|
5356
|
+
* removes the `visibilitychange` listener, closes the cross-tab
|
|
5357
|
+
* `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers.
|
|
5358
|
+
*
|
|
5359
|
+
* Call this from cleanup hooks when the client is being replaced before
|
|
5360
|
+
* its JS realm is destroyed. React Strict Mode and HMR are the common
|
|
5361
|
+
* cases. Any in-flight `fetch` calls continue to completion and may still
|
|
5362
|
+
* write to storage; dispose doesn't abort them or erase storage.
|
|
5363
|
+
*
|
|
5364
|
+
* Lifecycle caveat: because in-flight refreshes are not aborted, a
|
|
5365
|
+
* disposed instance can still persist a rotated session to storage after
|
|
5366
|
+
* `dispose()` returns. A subsequent `createClient` against the same
|
|
5367
|
+
* `storageKey` will pick up that session on its next read. If you need
|
|
5368
|
+
* strict isolation between client lifecycles, await any pending auth
|
|
5369
|
+
* operation before calling `dispose()` (or change the `storageKey` for
|
|
5370
|
+
* the replacement client).
|
|
5371
|
+
*
|
|
5372
|
+
* Safe to call repeatedly.
|
|
5373
|
+
*
|
|
5374
|
+
* @category Auth
|
|
5375
|
+
*
|
|
5376
|
+
* @example Cleanup on React unmount
|
|
5377
|
+
* ```ts
|
|
5378
|
+
* useEffect(() => {
|
|
5379
|
+
* const client = createClient(...)
|
|
5380
|
+
* return () => { client.auth.dispose() }
|
|
5381
|
+
* }, [])
|
|
5382
|
+
* ```
|
|
5383
|
+
*/
|
|
5384
|
+
async dispose(): Promise<void> {
|
|
5385
|
+
this._removeVisibilityChangedCallback()
|
|
5386
|
+
await this._stopAutoRefresh()
|
|
5387
|
+
this.broadcastChannel?.close()
|
|
5388
|
+
this.broadcastChannel = null
|
|
5389
|
+
this.stateChangeEmitters.clear()
|
|
5390
|
+
}
|
|
5391
|
+
|
|
4910
5392
|
/**
|
|
4911
5393
|
* Runs the auto refresh token tick.
|
|
4912
5394
|
*/
|
|
4913
5395
|
private async _autoRefreshTokenTick() {
|
|
4914
5396
|
this._debug('#_autoRefreshTokenTick()', 'begin')
|
|
4915
5397
|
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
5398
|
+
if (this.lock != null) {
|
|
5399
|
+
// TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which
|
|
5400
|
+
// throws `LockAcquireTimeoutError` immediately if the lock is held —
|
|
5401
|
+
// that's the fail-fast skip path that lets the tick bail out instead
|
|
5402
|
+
// of queuing behind a long-running operation.
|
|
5403
|
+
try {
|
|
5404
|
+
await this._acquireLock(0, async () => {
|
|
4921
5405
|
try {
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
5406
|
+
const now = Date.now()
|
|
5407
|
+
try {
|
|
5408
|
+
return await this._useSession(async (result) => {
|
|
5409
|
+
const {
|
|
5410
|
+
data: { session },
|
|
5411
|
+
} = result
|
|
5412
|
+
|
|
5413
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
5414
|
+
this._debug('#_autoRefreshTokenTick()', 'no session')
|
|
5415
|
+
return
|
|
5416
|
+
}
|
|
4931
5417
|
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
)
|
|
5418
|
+
const expiresInTicks = Math.floor(
|
|
5419
|
+
(session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
|
|
5420
|
+
)
|
|
4936
5421
|
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
5422
|
+
this._debug(
|
|
5423
|
+
'#_autoRefreshTokenTick()',
|
|
5424
|
+
`access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
|
|
5425
|
+
)
|
|
4941
5426
|
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
5427
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
5428
|
+
await this._callRefreshToken(session.refresh_token)
|
|
5429
|
+
}
|
|
5430
|
+
})
|
|
5431
|
+
} catch (e) {
|
|
5432
|
+
console.error(
|
|
5433
|
+
'Auto refresh tick failed with error. This is likely a transient error.',
|
|
5434
|
+
e
|
|
5435
|
+
)
|
|
5436
|
+
}
|
|
5437
|
+
} finally {
|
|
5438
|
+
this._debug('#_autoRefreshTokenTick()', 'end')
|
|
4951
5439
|
}
|
|
4952
|
-
}
|
|
4953
|
-
|
|
5440
|
+
})
|
|
5441
|
+
} catch (e) {
|
|
5442
|
+
if (e instanceof LockAcquireTimeoutError) {
|
|
5443
|
+
this._debug('auto refresh token tick lock not available')
|
|
5444
|
+
} else {
|
|
5445
|
+
throw e
|
|
4954
5446
|
}
|
|
4955
|
-
})
|
|
4956
|
-
} catch (e: any) {
|
|
4957
|
-
if (e.isAcquireTimeout || e instanceof LockAcquireTimeoutError) {
|
|
4958
|
-
this._debug('auto refresh token tick lock not available')
|
|
4959
|
-
} else {
|
|
4960
|
-
throw e
|
|
4961
5447
|
}
|
|
5448
|
+
return
|
|
5449
|
+
}
|
|
5450
|
+
|
|
5451
|
+
// Lockless default: skip if a refresh is already in flight.
|
|
5452
|
+
// `_callRefreshToken` also dedupes via the same field; this is just a
|
|
5453
|
+
// fast-path skip to avoid an unnecessary storage read.
|
|
5454
|
+
if (this.refreshingDeferred !== null) {
|
|
5455
|
+
this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping')
|
|
5456
|
+
return
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5459
|
+
try {
|
|
5460
|
+
const now = Date.now()
|
|
5461
|
+
|
|
5462
|
+
try {
|
|
5463
|
+
await this._useSession(async (result) => {
|
|
5464
|
+
const {
|
|
5465
|
+
data: { session },
|
|
5466
|
+
} = result
|
|
5467
|
+
|
|
5468
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
5469
|
+
this._debug('#_autoRefreshTokenTick()', 'no session')
|
|
5470
|
+
return
|
|
5471
|
+
}
|
|
5472
|
+
|
|
5473
|
+
// session will expire in this many ticks (or has already expired if <= 0)
|
|
5474
|
+
const expiresInTicks = Math.floor(
|
|
5475
|
+
(session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
|
|
5476
|
+
)
|
|
5477
|
+
|
|
5478
|
+
this._debug(
|
|
5479
|
+
'#_autoRefreshTokenTick()',
|
|
5480
|
+
`access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
|
|
5481
|
+
)
|
|
5482
|
+
|
|
5483
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
5484
|
+
await this._callRefreshToken(session.refresh_token)
|
|
5485
|
+
}
|
|
5486
|
+
})
|
|
5487
|
+
} catch (e) {
|
|
5488
|
+
console.error('Auto refresh tick failed with error. This is likely a transient error.', e)
|
|
5489
|
+
}
|
|
5490
|
+
} finally {
|
|
5491
|
+
this._debug('#_autoRefreshTokenTick()', 'end')
|
|
4962
5492
|
}
|
|
4963
5493
|
}
|
|
4964
5494
|
|
|
@@ -5015,24 +5545,29 @@ export default class GoTrueClient {
|
|
|
5015
5545
|
if (!calledFromInitialize) {
|
|
5016
5546
|
// called when the visibility has changed, i.e. the browser
|
|
5017
5547
|
// transitioned from hidden -> visible so we need to see if the session
|
|
5018
|
-
// should be recovered
|
|
5019
|
-
// the lock first asynchronously
|
|
5548
|
+
// should be recovered
|
|
5020
5549
|
await this.initializePromise
|
|
5021
5550
|
|
|
5022
|
-
|
|
5551
|
+
if (this.lock != null) {
|
|
5552
|
+
// TODO(v3): remove legacy lock path
|
|
5553
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
5554
|
+
if (document.visibilityState !== 'visible') {
|
|
5555
|
+
this._debug(
|
|
5556
|
+
methodName,
|
|
5557
|
+
'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
|
|
5558
|
+
)
|
|
5559
|
+
return
|
|
5560
|
+
}
|
|
5561
|
+
await this._recoverAndRefresh()
|
|
5562
|
+
})
|
|
5563
|
+
} else {
|
|
5023
5564
|
if (document.visibilityState !== 'visible') {
|
|
5024
|
-
this._debug(
|
|
5025
|
-
methodName,
|
|
5026
|
-
'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
|
|
5027
|
-
)
|
|
5028
|
-
|
|
5029
|
-
// visibility has changed while waiting for the lock, abort
|
|
5565
|
+
this._debug(methodName, 'visibilityState is no longer visible, skipping recovery')
|
|
5030
5566
|
return
|
|
5031
5567
|
}
|
|
5032
|
-
|
|
5033
5568
|
// recover the session
|
|
5034
5569
|
await this._recoverAndRefresh()
|
|
5035
|
-
}
|
|
5570
|
+
}
|
|
5036
5571
|
}
|
|
5037
5572
|
} else if (document.visibilityState === 'hidden') {
|
|
5038
5573
|
if (this.autoRefreshToken) {
|
|
@@ -5109,7 +5644,7 @@ export default class GoTrueClient {
|
|
|
5109
5644
|
}
|
|
5110
5645
|
|
|
5111
5646
|
/**
|
|
5112
|
-
* {@
|
|
5647
|
+
* {@link GoTrueMFAApi#enroll}
|
|
5113
5648
|
*/
|
|
5114
5649
|
private async _enroll(params: MFAEnrollTOTPParams): Promise<AuthMFAEnrollTOTPResponse>
|
|
5115
5650
|
private async _enroll(params: MFAEnrollPhoneParams): Promise<AuthMFAEnrollPhoneResponse>
|
|
@@ -5156,7 +5691,7 @@ export default class GoTrueClient {
|
|
|
5156
5691
|
}
|
|
5157
5692
|
|
|
5158
5693
|
/**
|
|
5159
|
-
* {@
|
|
5694
|
+
* {@link GoTrueMFAApi#verify}
|
|
5160
5695
|
*/
|
|
5161
5696
|
private async _verify(params: MFAVerifyTOTPParams): Promise<AuthMFAVerifyResponse>
|
|
5162
5697
|
private async _verify(params: MFAVerifyPhoneParams): Promise<AuthMFAVerifyResponse>
|
|
@@ -5164,7 +5699,7 @@ export default class GoTrueClient {
|
|
|
5164
5699
|
params: MFAVerifyWebauthnParams<T>
|
|
5165
5700
|
): Promise<AuthMFAVerifyResponse>
|
|
5166
5701
|
private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
|
|
5167
|
-
|
|
5702
|
+
const run = async (): Promise<AuthMFAVerifyResponse> => {
|
|
5168
5703
|
try {
|
|
5169
5704
|
return await this._useSession(async (result) => {
|
|
5170
5705
|
const { data: sessionData, error: sessionError } = result
|
|
@@ -5235,11 +5770,17 @@ export default class GoTrueClient {
|
|
|
5235
5770
|
}
|
|
5236
5771
|
throw error
|
|
5237
5772
|
}
|
|
5238
|
-
}
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
if (this.lock != null) {
|
|
5776
|
+
// TODO(v3): remove legacy lock path
|
|
5777
|
+
return this._acquireLock(this.lockAcquireTimeout, run)
|
|
5778
|
+
}
|
|
5779
|
+
return run()
|
|
5239
5780
|
}
|
|
5240
5781
|
|
|
5241
5782
|
/**
|
|
5242
|
-
* {@
|
|
5783
|
+
* {@link GoTrueMFAApi#challenge}
|
|
5243
5784
|
*/
|
|
5244
5785
|
private async _challenge(
|
|
5245
5786
|
params: MFAChallengeTOTPParams
|
|
@@ -5251,7 +5792,7 @@ export default class GoTrueClient {
|
|
|
5251
5792
|
params: MFAChallengeWebauthnParams
|
|
5252
5793
|
): Promise<Prettify<AuthMFAChallengeWebauthnResponse>>
|
|
5253
5794
|
private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
|
|
5254
|
-
|
|
5795
|
+
const run = async (): Promise<AuthMFAChallengeResponse> => {
|
|
5255
5796
|
try {
|
|
5256
5797
|
return await this._useSession(async (result) => {
|
|
5257
5798
|
const { data: sessionData, error: sessionError } = result
|
|
@@ -5324,18 +5865,21 @@ export default class GoTrueClient {
|
|
|
5324
5865
|
}
|
|
5325
5866
|
throw error
|
|
5326
5867
|
}
|
|
5327
|
-
}
|
|
5868
|
+
}
|
|
5869
|
+
|
|
5870
|
+
if (this.lock != null) {
|
|
5871
|
+
// TODO(v3): remove legacy lock path
|
|
5872
|
+
return this._acquireLock(this.lockAcquireTimeout, run)
|
|
5873
|
+
}
|
|
5874
|
+
return run()
|
|
5328
5875
|
}
|
|
5329
5876
|
|
|
5330
5877
|
/**
|
|
5331
|
-
* {@
|
|
5878
|
+
* {@link GoTrueMFAApi#challengeAndVerify}
|
|
5332
5879
|
*/
|
|
5333
5880
|
private async _challengeAndVerify(
|
|
5334
5881
|
params: MFAChallengeAndVerifyParams
|
|
5335
5882
|
): Promise<AuthMFAVerifyResponse> {
|
|
5336
|
-
// both _challenge and _verify independently acquire the lock, so no need
|
|
5337
|
-
// to acquire it here
|
|
5338
|
-
|
|
5339
5883
|
const { data: challengeData, error: challengeError } = await this._challenge({
|
|
5340
5884
|
factorId: params.factorId,
|
|
5341
5885
|
})
|
|
@@ -5351,10 +5895,9 @@ export default class GoTrueClient {
|
|
|
5351
5895
|
}
|
|
5352
5896
|
|
|
5353
5897
|
/**
|
|
5354
|
-
* {@
|
|
5898
|
+
* {@link GoTrueMFAApi#listFactors}
|
|
5355
5899
|
*/
|
|
5356
5900
|
private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
|
|
5357
|
-
// use #getUser instead of #_getUser as the former acquires a lock
|
|
5358
5901
|
const {
|
|
5359
5902
|
data: { user },
|
|
5360
5903
|
error: userError,
|
|
@@ -5385,7 +5928,7 @@ export default class GoTrueClient {
|
|
|
5385
5928
|
}
|
|
5386
5929
|
|
|
5387
5930
|
/**
|
|
5388
|
-
* {@
|
|
5931
|
+
* {@link GoTrueMFAApi#getAuthenticatorAssuranceLevel}
|
|
5389
5932
|
*/
|
|
5390
5933
|
private async _getAuthenticatorAssuranceLevel(
|
|
5391
5934
|
jwt?: string
|
|
@@ -5733,15 +6276,15 @@ export default class GoTrueClient {
|
|
|
5733
6276
|
* Extracts the JWT claims present in the access token by first verifying the
|
|
5734
6277
|
* JWT against the server's JSON Web Key Set endpoint
|
|
5735
6278
|
* `/.well-known/jwks.json` which is often cached, resulting in significantly
|
|
5736
|
-
* faster responses. Prefer this method over {@link
|
|
6279
|
+
* faster responses. Prefer this method over {@link GoTrueClient.getUser} which always
|
|
5737
6280
|
* sends a request to the Auth server for each JWT.
|
|
5738
6281
|
*
|
|
5739
6282
|
* If the project is not using an asymmetric JWT signing key (like ECC or
|
|
5740
|
-
* RSA) it always sends a request to the Auth server (similar to
|
|
5741
|
-
*
|
|
6283
|
+
* RSA) it always sends a request to the Auth server (similar to
|
|
6284
|
+
* {@link GoTrueClient.getUser}) to verify the JWT.
|
|
5742
6285
|
*
|
|
5743
6286
|
* @param jwt An optional specific JWT you wish to verify, not the one you
|
|
5744
|
-
* can obtain from {@link
|
|
6287
|
+
* can obtain from {@link GoTrueClient.getSession}.
|
|
5745
6288
|
* @param options Various additional options that allow you to customize the
|
|
5746
6289
|
* behavior of this method.
|
|
5747
6290
|
*
|
|
@@ -5834,8 +6377,13 @@ export default class GoTrueClient {
|
|
|
5834
6377
|
} = decodeJWT(token)
|
|
5835
6378
|
|
|
5836
6379
|
if (!options?.allowExpired) {
|
|
5837
|
-
// Reject expired JWTs should only happen if jwt argument was passed
|
|
5838
|
-
|
|
6380
|
+
// Reject expired JWTs should only happen if jwt argument was passed.
|
|
6381
|
+
// Rethrow as AuthInvalidJwtError so the outer catch converts it to { data, error }.
|
|
6382
|
+
try {
|
|
6383
|
+
validateExp(payload.exp)
|
|
6384
|
+
} catch (e) {
|
|
6385
|
+
throw new AuthInvalidJwtError(e instanceof Error ? e.message : 'JWT validation failed')
|
|
6386
|
+
}
|
|
5839
6387
|
}
|
|
5840
6388
|
|
|
5841
6389
|
const signingKey =
|
|
@@ -5898,4 +6446,392 @@ export default class GoTrueClient {
|
|
|
5898
6446
|
throw error
|
|
5899
6447
|
}
|
|
5900
6448
|
}
|
|
6449
|
+
|
|
6450
|
+
// --- Passkey Methods ---
|
|
6451
|
+
|
|
6452
|
+
/**
|
|
6453
|
+
* Sign in with a passkey. Handles the full WebAuthn ceremony:
|
|
6454
|
+
* 1. Fetches authentication challenge from server
|
|
6455
|
+
* 2. Prompts user via navigator.credentials.get()
|
|
6456
|
+
* 3. Verifies credential with server and creates session
|
|
6457
|
+
*
|
|
6458
|
+
* Requires `auth.experimental.passkey: true`.
|
|
6459
|
+
*
|
|
6460
|
+
* @category Auth
|
|
6461
|
+
*/
|
|
6462
|
+
async signInWithPasskey(
|
|
6463
|
+
credentials?: SignInWithPasskeyCredentials
|
|
6464
|
+
): Promise<AuthPasskeyAuthenticationVerifyResponse> {
|
|
6465
|
+
assertPasskeyExperimentalEnabled(this.experimental)
|
|
6466
|
+
try {
|
|
6467
|
+
if (!browserSupportsWebAuthn()) {
|
|
6468
|
+
return this._returnResult({
|
|
6469
|
+
data: null,
|
|
6470
|
+
error: new AuthUnknownError('Browser does not support WebAuthn', null),
|
|
6471
|
+
})
|
|
6472
|
+
}
|
|
6473
|
+
|
|
6474
|
+
// 1. Get challenge options from server
|
|
6475
|
+
const { data: options, error: optionsError } = await this._startPasskeyAuthentication({
|
|
6476
|
+
options: { captchaToken: credentials?.options?.captchaToken },
|
|
6477
|
+
})
|
|
6478
|
+
if (optionsError || !options) {
|
|
6479
|
+
return this._returnResult({ data: null, error: optionsError })
|
|
6480
|
+
}
|
|
6481
|
+
|
|
6482
|
+
// 2. Deserialize and prompt user via browser WebAuthn API
|
|
6483
|
+
const publicKeyOptions = deserializeCredentialRequestOptions(options.options)
|
|
6484
|
+
const signal = credentials?.options?.signal ?? webAuthnAbortService.createNewAbortSignal()
|
|
6485
|
+
const { data: credential, error: credentialError } = await getCredential({
|
|
6486
|
+
publicKey: publicKeyOptions,
|
|
6487
|
+
signal,
|
|
6488
|
+
})
|
|
6489
|
+
if (credentialError || !credential) {
|
|
6490
|
+
return this._returnResult({
|
|
6491
|
+
data: null,
|
|
6492
|
+
error: credentialError ?? new AuthUnknownError('WebAuthn ceremony failed', null),
|
|
6493
|
+
})
|
|
6494
|
+
}
|
|
6495
|
+
|
|
6496
|
+
// 3. Serialize and verify with server
|
|
6497
|
+
const serialized = serializeCredentialRequestResponse(credential)
|
|
6498
|
+
return this._verifyPasskeyAuthentication({
|
|
6499
|
+
challengeId: options.challenge_id,
|
|
6500
|
+
credential: serialized,
|
|
6501
|
+
})
|
|
6502
|
+
} catch (error) {
|
|
6503
|
+
if (isAuthError(error)) {
|
|
6504
|
+
return this._returnResult({ data: null, error })
|
|
6505
|
+
}
|
|
6506
|
+
throw error
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
|
|
6510
|
+
/**
|
|
6511
|
+
* Register a passkey for the current authenticated user. Handles the full WebAuthn ceremony:
|
|
6512
|
+
* 1. Fetches registration challenge from server
|
|
6513
|
+
* 2. Prompts user via navigator.credentials.create()
|
|
6514
|
+
* 3. Verifies credential with server
|
|
6515
|
+
*
|
|
6516
|
+
* Requires an active session. Requires `auth.experimental.passkey: true`.
|
|
6517
|
+
*
|
|
6518
|
+
* @category Auth
|
|
6519
|
+
*/
|
|
6520
|
+
async registerPasskey(
|
|
6521
|
+
credentials?: RegisterPasskeyCredentials
|
|
6522
|
+
): Promise<AuthPasskeyRegistrationVerifyResponse> {
|
|
6523
|
+
assertPasskeyExperimentalEnabled(this.experimental)
|
|
6524
|
+
try {
|
|
6525
|
+
if (!browserSupportsWebAuthn()) {
|
|
6526
|
+
return this._returnResult({
|
|
6527
|
+
data: null,
|
|
6528
|
+
error: new AuthUnknownError('Browser does not support WebAuthn', null),
|
|
6529
|
+
})
|
|
6530
|
+
}
|
|
6531
|
+
|
|
6532
|
+
// 1. Get challenge options from server
|
|
6533
|
+
const { data: options, error: optionsError } = await this._startPasskeyRegistration()
|
|
6534
|
+
if (optionsError || !options) {
|
|
6535
|
+
return this._returnResult({ data: null, error: optionsError })
|
|
6536
|
+
}
|
|
6537
|
+
|
|
6538
|
+
// 2. Deserialize and prompt user via browser WebAuthn API
|
|
6539
|
+
const publicKeyOptions = deserializeCredentialCreationOptions(options.options)
|
|
6540
|
+
const signal = credentials?.options?.signal ?? webAuthnAbortService.createNewAbortSignal()
|
|
6541
|
+
const { data: credential, error: credentialError } = await createCredential({
|
|
6542
|
+
publicKey: publicKeyOptions,
|
|
6543
|
+
signal,
|
|
6544
|
+
})
|
|
6545
|
+
if (credentialError || !credential) {
|
|
6546
|
+
return this._returnResult({
|
|
6547
|
+
data: null,
|
|
6548
|
+
error: credentialError ?? new AuthUnknownError('WebAuthn ceremony failed', null),
|
|
6549
|
+
})
|
|
6550
|
+
}
|
|
6551
|
+
|
|
6552
|
+
// 3. Serialize and verify with server
|
|
6553
|
+
const serialized = serializeCredentialCreationResponse(credential)
|
|
6554
|
+
return this._verifyPasskeyRegistration({
|
|
6555
|
+
challengeId: options.challenge_id,
|
|
6556
|
+
credential: serialized,
|
|
6557
|
+
})
|
|
6558
|
+
} catch (error) {
|
|
6559
|
+
if (isAuthError(error)) {
|
|
6560
|
+
return this._returnResult({ data: null, error })
|
|
6561
|
+
}
|
|
6562
|
+
throw error
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
|
|
6566
|
+
/**
|
|
6567
|
+
* Start passkey registration for the current authenticated user.
|
|
6568
|
+
* Returns WebAuthn credential creation options to pass to navigator.credentials.create().
|
|
6569
|
+
*/
|
|
6570
|
+
private async _startPasskeyRegistration(): Promise<AuthPasskeyRegistrationOptionsResponse> {
|
|
6571
|
+
assertPasskeyExperimentalEnabled(this.experimental)
|
|
6572
|
+
try {
|
|
6573
|
+
return await this._useSession(async (result) => {
|
|
6574
|
+
const {
|
|
6575
|
+
data: { session },
|
|
6576
|
+
error: sessionError,
|
|
6577
|
+
} = result
|
|
6578
|
+
if (sessionError) {
|
|
6579
|
+
return this._returnResult({ data: null, error: sessionError })
|
|
6580
|
+
}
|
|
6581
|
+
if (!session) {
|
|
6582
|
+
return this._returnResult({ data: null, error: new AuthSessionMissingError() })
|
|
6583
|
+
}
|
|
6584
|
+
const { data, error } = await _request(
|
|
6585
|
+
this.fetch,
|
|
6586
|
+
'POST',
|
|
6587
|
+
`${this.url}/passkeys/registration/options`,
|
|
6588
|
+
{
|
|
6589
|
+
headers: this.headers,
|
|
6590
|
+
jwt: session.access_token,
|
|
6591
|
+
body: {},
|
|
6592
|
+
}
|
|
6593
|
+
)
|
|
6594
|
+
if (error) {
|
|
6595
|
+
return this._returnResult({ data: null, error })
|
|
6596
|
+
}
|
|
6597
|
+
return this._returnResult({ data, error: null })
|
|
6598
|
+
})
|
|
6599
|
+
} catch (error) {
|
|
6600
|
+
if (isAuthError(error)) {
|
|
6601
|
+
return this._returnResult({ data: null, error })
|
|
6602
|
+
}
|
|
6603
|
+
throw error
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
|
|
6607
|
+
/**
|
|
6608
|
+
* Verify passkey registration with the credential response.
|
|
6609
|
+
* The credentialResponse should be the serialized output of navigator.credentials.create().
|
|
6610
|
+
*/
|
|
6611
|
+
private async _verifyPasskeyRegistration(
|
|
6612
|
+
params: VerifyPasskeyRegistrationParams
|
|
6613
|
+
): Promise<AuthPasskeyRegistrationVerifyResponse> {
|
|
6614
|
+
assertPasskeyExperimentalEnabled(this.experimental)
|
|
6615
|
+
try {
|
|
6616
|
+
return await this._useSession(async (result) => {
|
|
6617
|
+
const {
|
|
6618
|
+
data: { session },
|
|
6619
|
+
error: sessionError,
|
|
6620
|
+
} = result
|
|
6621
|
+
if (sessionError) {
|
|
6622
|
+
return this._returnResult({ data: null, error: sessionError })
|
|
6623
|
+
}
|
|
6624
|
+
if (!session) {
|
|
6625
|
+
return this._returnResult({ data: null, error: new AuthSessionMissingError() })
|
|
6626
|
+
}
|
|
6627
|
+
const { data, error } = await _request(
|
|
6628
|
+
this.fetch,
|
|
6629
|
+
'POST',
|
|
6630
|
+
`${this.url}/passkeys/registration/verify`,
|
|
6631
|
+
{
|
|
6632
|
+
headers: this.headers,
|
|
6633
|
+
jwt: session.access_token,
|
|
6634
|
+
body: {
|
|
6635
|
+
challenge_id: params.challengeId,
|
|
6636
|
+
credential: params.credential,
|
|
6637
|
+
},
|
|
6638
|
+
}
|
|
6639
|
+
)
|
|
6640
|
+
if (error) {
|
|
6641
|
+
return this._returnResult({ data: null, error })
|
|
6642
|
+
}
|
|
6643
|
+
return this._returnResult({ data, error: null })
|
|
6644
|
+
})
|
|
6645
|
+
} catch (error) {
|
|
6646
|
+
if (isAuthError(error)) {
|
|
6647
|
+
return this._returnResult({ data: null, error })
|
|
6648
|
+
}
|
|
6649
|
+
throw error
|
|
6650
|
+
}
|
|
6651
|
+
}
|
|
6652
|
+
|
|
6653
|
+
/**
|
|
6654
|
+
* Start passkey authentication.
|
|
6655
|
+
* Returns WebAuthn credential request options to pass to navigator.credentials.get().
|
|
6656
|
+
*/
|
|
6657
|
+
private async _startPasskeyAuthentication(
|
|
6658
|
+
params?: StartPasskeyAuthenticationParams
|
|
6659
|
+
): Promise<AuthPasskeyAuthenticationOptionsResponse> {
|
|
6660
|
+
assertPasskeyExperimentalEnabled(this.experimental)
|
|
6661
|
+
try {
|
|
6662
|
+
const { data, error } = await _request(
|
|
6663
|
+
this.fetch,
|
|
6664
|
+
'POST',
|
|
6665
|
+
`${this.url}/passkeys/authentication/options`,
|
|
6666
|
+
{
|
|
6667
|
+
headers: this.headers,
|
|
6668
|
+
body: {
|
|
6669
|
+
gotrue_meta_security: { captcha_token: params?.options?.captchaToken },
|
|
6670
|
+
},
|
|
6671
|
+
}
|
|
6672
|
+
)
|
|
6673
|
+
if (error) {
|
|
6674
|
+
return this._returnResult({ data: null, error })
|
|
6675
|
+
}
|
|
6676
|
+
return this._returnResult({ data, error: null })
|
|
6677
|
+
} catch (error) {
|
|
6678
|
+
if (isAuthError(error)) {
|
|
6679
|
+
return this._returnResult({ data: null, error })
|
|
6680
|
+
}
|
|
6681
|
+
throw error
|
|
6682
|
+
}
|
|
6683
|
+
}
|
|
6684
|
+
|
|
6685
|
+
/**
|
|
6686
|
+
* Verify passkey authentication and create a session.
|
|
6687
|
+
* The credential should be the serialized output of navigator.credentials.get().
|
|
6688
|
+
*/
|
|
6689
|
+
private async _verifyPasskeyAuthentication(
|
|
6690
|
+
params: VerifyPasskeyAuthenticationParams
|
|
6691
|
+
): Promise<AuthPasskeyAuthenticationVerifyResponse> {
|
|
6692
|
+
assertPasskeyExperimentalEnabled(this.experimental)
|
|
6693
|
+
try {
|
|
6694
|
+
const { data, error } = await _request(
|
|
6695
|
+
this.fetch,
|
|
6696
|
+
'POST',
|
|
6697
|
+
`${this.url}/passkeys/authentication/verify`,
|
|
6698
|
+
{
|
|
6699
|
+
headers: this.headers,
|
|
6700
|
+
body: {
|
|
6701
|
+
challenge_id: params.challengeId,
|
|
6702
|
+
credential: params.credential,
|
|
6703
|
+
},
|
|
6704
|
+
xform: _sessionResponse,
|
|
6705
|
+
}
|
|
6706
|
+
)
|
|
6707
|
+
if (error) {
|
|
6708
|
+
return this._returnResult({ data: null, error })
|
|
6709
|
+
}
|
|
6710
|
+
if (data.session) {
|
|
6711
|
+
await this._saveSession(data.session)
|
|
6712
|
+
await this._notifyAllSubscribers('SIGNED_IN', data.session)
|
|
6713
|
+
}
|
|
6714
|
+
return this._returnResult({ data, error: null })
|
|
6715
|
+
} catch (error) {
|
|
6716
|
+
if (isAuthError(error)) {
|
|
6717
|
+
return this._returnResult({ data: null, error })
|
|
6718
|
+
}
|
|
6719
|
+
throw error
|
|
6720
|
+
}
|
|
6721
|
+
}
|
|
6722
|
+
|
|
6723
|
+
/**
|
|
6724
|
+
* List all passkeys for the current user.
|
|
6725
|
+
*/
|
|
6726
|
+
private async _listPasskeys(): Promise<AuthPasskeyListResponse> {
|
|
6727
|
+
assertPasskeyExperimentalEnabled(this.experimental)
|
|
6728
|
+
try {
|
|
6729
|
+
return await this._useSession(async (result) => {
|
|
6730
|
+
const {
|
|
6731
|
+
data: { session },
|
|
6732
|
+
error: sessionError,
|
|
6733
|
+
} = result
|
|
6734
|
+
if (sessionError) {
|
|
6735
|
+
return this._returnResult({ data: null, error: sessionError })
|
|
6736
|
+
}
|
|
6737
|
+
if (!session) {
|
|
6738
|
+
return this._returnResult({ data: null, error: new AuthSessionMissingError() })
|
|
6739
|
+
}
|
|
6740
|
+
const { data, error } = await _request(this.fetch, 'GET', `${this.url}/passkeys`, {
|
|
6741
|
+
headers: this.headers,
|
|
6742
|
+
jwt: session.access_token,
|
|
6743
|
+
xform: (data: any) => ({ data, error: null }),
|
|
6744
|
+
})
|
|
6745
|
+
if (error) {
|
|
6746
|
+
return this._returnResult({ data: null, error })
|
|
6747
|
+
}
|
|
6748
|
+
return this._returnResult({ data, error: null })
|
|
6749
|
+
})
|
|
6750
|
+
} catch (error) {
|
|
6751
|
+
if (isAuthError(error)) {
|
|
6752
|
+
return this._returnResult({ data: null, error })
|
|
6753
|
+
}
|
|
6754
|
+
throw error
|
|
6755
|
+
}
|
|
6756
|
+
}
|
|
6757
|
+
|
|
6758
|
+
/**
|
|
6759
|
+
* Update a passkey.
|
|
6760
|
+
*/
|
|
6761
|
+
private async _updatePasskey(params: PasskeyUpdateParams): Promise<AuthPasskeyUpdateResponse> {
|
|
6762
|
+
assertPasskeyExperimentalEnabled(this.experimental)
|
|
6763
|
+
try {
|
|
6764
|
+
return await this._useSession(async (result) => {
|
|
6765
|
+
const {
|
|
6766
|
+
data: { session },
|
|
6767
|
+
error: sessionError,
|
|
6768
|
+
} = result
|
|
6769
|
+
if (sessionError) {
|
|
6770
|
+
return this._returnResult({ data: null, error: sessionError })
|
|
6771
|
+
}
|
|
6772
|
+
if (!session) {
|
|
6773
|
+
return this._returnResult({ data: null, error: new AuthSessionMissingError() })
|
|
6774
|
+
}
|
|
6775
|
+
const { data, error } = await _request(
|
|
6776
|
+
this.fetch,
|
|
6777
|
+
'PATCH',
|
|
6778
|
+
`${this.url}/passkeys/${params.passkeyId}`,
|
|
6779
|
+
{
|
|
6780
|
+
headers: this.headers,
|
|
6781
|
+
jwt: session.access_token,
|
|
6782
|
+
body: { friendly_name: params.friendlyName },
|
|
6783
|
+
}
|
|
6784
|
+
)
|
|
6785
|
+
if (error) {
|
|
6786
|
+
return this._returnResult({ data: null, error })
|
|
6787
|
+
}
|
|
6788
|
+
return this._returnResult({ data, error: null })
|
|
6789
|
+
})
|
|
6790
|
+
} catch (error) {
|
|
6791
|
+
if (isAuthError(error)) {
|
|
6792
|
+
return this._returnResult({ data: null, error })
|
|
6793
|
+
}
|
|
6794
|
+
throw error
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6797
|
+
|
|
6798
|
+
/**
|
|
6799
|
+
* Delete a passkey.
|
|
6800
|
+
*/
|
|
6801
|
+
private async _deletePasskey(params: PasskeyDeleteParams): Promise<AuthPasskeyDeleteResponse> {
|
|
6802
|
+
assertPasskeyExperimentalEnabled(this.experimental)
|
|
6803
|
+
try {
|
|
6804
|
+
return await this._useSession(async (result) => {
|
|
6805
|
+
const {
|
|
6806
|
+
data: { session },
|
|
6807
|
+
error: sessionError,
|
|
6808
|
+
} = result
|
|
6809
|
+
if (sessionError) {
|
|
6810
|
+
return this._returnResult({ data: null, error: sessionError })
|
|
6811
|
+
}
|
|
6812
|
+
if (!session) {
|
|
6813
|
+
return this._returnResult({ data: null, error: new AuthSessionMissingError() })
|
|
6814
|
+
}
|
|
6815
|
+
const { error } = await _request(
|
|
6816
|
+
this.fetch,
|
|
6817
|
+
'DELETE',
|
|
6818
|
+
`${this.url}/passkeys/${params.passkeyId}`,
|
|
6819
|
+
{
|
|
6820
|
+
headers: this.headers,
|
|
6821
|
+
jwt: session.access_token,
|
|
6822
|
+
noResolveJson: true,
|
|
6823
|
+
}
|
|
6824
|
+
)
|
|
6825
|
+
if (error) {
|
|
6826
|
+
return this._returnResult({ data: null, error })
|
|
6827
|
+
}
|
|
6828
|
+
return this._returnResult({ data: null, error: null })
|
|
6829
|
+
})
|
|
6830
|
+
} catch (error) {
|
|
6831
|
+
if (isAuthError(error)) {
|
|
6832
|
+
return this._returnResult({ data: null, error })
|
|
6833
|
+
}
|
|
6834
|
+
throw error
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
5901
6837
|
}
|