@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
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import GoTrueAdminApi from './GoTrueAdminApi';
|
|
2
|
-
import { AUTO_REFRESH_TICK_DURATION_MS, AUTO_REFRESH_TICK_THRESHOLD, DEFAULT_HEADERS, EXPIRY_MARGIN_MS, GOTRUE_URL, JWKS_TTL, STORAGE_KEY, } from './lib/constants';
|
|
3
|
-
import { AuthImplicitGrantRedirectError, AuthInvalidCredentialsError, AuthInvalidJwtError, AuthInvalidTokenResponseError, AuthPKCECodeVerifierMissingError, AuthPKCEGrantCodeExchangeError, AuthSessionMissingError, AuthUnknownError, isAuthApiError, isAuthError, isAuthImplicitGrantRedirectError, isAuthRetryableFetchError, isAuthSessionMissingError, } from './lib/errors';
|
|
2
|
+
import { AUTO_REFRESH_TICK_DURATION_MS, AUTO_REFRESH_TICK_THRESHOLD, DEFAULT_HEADERS, EXPIRY_MARGIN_MS, GOTRUE_URL, JWKS_TTL, REFRESH_FAILURE_COOLDOWN_MS, STORAGE_KEY, } from './lib/constants';
|
|
3
|
+
import { AuthImplicitGrantRedirectError, AuthInvalidCredentialsError, AuthInvalidJwtError, AuthInvalidTokenResponseError, AuthPKCECodeVerifierMissingError, AuthPKCEGrantCodeExchangeError, AuthRefreshDiscardedError, AuthSessionMissingError, AuthUnknownError, isAuthApiError, isAuthError, isAuthImplicitGrantRedirectError, isAuthRefreshDiscardedError, isAuthRetryableFetchError, isAuthSessionMissingError, } from './lib/errors';
|
|
4
4
|
import { _request, _sessionResponse, _sessionResponsePassword, _ssoResponse, _userResponse, } from './lib/fetch';
|
|
5
|
-
import { decodeJWT, deepClone, Deferred, generateCallbackId, getAlgorithm, getCodeChallengeAndMethod, getItemAsync, insecureUserWarningProxy, isBrowser, parseParametersFromURL, removeItemAsync, resolveFetch, retryable, setItemAsync, sleep, supportsLocalStorage, userNotAvailableProxy, validateExp, } from './lib/helpers';
|
|
5
|
+
import { assertPasskeyExperimentalEnabled, decodeJWT, deepClone, Deferred, generateCallbackId, getAlgorithm, getCodeChallengeAndMethod, getItemAsync, insecureUserWarningProxy, isBrowser, parseParametersFromURL, removeItemAsync, resolveFetch, retryable, setItemAsync, sleep, supportsLocalStorage, userNotAvailableProxy, validateExp, } from './lib/helpers';
|
|
6
6
|
import { memoryLocalStorageAdapter } from './lib/local-storage';
|
|
7
|
-
import { LockAcquireTimeoutError
|
|
7
|
+
import { LockAcquireTimeoutError } from './lib/locks';
|
|
8
8
|
import { polyfillGlobalThis } from './lib/polyfills';
|
|
9
9
|
import { version } from './lib/version';
|
|
10
10
|
import { bytesToBase64URL, stringToUint8Array } from './lib/base64url';
|
|
11
11
|
import { createSiweMessage, fromHex, getAddress, toHex, } from './lib/web3/ethereum';
|
|
12
|
-
import { deserializeCredentialCreationOptions, deserializeCredentialRequestOptions, serializeCredentialCreationResponse, serializeCredentialRequestResponse, WebAuthnApi, } from './lib/webauthn';
|
|
12
|
+
import { createCredential, deserializeCredentialCreationOptions, deserializeCredentialRequestOptions, getCredential, serializeCredentialCreationResponse, serializeCredentialRequestResponse, browserSupportsWebAuthn, webAuthnAbortService, WebAuthnApi, } from './lib/webauthn';
|
|
13
13
|
polyfillGlobalThis(); // Make "globalThis" available
|
|
14
14
|
const DEFAULT_OPTIONS = {
|
|
15
15
|
url: GOTRUE_URL,
|
|
@@ -22,9 +22,16 @@ const DEFAULT_OPTIONS = {
|
|
|
22
22
|
debug: false,
|
|
23
23
|
hasCustomAuthorizationHeader: false,
|
|
24
24
|
throwOnError: false,
|
|
25
|
-
lockAcquireTimeout: 5000, // 5 seconds
|
|
25
|
+
lockAcquireTimeout: 5000, // 5 seconds. Only used when a custom `lock` is supplied. TODO(v3): remove.
|
|
26
26
|
skipAutoInitialize: false,
|
|
27
|
+
experimental: {},
|
|
27
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* No-op lock used internally as a placeholder. Kept so older test setups that
|
|
31
|
+
* inject this exact reference do not break; new code never sees it because
|
|
32
|
+
* `this.lock` stays `null` when no custom lock is supplied (lockless path).
|
|
33
|
+
* TODO(v3): remove with the legacy lock path.
|
|
34
|
+
*/
|
|
28
35
|
async function lockNoOp(name, acquireTimeout, fn) {
|
|
29
36
|
return await fn();
|
|
30
37
|
}
|
|
@@ -58,13 +65,21 @@ class GoTrueClient {
|
|
|
58
65
|
/**
|
|
59
66
|
* Create a new client for use in the browser.
|
|
60
67
|
*
|
|
61
|
-
* @example
|
|
68
|
+
* @example Using supabase-js (recommended)
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { createClient } from '@supabase/supabase-js'
|
|
71
|
+
*
|
|
72
|
+
* const supabase = createClient('https://xyzcompany.supabase.co', 'your-publishable-key')
|
|
73
|
+
* const { data, error } = await supabase.auth.getUser()
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example Standalone import for bundle-sensitive environments
|
|
62
77
|
* ```ts
|
|
63
78
|
* import { GoTrueClient } from '@supabase/auth-js'
|
|
64
79
|
*
|
|
65
80
|
* const auth = new GoTrueClient({
|
|
66
81
|
* url: 'https://xyzcompany.supabase.co/auth/v1',
|
|
67
|
-
* headers: { apikey: '
|
|
82
|
+
* headers: { apikey: 'your-publishable-key' },
|
|
68
83
|
* storageKey: 'supabase-auth',
|
|
69
84
|
* })
|
|
70
85
|
* ```
|
|
@@ -81,6 +96,29 @@ class GoTrueClient {
|
|
|
81
96
|
this.autoRefreshTickTimeout = null;
|
|
82
97
|
this.visibilityChangedCallback = null;
|
|
83
98
|
this.refreshingDeferred = null;
|
|
99
|
+
/**
|
|
100
|
+
* Cache of the most recent refresh failure, keyed by the refresh token
|
|
101
|
+
* that failed. Serial callers passing the *same* token within
|
|
102
|
+
* `REFRESH_FAILURE_COOLDOWN_MS` (including subsequent auto-refresh ticks)
|
|
103
|
+
* receive this cached result instead of firing another `/token` request.
|
|
104
|
+
* Callers passing a *different* token (token rotation pickup, explicit
|
|
105
|
+
* `setSession`/`refreshSession({ refresh_token })`, multi-account switch)
|
|
106
|
+
* bypass the cache and attempt a fresh refresh as they should.
|
|
107
|
+
* Cleared on any successful refresh (locally or via BroadcastChannel from
|
|
108
|
+
* another tab) and on `_removeSession`.
|
|
109
|
+
*
|
|
110
|
+
* Pairs with `refreshingDeferred`: concurrent callers share the in-flight
|
|
111
|
+
* promise, serial callers within the cooldown share the failure result.
|
|
112
|
+
*/
|
|
113
|
+
this.lastRefreshFailure = null;
|
|
114
|
+
/**
|
|
115
|
+
* Monotonic counter incremented at the top of `_removeSession`, before any
|
|
116
|
+
* `await`. The commit guard inside `_callRefreshToken` captures this value
|
|
117
|
+
* before `_saveSession` and re-checks it after, so a `signOut` that
|
|
118
|
+
* interleaves inside `_saveSession`'s storage-write awaits is still caught
|
|
119
|
+
* (the post-fetch storage snapshot alone misses that window).
|
|
120
|
+
*/
|
|
121
|
+
this._sessionRemovalEpoch = 0;
|
|
84
122
|
/**
|
|
85
123
|
* Keeps track of the async client initialization.
|
|
86
124
|
* When null or not yet resolved the auth state is `unknown`
|
|
@@ -88,9 +126,29 @@ class GoTrueClient {
|
|
|
88
126
|
* Keep extra care to never reject or throw uncaught errors
|
|
89
127
|
*/
|
|
90
128
|
this.initializePromise = null;
|
|
129
|
+
/**
|
|
130
|
+
* Non-null only while `initialize()` is running. While open,
|
|
131
|
+
* `_notifyAllSubscribers` enqueues init-chain notifications (those fired with
|
|
132
|
+
* `broadcast = true`, i.e. from `_recoverAndRefresh`) into this array instead
|
|
133
|
+
* of firing directly, so that `initializePromise` is guaranteed to be
|
|
134
|
+
* resolved before any subscriber callback runs. Callbacks that call
|
|
135
|
+
* `getSession()` / `getUser()` etc. would otherwise deadlock because those
|
|
136
|
+
* methods await `initializePromise`. Notifications from the incoming
|
|
137
|
+
* BroadcastChannel handler (`broadcast = false`) are not enqueued — they fire
|
|
138
|
+
* immediately. Flushed (in order) by `initialize()` after `initializePromise`
|
|
139
|
+
* settles.
|
|
140
|
+
*/
|
|
141
|
+
this._pendingInitNotifications = null;
|
|
91
142
|
this.detectSessionInUrl = true;
|
|
92
143
|
this.hasCustomAuthorizationHeader = false;
|
|
93
144
|
this.suppressGetSessionWarning = false;
|
|
145
|
+
/**
|
|
146
|
+
* Custom lock function passed via `settings.lock`. When non-null, every auth
|
|
147
|
+
* operation runs inside `_acquireLock`. When null (the default), the client
|
|
148
|
+
* uses its lockless coordination (refresh single-flight + commit guard).
|
|
149
|
+
* TODO(v3): remove along with the legacy lock path.
|
|
150
|
+
*/
|
|
151
|
+
this.lock = null;
|
|
94
152
|
this.lockAcquired = false;
|
|
95
153
|
this.pendingInLock = [];
|
|
96
154
|
/**
|
|
@@ -115,29 +173,32 @@ class GoTrueClient {
|
|
|
115
173
|
}
|
|
116
174
|
this.persistSession = settings.persistSession;
|
|
117
175
|
this.autoRefreshToken = settings.autoRefreshToken;
|
|
176
|
+
this.experimental = (_b = settings.experimental) !== null && _b !== void 0 ? _b : {};
|
|
118
177
|
this.admin = new GoTrueAdminApi({
|
|
119
178
|
url: settings.url,
|
|
120
179
|
headers: settings.headers,
|
|
121
180
|
fetch: settings.fetch,
|
|
181
|
+
experimental: this.experimental,
|
|
122
182
|
});
|
|
123
183
|
this.url = settings.url;
|
|
124
184
|
this.headers = settings.headers;
|
|
125
185
|
this.fetch = resolveFetch(settings.fetch);
|
|
126
|
-
this.lock = settings.lock || lockNoOp;
|
|
127
186
|
this.detectSessionInUrl = settings.detectSessionInUrl;
|
|
128
187
|
this.flowType = settings.flowType;
|
|
129
188
|
this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader;
|
|
130
189
|
this.throwOnError = settings.throwOnError;
|
|
190
|
+
// Always wire `lockAcquireTimeout` even on the lockless path: consumers
|
|
191
|
+
// (including supabase-js tests) read it off the client to verify option
|
|
192
|
+
// flow-through.
|
|
131
193
|
this.lockAcquireTimeout = settings.lockAcquireTimeout;
|
|
132
|
-
|
|
194
|
+
// TODO(v3): remove. Legacy opt-in path preserved for backwards
|
|
195
|
+
// compatibility with callers passing a custom `lock` (typically React
|
|
196
|
+
// Native `processLock` or Node multi-process setups). When `settings.lock`
|
|
197
|
+
// is null the client uses its lockless coordination — no `navigator.locks`
|
|
198
|
+
// by default, no implicit `processLock`.
|
|
199
|
+
if (settings.lock != null) {
|
|
133
200
|
this.lock = settings.lock;
|
|
134
201
|
}
|
|
135
|
-
else if (this.persistSession && isBrowser() && ((_b = globalThis === null || globalThis === void 0 ? void 0 : globalThis.navigator) === null || _b === void 0 ? void 0 : _b.locks)) {
|
|
136
|
-
this.lock = navigatorLock;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
this.lock = lockNoOp;
|
|
140
|
-
}
|
|
141
202
|
if (!this.jwks) {
|
|
142
203
|
this.jwks = { keys: [] };
|
|
143
204
|
this.jwks_cached_at = Number.MIN_SAFE_INTEGER;
|
|
@@ -159,6 +220,15 @@ class GoTrueClient {
|
|
|
159
220
|
listGrants: this._listOAuthGrants.bind(this),
|
|
160
221
|
revokeGrant: this._revokeOAuthGrant.bind(this),
|
|
161
222
|
};
|
|
223
|
+
this.passkey = {
|
|
224
|
+
startRegistration: this._startPasskeyRegistration.bind(this),
|
|
225
|
+
verifyRegistration: this._verifyPasskeyRegistration.bind(this),
|
|
226
|
+
startAuthentication: this._startPasskeyAuthentication.bind(this),
|
|
227
|
+
verifyAuthentication: this._verifyPasskeyAuthentication.bind(this),
|
|
228
|
+
list: this._listPasskeys.bind(this),
|
|
229
|
+
update: this._updatePasskey.bind(this),
|
|
230
|
+
delete: this._deletePasskey.bind(this),
|
|
231
|
+
};
|
|
162
232
|
if (this.persistSession) {
|
|
163
233
|
if (settings.storage) {
|
|
164
234
|
this.storage = settings.storage;
|
|
@@ -189,6 +259,12 @@ class GoTrueClient {
|
|
|
189
259
|
}
|
|
190
260
|
(_c = this.broadcastChannel) === null || _c === void 0 ? void 0 : _c.addEventListener('message', async (event) => {
|
|
191
261
|
this._debug('received broadcast notification from other tab or client', event);
|
|
262
|
+
// Another tab successfully refreshed or signed in — any cached
|
|
263
|
+
// failure in this tab is stale and should not block the next
|
|
264
|
+
// refresh attempt.
|
|
265
|
+
if (event.data.event === 'TOKEN_REFRESHED' || event.data.event === 'SIGNED_IN') {
|
|
266
|
+
this.lastRefreshFailure = null;
|
|
267
|
+
}
|
|
192
268
|
try {
|
|
193
269
|
await this._notifyAllSubscribers(event.data.event, event.data.session, false); // broadcast = false so we don't get an endless loop of messages
|
|
194
270
|
}
|
|
@@ -234,22 +310,55 @@ class GoTrueClient {
|
|
|
234
310
|
return this;
|
|
235
311
|
}
|
|
236
312
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
313
|
+
* Initialize the auth client by loading the session from storage or
|
|
314
|
+
* detecting it from the URL after an OAuth, magic-link, or password-recovery
|
|
315
|
+
* redirect.
|
|
316
|
+
*
|
|
317
|
+
* **Most callers do not need to invoke this directly.** The client calls it
|
|
318
|
+
* automatically during construction, and to react to sign-in events (including
|
|
319
|
+
* post-redirect events) you should subscribe to `onAuthStateChange` rather
|
|
320
|
+
* than awaiting `initialize()`.
|
|
321
|
+
*
|
|
322
|
+
* You only need to call it manually when you have opted out of the automatic
|
|
323
|
+
* call by passing `skipAutoInitialize: true` — for example, in an SSR context
|
|
324
|
+
* where you need to control initialization timing. In that case, awaiting
|
|
325
|
+
* `initialize()` returns the resolved session result (or any error encountered
|
|
326
|
+
* while detecting it from the URL).
|
|
240
327
|
*
|
|
241
328
|
* @category Auth
|
|
242
329
|
*/
|
|
243
330
|
async initialize() {
|
|
331
|
+
var _a;
|
|
244
332
|
if (this.initializePromise) {
|
|
245
333
|
return await this.initializePromise;
|
|
246
334
|
}
|
|
335
|
+
// Open the notification queue before _initialize() runs so that every
|
|
336
|
+
// _notifyAllSubscribers call inside the init chain enqueues instead of
|
|
337
|
+
// firing. Without this, a callback receiving SIGNED_IN (or TOKEN_REFRESHED
|
|
338
|
+
// / SIGNED_OUT) during _recoverAndRefresh would deadlock if it called
|
|
339
|
+
// getSession() / getUser() — those methods await initializePromise, which
|
|
340
|
+
// can only resolve after the callback returns, which can only return after
|
|
341
|
+
// getSession() resolves. The queue is flushed below after initializePromise
|
|
342
|
+
// has settled, so callbacks run with a fully resolved initializePromise.
|
|
343
|
+
this._pendingInitNotifications = [];
|
|
247
344
|
this.initializePromise = (async () => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
345
|
+
if (this.lock != null) {
|
|
346
|
+
// TODO(v3): remove legacy lock path
|
|
347
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
348
|
+
return await this._initialize();
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return await this._initialize();
|
|
251
352
|
})();
|
|
252
|
-
|
|
353
|
+
const result = await this.initializePromise;
|
|
354
|
+
// initializePromise is now resolved — flush queued notifications in order.
|
|
355
|
+
// Callbacks can safely call getSession() / getUser() / signOut() etc.
|
|
356
|
+
const queue = (_a = this._pendingInitNotifications) !== null && _a !== void 0 ? _a : [];
|
|
357
|
+
this._pendingInitNotifications = null;
|
|
358
|
+
for (const n of queue) {
|
|
359
|
+
await this._notifyAllSubscribers(n.event, n.session, n.broadcast);
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
253
362
|
}
|
|
254
363
|
/**
|
|
255
364
|
* IMPORTANT:
|
|
@@ -783,6 +892,21 @@ class GoTrueClient {
|
|
|
783
892
|
* password: 'some-password',
|
|
784
893
|
* })
|
|
785
894
|
* ```
|
|
895
|
+
*
|
|
896
|
+
* @exampleDescription Handling errors
|
|
897
|
+
* 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.
|
|
898
|
+
*
|
|
899
|
+
* @example Handling errors
|
|
900
|
+
* ```js
|
|
901
|
+
* const { data, error } = await supabase.auth.signInWithPassword({
|
|
902
|
+
* email: 'example@email.com',
|
|
903
|
+
* password: 'example-password',
|
|
904
|
+
* })
|
|
905
|
+
* if (error) {
|
|
906
|
+
* console.error(error)
|
|
907
|
+
* return
|
|
908
|
+
* }
|
|
909
|
+
* ```
|
|
786
910
|
*/
|
|
787
911
|
async signInWithPassword(credentials) {
|
|
788
912
|
try {
|
|
@@ -1098,9 +1222,13 @@ class GoTrueClient {
|
|
|
1098
1222
|
*/
|
|
1099
1223
|
async exchangeCodeForSession(authCode) {
|
|
1100
1224
|
await this.initializePromise;
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1225
|
+
if (this.lock != null) {
|
|
1226
|
+
// TODO(v3): remove legacy lock path
|
|
1227
|
+
return this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
1228
|
+
return this._exchangeCodeForSession(authCode);
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
return this._exchangeCodeForSession(authCode);
|
|
1104
1232
|
}
|
|
1105
1233
|
/**
|
|
1106
1234
|
* Signs in a user by verifying a message signed by the user's private key.
|
|
@@ -1202,7 +1330,7 @@ class GoTrueClient {
|
|
|
1202
1330
|
}
|
|
1203
1331
|
}
|
|
1204
1332
|
async signInWithEthereum(credentials) {
|
|
1205
|
-
var _a, _b, _c, _d,
|
|
1333
|
+
var _a, _b, _c, _d, _f, _g, _h, _j, _k, _l, _m;
|
|
1206
1334
|
// TODO: flatten type
|
|
1207
1335
|
let message;
|
|
1208
1336
|
let signature;
|
|
@@ -1262,11 +1390,11 @@ class GoTrueClient {
|
|
|
1262
1390
|
version: '1',
|
|
1263
1391
|
chainId: chainId,
|
|
1264
1392
|
nonce: (_c = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _c === void 0 ? void 0 : _c.nonce,
|
|
1265
|
-
issuedAt: (
|
|
1266
|
-
expirationTime: (
|
|
1267
|
-
notBefore: (
|
|
1268
|
-
requestId: (
|
|
1269
|
-
resources: (
|
|
1393
|
+
issuedAt: (_f = (_d = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _d === void 0 ? void 0 : _d.issuedAt) !== null && _f !== void 0 ? _f : new Date(),
|
|
1394
|
+
expirationTime: (_g = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _g === void 0 ? void 0 : _g.expirationTime,
|
|
1395
|
+
notBefore: (_h = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _h === void 0 ? void 0 : _h.notBefore,
|
|
1396
|
+
requestId: (_j = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _j === void 0 ? void 0 : _j.requestId,
|
|
1397
|
+
resources: (_k = options === null || options === void 0 ? void 0 : options.signInWithEthereum) === null || _k === void 0 ? void 0 : _k.resources,
|
|
1270
1398
|
};
|
|
1271
1399
|
message = createSiweMessage(siweMessage);
|
|
1272
1400
|
// Sign message
|
|
@@ -1279,8 +1407,8 @@ class GoTrueClient {
|
|
|
1279
1407
|
const { data, error } = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=web3`, {
|
|
1280
1408
|
headers: this.headers,
|
|
1281
1409
|
body: Object.assign({ chain: 'ethereum', message,
|
|
1282
|
-
signature }, (((
|
|
1283
|
-
? { gotrue_meta_security: { captcha_token: (
|
|
1410
|
+
signature }, (((_l = credentials.options) === null || _l === void 0 ? void 0 : _l.captchaToken)
|
|
1411
|
+
? { gotrue_meta_security: { captcha_token: (_m = credentials.options) === null || _m === void 0 ? void 0 : _m.captchaToken } }
|
|
1284
1412
|
: null)),
|
|
1285
1413
|
xform: _sessionResponse,
|
|
1286
1414
|
});
|
|
@@ -1305,7 +1433,7 @@ class GoTrueClient {
|
|
|
1305
1433
|
}
|
|
1306
1434
|
}
|
|
1307
1435
|
async signInWithSolana(credentials) {
|
|
1308
|
-
var _a, _b, _c, _d,
|
|
1436
|
+
var _a, _b, _c, _d, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
1309
1437
|
let message;
|
|
1310
1438
|
let signature;
|
|
1311
1439
|
if ('message' in credentials) {
|
|
@@ -1390,17 +1518,17 @@ class GoTrueClient {
|
|
|
1390
1518
|
...(((_d = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _d === void 0 ? void 0 : _d.notBefore)
|
|
1391
1519
|
? [`Not Before: ${options.signInWithSolana.notBefore}`]
|
|
1392
1520
|
: []),
|
|
1393
|
-
...(((
|
|
1521
|
+
...(((_f = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _f === void 0 ? void 0 : _f.expirationTime)
|
|
1394
1522
|
? [`Expiration Time: ${options.signInWithSolana.expirationTime}`]
|
|
1395
1523
|
: []),
|
|
1396
|
-
...(((
|
|
1524
|
+
...(((_g = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _g === void 0 ? void 0 : _g.chainId)
|
|
1397
1525
|
? [`Chain ID: ${options.signInWithSolana.chainId}`]
|
|
1398
1526
|
: []),
|
|
1399
|
-
...(((
|
|
1400
|
-
...(((
|
|
1527
|
+
...(((_h = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _h === void 0 ? void 0 : _h.nonce) ? [`Nonce: ${options.signInWithSolana.nonce}`] : []),
|
|
1528
|
+
...(((_j = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _j === void 0 ? void 0 : _j.requestId)
|
|
1401
1529
|
? [`Request ID: ${options.signInWithSolana.requestId}`]
|
|
1402
1530
|
: []),
|
|
1403
|
-
...(((
|
|
1531
|
+
...(((_l = (_k = options === null || options === void 0 ? void 0 : options.signInWithSolana) === null || _k === void 0 ? void 0 : _k.resources) === null || _l === void 0 ? void 0 : _l.length)
|
|
1404
1532
|
? [
|
|
1405
1533
|
'Resources',
|
|
1406
1534
|
...options.signInWithSolana.resources.map((resource) => `- ${resource}`),
|
|
@@ -1417,8 +1545,8 @@ class GoTrueClient {
|
|
|
1417
1545
|
try {
|
|
1418
1546
|
const { data, error } = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=web3`, {
|
|
1419
1547
|
headers: this.headers,
|
|
1420
|
-
body: Object.assign({ chain: 'solana', message, signature: bytesToBase64URL(signature) }, (((
|
|
1421
|
-
? { gotrue_meta_security: { captcha_token: (
|
|
1548
|
+
body: Object.assign({ chain: 'solana', message, signature: bytesToBase64URL(signature) }, (((_m = credentials.options) === null || _m === void 0 ? void 0 : _m.captchaToken)
|
|
1549
|
+
? { gotrue_meta_security: { captcha_token: (_o = credentials.options) === null || _o === void 0 ? void 0 : _o.captchaToken } }
|
|
1422
1550
|
: null)),
|
|
1423
1551
|
xform: _sessionResponse,
|
|
1424
1552
|
});
|
|
@@ -1470,7 +1598,7 @@ class GoTrueClient {
|
|
|
1470
1598
|
}
|
|
1471
1599
|
if (data.session) {
|
|
1472
1600
|
await this._saveSession(data.session);
|
|
1473
|
-
await this._notifyAllSubscribers('SIGNED_IN', data.session);
|
|
1601
|
+
await this._notifyAllSubscribers(redirectType === 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN', data.session);
|
|
1474
1602
|
}
|
|
1475
1603
|
return this._returnResult({ data: Object.assign(Object.assign({}, data), { redirectType: redirectType !== null && redirectType !== void 0 ? redirectType : null }), error });
|
|
1476
1604
|
}
|
|
@@ -1673,7 +1801,7 @@ class GoTrueClient {
|
|
|
1673
1801
|
* ```
|
|
1674
1802
|
*/
|
|
1675
1803
|
async signInWithOtp(credentials) {
|
|
1676
|
-
var _a, _b, _c, _d,
|
|
1804
|
+
var _a, _b, _c, _d, _f;
|
|
1677
1805
|
try {
|
|
1678
1806
|
if ('email' in credentials) {
|
|
1679
1807
|
const { email, options } = credentials;
|
|
@@ -1706,7 +1834,7 @@ class GoTrueClient {
|
|
|
1706
1834
|
data: (_c = options === null || options === void 0 ? void 0 : options.data) !== null && _c !== void 0 ? _c : {},
|
|
1707
1835
|
create_user: (_d = options === null || options === void 0 ? void 0 : options.shouldCreateUser) !== null && _d !== void 0 ? _d : true,
|
|
1708
1836
|
gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
|
|
1709
|
-
channel: (
|
|
1837
|
+
channel: (_f = options === null || options === void 0 ? void 0 : options.channel) !== null && _f !== void 0 ? _f : 'sms',
|
|
1710
1838
|
},
|
|
1711
1839
|
});
|
|
1712
1840
|
return this._returnResult({
|
|
@@ -1952,7 +2080,7 @@ class GoTrueClient {
|
|
|
1952
2080
|
* ```
|
|
1953
2081
|
*/
|
|
1954
2082
|
async signInWithSSO(params) {
|
|
1955
|
-
var _a, _b, _c, _d,
|
|
2083
|
+
var _a, _b, _c, _d, _f;
|
|
1956
2084
|
try {
|
|
1957
2085
|
let codeChallenge = null;
|
|
1958
2086
|
let codeChallengeMethod = null;
|
|
@@ -1968,7 +2096,7 @@ class GoTrueClient {
|
|
|
1968
2096
|
xform: _ssoResponse,
|
|
1969
2097
|
});
|
|
1970
2098
|
// Automatically redirect in browser unless skipBrowserRedirect is true
|
|
1971
|
-
if (((_d = result.data) === null || _d === void 0 ? void 0 : _d.url) && isBrowser() && !((
|
|
2099
|
+
if (((_d = result.data) === null || _d === void 0 ? void 0 : _d.url) && isBrowser() && !((_f = params.options) === null || _f === void 0 ? void 0 : _f.skipBrowserRedirect)) {
|
|
1972
2100
|
window.location.assign(result.data.url);
|
|
1973
2101
|
}
|
|
1974
2102
|
return this._returnResult(result);
|
|
@@ -2004,9 +2132,13 @@ class GoTrueClient {
|
|
|
2004
2132
|
*/
|
|
2005
2133
|
async reauthenticate() {
|
|
2006
2134
|
await this.initializePromise;
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2135
|
+
if (this.lock != null) {
|
|
2136
|
+
// TODO(v3): remove legacy lock path
|
|
2137
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2138
|
+
return await this._reauthenticate();
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
return await this._reauthenticate();
|
|
2010
2142
|
}
|
|
2011
2143
|
async _reauthenticate() {
|
|
2012
2144
|
try {
|
|
@@ -2094,15 +2226,26 @@ class GoTrueClient {
|
|
|
2094
2226
|
const endpoint = `${this.url}/resend`;
|
|
2095
2227
|
if ('email' in credentials) {
|
|
2096
2228
|
const { email, type, options } = credentials;
|
|
2229
|
+
let codeChallenge = null;
|
|
2230
|
+
let codeChallengeMethod = null;
|
|
2231
|
+
if (this.flowType === 'pkce') {
|
|
2232
|
+
;
|
|
2233
|
+
[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(this.storage, this.storageKey);
|
|
2234
|
+
}
|
|
2097
2235
|
const { error } = await _request(this.fetch, 'POST', endpoint, {
|
|
2098
2236
|
headers: this.headers,
|
|
2099
2237
|
body: {
|
|
2100
2238
|
email,
|
|
2101
2239
|
type,
|
|
2102
2240
|
gotrue_meta_security: { captcha_token: options === null || options === void 0 ? void 0 : options.captchaToken },
|
|
2241
|
+
code_challenge: codeChallenge,
|
|
2242
|
+
code_challenge_method: codeChallengeMethod,
|
|
2103
2243
|
},
|
|
2104
2244
|
redirectTo: options === null || options === void 0 ? void 0 : options.emailRedirectTo,
|
|
2105
2245
|
});
|
|
2246
|
+
if (error) {
|
|
2247
|
+
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`);
|
|
2248
|
+
}
|
|
2106
2249
|
return this._returnResult({ data: { user: null, session: null }, error });
|
|
2107
2250
|
}
|
|
2108
2251
|
else if ('phone' in credentials) {
|
|
@@ -2123,6 +2266,7 @@ class GoTrueClient {
|
|
|
2123
2266
|
throw new AuthInvalidCredentialsError('You must provide either an email or phone number and a type');
|
|
2124
2267
|
}
|
|
2125
2268
|
catch (error) {
|
|
2269
|
+
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`);
|
|
2126
2270
|
if (isAuthError(error)) {
|
|
2127
2271
|
return this._returnResult({ data: { user: null, session: null }, error });
|
|
2128
2272
|
}
|
|
@@ -2138,7 +2282,7 @@ class GoTrueClient {
|
|
|
2138
2282
|
* to the client. If that storage is based on request cookies for example,
|
|
2139
2283
|
* the values in it may not be authentic and therefore it's strongly advised
|
|
2140
2284
|
* against using this method and its results in such circumstances. A warning
|
|
2141
|
-
* will be emitted if this is detected. Use {@link
|
|
2285
|
+
* will be emitted if this is detected. Use {@link GoTrueClient.getUser} instead.
|
|
2142
2286
|
*
|
|
2143
2287
|
* @category Auth
|
|
2144
2288
|
*
|
|
@@ -2149,7 +2293,7 @@ class GoTrueClient {
|
|
|
2149
2293
|
* - If the session's access token is expired or is about to expire, this method will use the refresh token to refresh the session.
|
|
2150
2294
|
* - 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.
|
|
2151
2295
|
* - **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.
|
|
2152
|
-
* -
|
|
2296
|
+
* - 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.
|
|
2153
2297
|
*
|
|
2154
2298
|
* @example Get the session data
|
|
2155
2299
|
* ```js
|
|
@@ -2216,15 +2360,24 @@ class GoTrueClient {
|
|
|
2216
2360
|
*/
|
|
2217
2361
|
async getSession() {
|
|
2218
2362
|
await this.initializePromise;
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2363
|
+
if (this.lock != null) {
|
|
2364
|
+
// TODO(v3): remove legacy lock path
|
|
2365
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2366
|
+
return this._useSession(async (result) => {
|
|
2367
|
+
return result;
|
|
2368
|
+
});
|
|
2222
2369
|
});
|
|
2370
|
+
}
|
|
2371
|
+
return await this._useSession(async (result) => {
|
|
2372
|
+
return result;
|
|
2223
2373
|
});
|
|
2224
|
-
return result;
|
|
2225
2374
|
}
|
|
2226
2375
|
/**
|
|
2227
2376
|
* Acquires a global lock based on the storage key.
|
|
2377
|
+
*
|
|
2378
|
+
* TODO(v3): remove along with the legacy lock path. Only called when
|
|
2379
|
+
* `this.lock` is non-null (custom lock supplied via constructor). The
|
|
2380
|
+
* default lockless path bypasses this entirely.
|
|
2228
2381
|
*/
|
|
2229
2382
|
async _acquireLock(acquireTimeout, fn) {
|
|
2230
2383
|
this._debug('#_acquireLock', 'begin', acquireTimeout);
|
|
@@ -2241,7 +2394,7 @@ class GoTrueClient {
|
|
|
2241
2394
|
try {
|
|
2242
2395
|
await result;
|
|
2243
2396
|
}
|
|
2244
|
-
catch (
|
|
2397
|
+
catch (_e) {
|
|
2245
2398
|
// we just care if it finished
|
|
2246
2399
|
}
|
|
2247
2400
|
})());
|
|
@@ -2280,15 +2433,17 @@ class GoTrueClient {
|
|
|
2280
2433
|
}
|
|
2281
2434
|
}
|
|
2282
2435
|
/**
|
|
2283
|
-
* Use instead of {@link
|
|
2284
|
-
*
|
|
2285
|
-
*
|
|
2286
|
-
* session at once across multiple tabs or processes.
|
|
2436
|
+
* Use instead of {@link GoTrueClient.getSession} inside the library. Loads the session
|
|
2437
|
+
* via `__loadSession` (which may trigger a refresh if the access token is
|
|
2438
|
+
* within the expiry margin) and runs `fn` with the result.
|
|
2287
2439
|
*/
|
|
2288
2440
|
async _useSession(fn) {
|
|
2289
2441
|
this._debug('#_useSession', 'begin');
|
|
2290
2442
|
try {
|
|
2291
|
-
//
|
|
2443
|
+
// Concurrent callers may both reach __loadSession; storage reads are
|
|
2444
|
+
// idempotent, and the only write path inside it (refresh) is
|
|
2445
|
+
// single-flighted downstream by `refreshingDeferred` in
|
|
2446
|
+
// `_callRefreshToken`. No serialization is needed at this layer.
|
|
2292
2447
|
const result = await this.__loadSession();
|
|
2293
2448
|
return await fn(result);
|
|
2294
2449
|
}
|
|
@@ -2299,11 +2454,12 @@ class GoTrueClient {
|
|
|
2299
2454
|
/**
|
|
2300
2455
|
* NEVER USE DIRECTLY!
|
|
2301
2456
|
*
|
|
2302
|
-
* Always use
|
|
2457
|
+
* Always use `_useSession`.
|
|
2303
2458
|
*/
|
|
2304
2459
|
async __loadSession() {
|
|
2305
2460
|
this._debug('#__loadSession()', 'begin');
|
|
2306
|
-
if (!this.lockAcquired) {
|
|
2461
|
+
if (this.lock != null && !this.lockAcquired) {
|
|
2462
|
+
// TODO(v3): remove. Only meaningful on the legacy lock path.
|
|
2307
2463
|
this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack);
|
|
2308
2464
|
}
|
|
2309
2465
|
try {
|
|
@@ -2357,6 +2513,25 @@ class GoTrueClient {
|
|
|
2357
2513
|
}
|
|
2358
2514
|
const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token);
|
|
2359
2515
|
if (error) {
|
|
2516
|
+
// Proactive-preserve mirror: `_callRefreshToken` keeps the session
|
|
2517
|
+
// in storage when refresh fails non-retryably but the access token
|
|
2518
|
+
// is still inside its real expiry window. Hand the caller the
|
|
2519
|
+
// still-valid session instead of translating the refresh error
|
|
2520
|
+
// into `session: null`. If the access token has actually expired,
|
|
2521
|
+
// the session is genuinely dead and the error stands. Explicit
|
|
2522
|
+
// refresh entry points (`refreshSession`, `setSession`)
|
|
2523
|
+
// intentionally bypass this fallback — they want to know the
|
|
2524
|
+
// refresh failed.
|
|
2525
|
+
const accessTokenStillValid = !!(currentSession.expires_at && currentSession.expires_at * 1000 > Date.now());
|
|
2526
|
+
if (accessTokenStillValid) {
|
|
2527
|
+
// Race guard: a concurrent `signOut` may have cleared storage
|
|
2528
|
+
// during the refresh attempt. Don't hand back a session that no
|
|
2529
|
+
// longer exists on disk.
|
|
2530
|
+
const stillStored = (await getItemAsync(this.storage, this.storageKey));
|
|
2531
|
+
if (stillStored && stillStored.refresh_token === currentSession.refresh_token) {
|
|
2532
|
+
return this._returnResult({ data: { session: currentSession }, error: null });
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2360
2535
|
return this._returnResult({ data: { session: null }, error });
|
|
2361
2536
|
}
|
|
2362
2537
|
return this._returnResult({ data: { session }, error: null });
|
|
@@ -2446,9 +2621,16 @@ class GoTrueClient {
|
|
|
2446
2621
|
return await this._getUser(jwt);
|
|
2447
2622
|
}
|
|
2448
2623
|
await this.initializePromise;
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2624
|
+
let result;
|
|
2625
|
+
if (this.lock != null) {
|
|
2626
|
+
// TODO(v3): remove legacy lock path
|
|
2627
|
+
result = await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2628
|
+
return await this._getUser();
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
else {
|
|
2632
|
+
result = await this._getUser();
|
|
2633
|
+
}
|
|
2452
2634
|
if (result.data.user) {
|
|
2453
2635
|
this.suppressGetSessionWarning = true;
|
|
2454
2636
|
}
|
|
@@ -2609,9 +2791,13 @@ class GoTrueClient {
|
|
|
2609
2791
|
*/
|
|
2610
2792
|
async updateUser(attributes, options = {}) {
|
|
2611
2793
|
await this.initializePromise;
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2794
|
+
if (this.lock != null) {
|
|
2795
|
+
// TODO(v3): remove legacy lock path
|
|
2796
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2797
|
+
return await this._updateUser(attributes, options);
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
return await this._updateUser(attributes, options);
|
|
2615
2801
|
}
|
|
2616
2802
|
async _updateUser(attributes, options = {}) {
|
|
2617
2803
|
try {
|
|
@@ -2780,9 +2966,13 @@ class GoTrueClient {
|
|
|
2780
2966
|
*/
|
|
2781
2967
|
async setSession(currentSession) {
|
|
2782
2968
|
await this.initializePromise;
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2969
|
+
if (this.lock != null) {
|
|
2970
|
+
// TODO(v3): remove legacy lock path
|
|
2971
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
2972
|
+
return await this._setSession(currentSession);
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
return await this._setSession(currentSession);
|
|
2786
2976
|
}
|
|
2787
2977
|
async _setSession(currentSession) {
|
|
2788
2978
|
try {
|
|
@@ -2960,9 +3150,13 @@ class GoTrueClient {
|
|
|
2960
3150
|
*/
|
|
2961
3151
|
async refreshSession(currentSession) {
|
|
2962
3152
|
await this.initializePromise;
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3153
|
+
if (this.lock != null) {
|
|
3154
|
+
// TODO(v3): remove legacy lock path
|
|
3155
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3156
|
+
return await this._refreshSession(currentSession);
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
return await this._refreshSession(currentSession);
|
|
2966
3160
|
}
|
|
2967
3161
|
async _refreshSession(currentSession) {
|
|
2968
3162
|
try {
|
|
@@ -2999,6 +3193,7 @@ class GoTrueClient {
|
|
|
2999
3193
|
* Gets the session data from a URL string
|
|
3000
3194
|
*/
|
|
3001
3195
|
async _getSessionFromURL(params, callbackUrlType) {
|
|
3196
|
+
var _a;
|
|
3002
3197
|
try {
|
|
3003
3198
|
if (!isBrowser())
|
|
3004
3199
|
throw new AuthImplicitGrantRedirectError('No browser detected.');
|
|
@@ -3037,7 +3232,10 @@ class GoTrueClient {
|
|
|
3037
3232
|
const url = new URL(window.location.href);
|
|
3038
3233
|
url.searchParams.delete('code');
|
|
3039
3234
|
window.history.replaceState(window.history.state, '', url.toString());
|
|
3040
|
-
return {
|
|
3235
|
+
return {
|
|
3236
|
+
data: { session: data.session, redirectType: (_a = data.redirectType) !== null && _a !== void 0 ? _a : null },
|
|
3237
|
+
error: null,
|
|
3238
|
+
};
|
|
3041
3239
|
}
|
|
3042
3240
|
const { provider_token, provider_refresh_token, access_token, refresh_token, expires_in, expires_at, token_type, } = params;
|
|
3043
3241
|
if (!access_token || !expires_in || !refresh_token || !token_type) {
|
|
@@ -3096,7 +3294,7 @@ class GoTrueClient {
|
|
|
3096
3294
|
if (typeof this.detectSessionInUrl === 'function') {
|
|
3097
3295
|
return this.detectSessionInUrl(new URL(window.location.href), params);
|
|
3098
3296
|
}
|
|
3099
|
-
return Boolean(params.access_token || params.error_description);
|
|
3297
|
+
return Boolean(params.access_token || params.error || params.error_description || params.error_code);
|
|
3100
3298
|
}
|
|
3101
3299
|
/**
|
|
3102
3300
|
* Checks if the current URL and backing storage contain parameters given by a PKCE flow
|
|
@@ -3113,37 +3311,56 @@ class GoTrueClient {
|
|
|
3113
3311
|
*
|
|
3114
3312
|
* If using `others` scope, no `SIGNED_OUT` event is fired!
|
|
3115
3313
|
*
|
|
3314
|
+
* **Warning:** the default `scope` is `'global'`. This signs the user out of
|
|
3315
|
+
* **every device they are currently signed in on**, not just the current
|
|
3316
|
+
* tab/session. If you only want to sign the user out of the current session
|
|
3317
|
+
* (the behavior most other auth libraries default to), pass
|
|
3318
|
+
* `{ scope: 'local' }` explicitly.
|
|
3319
|
+
*
|
|
3116
3320
|
* @category Auth
|
|
3117
3321
|
*
|
|
3118
3322
|
* @remarks
|
|
3119
3323
|
* - In order to use the `signOut()` method, the user needs to be signed in first.
|
|
3120
|
-
* - By default, `signOut()` uses the global scope, which signs out
|
|
3324
|
+
* - By default, `signOut()` uses the **global** scope, which signs out the user
|
|
3325
|
+
* on every device they are signed in on (not just the current one). Pass
|
|
3326
|
+
* `{ scope: 'local' }` to only sign out the current session. This is
|
|
3327
|
+
* usually what apps want on a "Sign out" button, especially when users
|
|
3328
|
+
* sign in from multiple devices and do not expect signing out of one to
|
|
3329
|
+
* terminate the others.
|
|
3121
3330
|
* - 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.
|
|
3122
3331
|
*
|
|
3123
|
-
* @example Sign out (
|
|
3332
|
+
* @example Sign out of every device (global – default)
|
|
3124
3333
|
* ```js
|
|
3125
3334
|
* const { error } = await supabase.auth.signOut()
|
|
3126
3335
|
* ```
|
|
3127
3336
|
*
|
|
3128
|
-
* @example Sign out
|
|
3337
|
+
* @example Sign out only the current session (recommended for most apps)
|
|
3129
3338
|
* ```js
|
|
3130
3339
|
* const { error } = await supabase.auth.signOut({ scope: 'local' })
|
|
3131
3340
|
* ```
|
|
3132
3341
|
*
|
|
3133
|
-
* @example Sign out
|
|
3342
|
+
* @example Sign out of all other sessions, keep the current one
|
|
3134
3343
|
* ```js
|
|
3135
3344
|
* const { error } = await supabase.auth.signOut({ scope: 'others' })
|
|
3136
3345
|
* ```
|
|
3137
3346
|
*/
|
|
3138
3347
|
async signOut(options = { scope: 'global' }) {
|
|
3139
3348
|
await this.initializePromise;
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3349
|
+
if (this.lock != null) {
|
|
3350
|
+
// TODO(v3): remove legacy lock path
|
|
3351
|
+
return await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3352
|
+
return await this._signOut(options);
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
return await this._signOut(options);
|
|
3143
3356
|
}
|
|
3144
3357
|
async _signOut({ scope } = { scope: 'global' }) {
|
|
3145
3358
|
return await this._useSession(async (result) => {
|
|
3146
3359
|
var _a;
|
|
3360
|
+
const removeCurrentSession = async () => {
|
|
3361
|
+
await this._removeSession();
|
|
3362
|
+
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`);
|
|
3363
|
+
};
|
|
3147
3364
|
const { data, error: sessionError } = result;
|
|
3148
3365
|
if (sessionError && !isAuthSessionMissingError(sessionError)) {
|
|
3149
3366
|
return this._returnResult({ error: sessionError });
|
|
@@ -3157,13 +3374,15 @@ class GoTrueClient {
|
|
|
3157
3374
|
if (!((isAuthApiError(error) &&
|
|
3158
3375
|
(error.status === 404 || error.status === 401 || error.status === 403)) ||
|
|
3159
3376
|
isAuthSessionMissingError(error))) {
|
|
3377
|
+
if (scope !== 'others') {
|
|
3378
|
+
await removeCurrentSession();
|
|
3379
|
+
}
|
|
3160
3380
|
return this._returnResult({ error });
|
|
3161
3381
|
}
|
|
3162
3382
|
}
|
|
3163
3383
|
}
|
|
3164
3384
|
if (scope !== 'others') {
|
|
3165
|
-
await
|
|
3166
|
-
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`);
|
|
3385
|
+
await removeCurrentSession();
|
|
3167
3386
|
}
|
|
3168
3387
|
return this._returnResult({ error: null });
|
|
3169
3388
|
});
|
|
@@ -3175,18 +3394,8 @@ class GoTrueClient {
|
|
|
3175
3394
|
* - Subscribes to important events occurring on the user's session.
|
|
3176
3395
|
* - Use on the frontend/client. It is less useful on the server.
|
|
3177
3396
|
* - 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.
|
|
3178
|
-
* -
|
|
3179
|
-
*
|
|
3180
|
-
* - Limit the number of `await` calls in `async` callbacks.
|
|
3181
|
-
* - 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:
|
|
3182
|
-
* ```js
|
|
3183
|
-
* supabase.auth.onAuthStateChange((event, session) => {
|
|
3184
|
-
* setTimeout(async () => {
|
|
3185
|
-
* // await on other Supabase function here
|
|
3186
|
-
* // this runs right after the callback has finished
|
|
3187
|
-
* }, 0)
|
|
3188
|
-
* })
|
|
3189
|
-
* ```
|
|
3397
|
+
* - Callbacks can be `async` and can safely call other Supabase auth methods (`getUser`, `setSession`, etc.) from inside the callback.
|
|
3398
|
+
* - Keep callbacks quick. Events are awaited in order, so a slow callback delays subsequent events to subscribers in this tab.
|
|
3190
3399
|
* - Emitted events:
|
|
3191
3400
|
* - `INITIAL_SESSION`
|
|
3192
3401
|
* - Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
|
|
@@ -3369,9 +3578,15 @@ class GoTrueClient {
|
|
|
3369
3578
|
this.stateChangeEmitters.set(id, subscription);
|
|
3370
3579
|
(async () => {
|
|
3371
3580
|
await this.initializePromise;
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3581
|
+
if (this.lock != null) {
|
|
3582
|
+
// TODO(v3): remove legacy lock path
|
|
3583
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
3584
|
+
this._emitInitialSession(id);
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3587
|
+
else {
|
|
3588
|
+
await this._emitInitialSession(id);
|
|
3589
|
+
}
|
|
3375
3590
|
})();
|
|
3376
3591
|
return { data: { subscription } };
|
|
3377
3592
|
}
|
|
@@ -3388,7 +3603,10 @@ class GoTrueClient {
|
|
|
3388
3603
|
catch (err) {
|
|
3389
3604
|
await ((_b = this.stateChangeEmitters.get(id)) === null || _b === void 0 ? void 0 : _b.callback('INITIAL_SESSION', null));
|
|
3390
3605
|
this._debug('INITIAL_SESSION', 'callback id', id, 'error', err);
|
|
3391
|
-
if (isAuthSessionMissingError(err)) {
|
|
3606
|
+
if (isAuthSessionMissingError(err) || isAuthRetryableFetchError(err)) {
|
|
3607
|
+
// A missing session or a transient/aborted network failure (e.g. a
|
|
3608
|
+
// superseded page navigation cancelling the in-flight request) is
|
|
3609
|
+
// not an application error — warn rather than surface it raw.
|
|
3392
3610
|
console.warn(err);
|
|
3393
3611
|
}
|
|
3394
3612
|
else {
|
|
@@ -3584,7 +3802,7 @@ class GoTrueClient {
|
|
|
3584
3802
|
var _a;
|
|
3585
3803
|
try {
|
|
3586
3804
|
const { data, error } = await this._useSession(async (result) => {
|
|
3587
|
-
var _a, _b, _c, _d,
|
|
3805
|
+
var _a, _b, _c, _d, _f;
|
|
3588
3806
|
const { data, error } = result;
|
|
3589
3807
|
if (error)
|
|
3590
3808
|
throw error;
|
|
@@ -3596,7 +3814,7 @@ class GoTrueClient {
|
|
|
3596
3814
|
});
|
|
3597
3815
|
return await _request(this.fetch, 'GET', url, {
|
|
3598
3816
|
headers: this.headers,
|
|
3599
|
-
jwt: (
|
|
3817
|
+
jwt: (_f = (_d = data.session) === null || _d === void 0 ? void 0 : _d.access_token) !== null && _f !== void 0 ? _f : undefined,
|
|
3600
3818
|
});
|
|
3601
3819
|
});
|
|
3602
3820
|
if (error)
|
|
@@ -3713,7 +3931,10 @@ class GoTrueClient {
|
|
|
3713
3931
|
* @param refreshToken A valid refresh token that was returned on login.
|
|
3714
3932
|
*/
|
|
3715
3933
|
async _refreshAccessToken(refreshToken) {
|
|
3716
|
-
|
|
3934
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
3935
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
3936
|
+
// enabled (logs may be forwarded to third-party services).
|
|
3937
|
+
const debugName = `#_refreshAccessToken()`;
|
|
3717
3938
|
this._debug(debugName, 'begin');
|
|
3718
3939
|
try {
|
|
3719
3940
|
const startedAt = Date.now();
|
|
@@ -3820,10 +4041,17 @@ class GoTrueClient {
|
|
|
3820
4041
|
if (this.autoRefreshToken && currentSession.refresh_token) {
|
|
3821
4042
|
const { error } = await this._callRefreshToken(currentSession.refresh_token);
|
|
3822
4043
|
if (error) {
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
4044
|
+
// `_callRefreshToken` is the single source of truth for refresh
|
|
4045
|
+
// outcomes: it removes the session itself when the access token
|
|
4046
|
+
// is actually expired, and preserves it when the token is still
|
|
4047
|
+
// valid (proactive-preserve). Don't second-guess that here — a
|
|
4048
|
+
// local `_removeSession` would emit a duplicate `SIGNED_OUT` on
|
|
4049
|
+
// genuine failures and undo the proactive-preserve at init time.
|
|
4050
|
+
if (isAuthRefreshDiscardedError(error)) {
|
|
4051
|
+
this._debug(debugName, 'refresh discarded by commit guard', error);
|
|
4052
|
+
}
|
|
4053
|
+
else {
|
|
4054
|
+
this._debug(debugName, 'refresh failed', error);
|
|
3827
4055
|
}
|
|
3828
4056
|
}
|
|
3829
4057
|
}
|
|
@@ -3856,7 +4084,16 @@ class GoTrueClient {
|
|
|
3856
4084
|
}
|
|
3857
4085
|
catch (err) {
|
|
3858
4086
|
this._debug(debugName, 'error', err);
|
|
3859
|
-
|
|
4087
|
+
if (isAuthRetryableFetchError(err)) {
|
|
4088
|
+
// Transient/aborted network failure during session recovery (e.g. a
|
|
4089
|
+
// superseded page navigation cancelling the in-flight request). Warn
|
|
4090
|
+
// rather than surface it raw; the session is untouched and recovery
|
|
4091
|
+
// will run again on the next load.
|
|
4092
|
+
console.warn(err);
|
|
4093
|
+
}
|
|
4094
|
+
else {
|
|
4095
|
+
console.error(err);
|
|
4096
|
+
}
|
|
3860
4097
|
return;
|
|
3861
4098
|
}
|
|
3862
4099
|
finally {
|
|
@@ -3872,18 +4109,91 @@ class GoTrueClient {
|
|
|
3872
4109
|
if (this.refreshingDeferred) {
|
|
3873
4110
|
return this.refreshingDeferred.promise;
|
|
3874
4111
|
}
|
|
3875
|
-
|
|
4112
|
+
// Serial failure cooldown: callers passing the *same* refresh token
|
|
4113
|
+
// after a recent failure receive the cached result instead of firing
|
|
4114
|
+
// another `/token` request. This caps the proactive-refresh storm
|
|
4115
|
+
// where every `getSession()` call inside the 90s EXPIRY_MARGIN_MS
|
|
4116
|
+
// window kept re-firing against the same broken refresh token during
|
|
4117
|
+
// outages. Concurrent callers already share `refreshingDeferred`; this
|
|
4118
|
+
// cache covers serial callers spaced across cooldown windows.
|
|
4119
|
+
//
|
|
4120
|
+
// Token-keyed so callers with a fresh refresh token (rotation pickup
|
|
4121
|
+
// from another tab, explicit `setSession`/`refreshSession({ refresh_token })`,
|
|
4122
|
+
// multi-account switch) bypass the cache and attempt a real refresh.
|
|
4123
|
+
if (this.lastRefreshFailure &&
|
|
4124
|
+
this.lastRefreshFailure.refreshToken === refreshToken &&
|
|
4125
|
+
Date.now() < this.lastRefreshFailure.expiresAt) {
|
|
4126
|
+
this._debug('#_callRefreshToken()', 'returning cached failure (cooldown active)');
|
|
4127
|
+
return this.lastRefreshFailure.result;
|
|
4128
|
+
}
|
|
4129
|
+
// Refresh tokens are long-lived bearer credentials; do NOT include any
|
|
4130
|
+
// fragment of the token in the debug tag, even when `debug: true` is
|
|
4131
|
+
// enabled (logs may be forwarded to third-party services).
|
|
4132
|
+
const debugName = `#_callRefreshToken()`;
|
|
3876
4133
|
this._debug(debugName, 'begin');
|
|
3877
4134
|
try {
|
|
3878
4135
|
this.refreshingDeferred = new Deferred();
|
|
4136
|
+
// Snapshot storage before the fetch. The commit guard discards the
|
|
4137
|
+
// rotated tokens only when a non-null pre-fetch snapshot changed under
|
|
4138
|
+
// us — typical case: a concurrent `signOut` ran `_removeSession`, or
|
|
4139
|
+
// another tab's refresh rewrote the slot. Callers passing
|
|
4140
|
+
// externally-sourced tokens (SSR cookie handoff, multi-account
|
|
4141
|
+
// switching, `setSession`/`refreshSession({ refresh_token })`) may
|
|
4142
|
+
// start from a null snapshot OR from a non-null snapshot whose
|
|
4143
|
+
// refresh_token differs from the one they're hydrating; in both
|
|
4144
|
+
// cases the guard fires only when storage was *modified between
|
|
4145
|
+
// snapshots*, not when the input token disagrees with what's stored.
|
|
4146
|
+
const storedAtStart = (await getItemAsync(this.storage, this.storageKey));
|
|
3879
4147
|
const { data, error } = await this._refreshAccessToken(refreshToken);
|
|
3880
4148
|
if (error)
|
|
3881
4149
|
throw error;
|
|
3882
4150
|
if (!data.session)
|
|
3883
4151
|
throw new AuthSessionMissingError();
|
|
4152
|
+
const storedAfter = (await getItemAsync(this.storage, this.storageKey));
|
|
4153
|
+
const storageChangedUnderUs = storedAtStart !== null &&
|
|
4154
|
+
(storedAfter === null || storedAfter.refresh_token !== storedAtStart.refresh_token);
|
|
4155
|
+
if (storageChangedUnderUs) {
|
|
4156
|
+
this._debug(debugName, 'commit guard: storage changed since refresh started, discarding rotated tokens', {
|
|
4157
|
+
// Presence indicators only — never log refresh token fragments,
|
|
4158
|
+
// even partial. Logs may be forwarded to third-party services.
|
|
4159
|
+
startedWith: 'present',
|
|
4160
|
+
nowHolds: storedAfter ? 'replaced' : 'cleared',
|
|
4161
|
+
});
|
|
4162
|
+
const discarded = {
|
|
4163
|
+
data: null,
|
|
4164
|
+
error: new AuthRefreshDiscardedError(),
|
|
4165
|
+
};
|
|
4166
|
+
this.refreshingDeferred.resolve(discarded);
|
|
4167
|
+
return discarded;
|
|
4168
|
+
}
|
|
4169
|
+
// Second leg of the commit guard: close the TOCTOU window between the
|
|
4170
|
+
// synchronous `storageChangedUnderUs` check and the actual storage
|
|
4171
|
+
// writes inside `_saveSession`. A concurrent `signOut → _removeSession`
|
|
4172
|
+
// can land inside `_saveSession`'s `await setItemAsync(...)` yields and
|
|
4173
|
+
// clear storage just before we overwrite it. Capture the epoch BEFORE
|
|
4174
|
+
// the save and re-check after; if it advanced, undo the write directly
|
|
4175
|
+
// (do NOT call `_removeSession` — that would emit a duplicate
|
|
4176
|
+
// SIGNED_OUT for the concurrent signOut that already fired one).
|
|
4177
|
+
const epochBeforeSave = this._sessionRemovalEpoch;
|
|
3884
4178
|
await this._saveSession(data.session);
|
|
4179
|
+
if (this._sessionRemovalEpoch !== epochBeforeSave) {
|
|
4180
|
+
this._debug(debugName, 'commit guard (post-save): _removeSession ran during _saveSession, undoing write');
|
|
4181
|
+
await removeItemAsync(this.storage, this.storageKey);
|
|
4182
|
+
if (this.userStorage) {
|
|
4183
|
+
await removeItemAsync(this.userStorage, this.storageKey + '-user');
|
|
4184
|
+
}
|
|
4185
|
+
const discarded = {
|
|
4186
|
+
data: null,
|
|
4187
|
+
error: new AuthRefreshDiscardedError(),
|
|
4188
|
+
};
|
|
4189
|
+
this.refreshingDeferred.resolve(discarded);
|
|
4190
|
+
return discarded;
|
|
4191
|
+
}
|
|
3885
4192
|
await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session);
|
|
3886
4193
|
const result = { data: data.session, error: null };
|
|
4194
|
+
// Refresh succeeded — clear any cached failure so the next caller
|
|
4195
|
+
// (including the auto-refresh ticker) attempts a real refresh again.
|
|
4196
|
+
this.lastRefreshFailure = null;
|
|
3887
4197
|
this.refreshingDeferred.resolve(result);
|
|
3888
4198
|
return result;
|
|
3889
4199
|
}
|
|
@@ -3892,8 +4202,35 @@ class GoTrueClient {
|
|
|
3892
4202
|
if (isAuthError(error)) {
|
|
3893
4203
|
const result = { data: null, error };
|
|
3894
4204
|
if (!isAuthRetryableFetchError(error)) {
|
|
3895
|
-
|
|
4205
|
+
// Proactive vs reactive distinction: a refresh fires whenever
|
|
4206
|
+
// the access token is within EXPIRY_MARGIN_MS of expiry. If the
|
|
4207
|
+
// access token is *still valid* at this moment, the refresh was
|
|
4208
|
+
// proactive and the existing session is still usable until its
|
|
4209
|
+
// real expiry — destroying it now would log out a user whose
|
|
4210
|
+
// access token works. If the access token has actually expired,
|
|
4211
|
+
// the refresh token is the only credential left and it just got
|
|
4212
|
+
// rejected — the session is genuinely dead. `__loadSession`
|
|
4213
|
+
// mirrors this distinction on the read path so callers see the
|
|
4214
|
+
// preserved session instead of `session: null`.
|
|
4215
|
+
const storedNow = (await getItemAsync(this.storage, this.storageKey));
|
|
4216
|
+
const accessTokenStillValid = !!((storedNow === null || storedNow === void 0 ? void 0 : storedNow.expires_at) && storedNow.expires_at * 1000 > Date.now());
|
|
4217
|
+
if (accessTokenStillValid) {
|
|
4218
|
+
this._debug(debugName, 'proactive refresh failed, access token still valid — preserving session');
|
|
4219
|
+
}
|
|
4220
|
+
else {
|
|
4221
|
+
await this._removeSession();
|
|
4222
|
+
}
|
|
3896
4223
|
}
|
|
4224
|
+
// Cache the failure so serial callers (and the next auto-refresh
|
|
4225
|
+
// tick) passing the same refresh token within the cooldown window
|
|
4226
|
+
// receive it synchronously instead of firing another `/token`
|
|
4227
|
+
// call. Set after the optional `_removeSession` above (which
|
|
4228
|
+
// clears the cache as part of teardown) so the cache survives.
|
|
4229
|
+
this.lastRefreshFailure = {
|
|
4230
|
+
refreshToken,
|
|
4231
|
+
result,
|
|
4232
|
+
expiresAt: Date.now() + REFRESH_FAILURE_COOLDOWN_MS,
|
|
4233
|
+
};
|
|
3897
4234
|
(_a = this.refreshingDeferred) === null || _a === void 0 ? void 0 : _a.resolve(result);
|
|
3898
4235
|
return result;
|
|
3899
4236
|
}
|
|
@@ -3906,6 +4243,19 @@ class GoTrueClient {
|
|
|
3906
4243
|
}
|
|
3907
4244
|
}
|
|
3908
4245
|
async _notifyAllSubscribers(event, session, broadcast = true) {
|
|
4246
|
+
if (this._pendingInitNotifications !== null && broadcast) {
|
|
4247
|
+
// We're inside initialize() before initializePromise has resolved, and
|
|
4248
|
+
// this notification originates from the init chain (_recoverAndRefresh),
|
|
4249
|
+
// which always fires with broadcast = true. Enqueue instead of firing so
|
|
4250
|
+
// that callbacks can safely call getSession() / getUser() without
|
|
4251
|
+
// deadlocking on initializePromise.
|
|
4252
|
+
//
|
|
4253
|
+
// Notifications with broadcast = false come from the incoming
|
|
4254
|
+
// BroadcastChannel handler (a cross-tab event), not the init chain, so
|
|
4255
|
+
// they are fired immediately to preserve multi-tab ordering.
|
|
4256
|
+
this._pendingInitNotifications.push({ event, session, broadcast });
|
|
4257
|
+
return;
|
|
4258
|
+
}
|
|
3909
4259
|
const debugName = `#_notifyAllSubscribers(${event})`;
|
|
3910
4260
|
this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`);
|
|
3911
4261
|
try {
|
|
@@ -3942,7 +4292,6 @@ class GoTrueClient {
|
|
|
3942
4292
|
// _saveSession is always called whenever a new session has been acquired
|
|
3943
4293
|
// so we can safely suppress the warning returned by future getSession calls
|
|
3944
4294
|
this.suppressGetSessionWarning = true;
|
|
3945
|
-
await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`);
|
|
3946
4295
|
// Create a shallow copy to work with, to avoid mutating the original session object if it's used elsewhere
|
|
3947
4296
|
const sessionToProcess = Object.assign({}, session);
|
|
3948
4297
|
const userIsProxy = sessionToProcess.user && sessionToProcess.user.__isUserNotAvailableProxy === true;
|
|
@@ -3975,7 +4324,15 @@ class GoTrueClient {
|
|
|
3975
4324
|
}
|
|
3976
4325
|
}
|
|
3977
4326
|
async _removeSession() {
|
|
4327
|
+
// Bump synchronously, BEFORE any `await`, so that `_callRefreshToken`'s
|
|
4328
|
+
// post-save check sees the increment whenever this method has started —
|
|
4329
|
+
// even if it hasn't finished. Pairs with the epoch check in
|
|
4330
|
+
// `_callRefreshToken`. See `_sessionRemovalEpoch` field doc.
|
|
4331
|
+
this._sessionRemovalEpoch += 1;
|
|
3978
4332
|
this._debug('#_removeSession()');
|
|
4333
|
+
// The session is gone — no point holding on to a cached refresh failure
|
|
4334
|
+
// for a token that no longer exists. Synchronous, before any `await`.
|
|
4335
|
+
this.lastRefreshFailure = null;
|
|
3979
4336
|
this.suppressGetSessionWarning = false;
|
|
3980
4337
|
await removeItemAsync(this.storage, this.storageKey);
|
|
3981
4338
|
await removeItemAsync(this.storage, this.storageKey + '-code-verifier');
|
|
@@ -3988,8 +4345,8 @@ class GoTrueClient {
|
|
|
3988
4345
|
/**
|
|
3989
4346
|
* Removes any registered visibilitychange callback.
|
|
3990
4347
|
*
|
|
3991
|
-
* {@
|
|
3992
|
-
* {@
|
|
4348
|
+
* {@link GoTrueClient.startAutoRefresh}
|
|
4349
|
+
* {@link GoTrueClient.stopAutoRefresh}
|
|
3993
4350
|
*/
|
|
3994
4351
|
_removeVisibilityChangedCallback() {
|
|
3995
4352
|
this._debug('#_removeVisibilityChangedCallback()');
|
|
@@ -4005,7 +4362,7 @@ class GoTrueClient {
|
|
|
4005
4362
|
}
|
|
4006
4363
|
}
|
|
4007
4364
|
/**
|
|
4008
|
-
* This is the private implementation of {@link
|
|
4365
|
+
* This is the private implementation of {@link GoTrueClient.startAutoRefresh}. Use this
|
|
4009
4366
|
* within the library.
|
|
4010
4367
|
*/
|
|
4011
4368
|
async _startAutoRefresh() {
|
|
@@ -4047,7 +4404,7 @@ class GoTrueClient {
|
|
|
4047
4404
|
}
|
|
4048
4405
|
}
|
|
4049
4406
|
/**
|
|
4050
|
-
* This is the private implementation of {@link
|
|
4407
|
+
* This is the private implementation of {@link GoTrueClient.stopAutoRefresh}. Use this
|
|
4051
4408
|
* within the library.
|
|
4052
4409
|
*/
|
|
4053
4410
|
async _stopAutoRefresh() {
|
|
@@ -4083,7 +4440,7 @@ class GoTrueClient {
|
|
|
4083
4440
|
* platform's foreground indication mechanism and call these methods
|
|
4084
4441
|
* appropriately to conserve resources.
|
|
4085
4442
|
*
|
|
4086
|
-
* {@
|
|
4443
|
+
* {@link GoTrueClient.stopAutoRefresh}
|
|
4087
4444
|
*
|
|
4088
4445
|
* @category Auth
|
|
4089
4446
|
*
|
|
@@ -4117,7 +4474,7 @@ class GoTrueClient {
|
|
|
4117
4474
|
* If you call this method any managed visibility change callback will be
|
|
4118
4475
|
* removed and you must manage visibility changes on your own.
|
|
4119
4476
|
*
|
|
4120
|
-
* See {@link
|
|
4477
|
+
* See {@link GoTrueClient.startAutoRefresh} for more details.
|
|
4121
4478
|
*
|
|
4122
4479
|
* @category Auth
|
|
4123
4480
|
*
|
|
@@ -4145,47 +4502,122 @@ class GoTrueClient {
|
|
|
4145
4502
|
this._removeVisibilityChangedCallback();
|
|
4146
4503
|
await this._stopAutoRefresh();
|
|
4147
4504
|
}
|
|
4505
|
+
/**
|
|
4506
|
+
* Tears down the client's background work: stops the auto-refresh interval,
|
|
4507
|
+
* removes the `visibilitychange` listener, closes the cross-tab
|
|
4508
|
+
* `BroadcastChannel`, and clears registered `onAuthStateChange` subscribers.
|
|
4509
|
+
*
|
|
4510
|
+
* Call this from cleanup hooks when the client is being replaced before
|
|
4511
|
+
* its JS realm is destroyed. React Strict Mode and HMR are the common
|
|
4512
|
+
* cases. Any in-flight `fetch` calls continue to completion and may still
|
|
4513
|
+
* write to storage; dispose doesn't abort them or erase storage.
|
|
4514
|
+
*
|
|
4515
|
+
* Lifecycle caveat: because in-flight refreshes are not aborted, a
|
|
4516
|
+
* disposed instance can still persist a rotated session to storage after
|
|
4517
|
+
* `dispose()` returns. A subsequent `createClient` against the same
|
|
4518
|
+
* `storageKey` will pick up that session on its next read. If you need
|
|
4519
|
+
* strict isolation between client lifecycles, await any pending auth
|
|
4520
|
+
* operation before calling `dispose()` (or change the `storageKey` for
|
|
4521
|
+
* the replacement client).
|
|
4522
|
+
*
|
|
4523
|
+
* Safe to call repeatedly.
|
|
4524
|
+
*
|
|
4525
|
+
* @category Auth
|
|
4526
|
+
*
|
|
4527
|
+
* @example Cleanup on React unmount
|
|
4528
|
+
* ```ts
|
|
4529
|
+
* useEffect(() => {
|
|
4530
|
+
* const client = createClient(...)
|
|
4531
|
+
* return () => { client.auth.dispose() }
|
|
4532
|
+
* }, [])
|
|
4533
|
+
* ```
|
|
4534
|
+
*/
|
|
4535
|
+
async dispose() {
|
|
4536
|
+
var _a;
|
|
4537
|
+
this._removeVisibilityChangedCallback();
|
|
4538
|
+
await this._stopAutoRefresh();
|
|
4539
|
+
(_a = this.broadcastChannel) === null || _a === void 0 ? void 0 : _a.close();
|
|
4540
|
+
this.broadcastChannel = null;
|
|
4541
|
+
this.stateChangeEmitters.clear();
|
|
4542
|
+
}
|
|
4148
4543
|
/**
|
|
4149
4544
|
* Runs the auto refresh token tick.
|
|
4150
4545
|
*/
|
|
4151
4546
|
async _autoRefreshTokenTick() {
|
|
4152
4547
|
this._debug('#_autoRefreshTokenTick()', 'begin');
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4548
|
+
if (this.lock != null) {
|
|
4549
|
+
// TODO(v3): remove legacy lock path. Uses `_acquireLock(0, ...)` which
|
|
4550
|
+
// throws `LockAcquireTimeoutError` immediately if the lock is held —
|
|
4551
|
+
// that's the fail-fast skip path that lets the tick bail out instead
|
|
4552
|
+
// of queuing behind a long-running operation.
|
|
4553
|
+
try {
|
|
4554
|
+
await this._acquireLock(0, async () => {
|
|
4157
4555
|
try {
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4556
|
+
const now = Date.now();
|
|
4557
|
+
try {
|
|
4558
|
+
return await this._useSession(async (result) => {
|
|
4559
|
+
const { data: { session }, } = result;
|
|
4560
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
4561
|
+
this._debug('#_autoRefreshTokenTick()', 'no session');
|
|
4562
|
+
return;
|
|
4563
|
+
}
|
|
4564
|
+
const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS);
|
|
4565
|
+
this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`);
|
|
4566
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
4567
|
+
await this._callRefreshToken(session.refresh_token);
|
|
4568
|
+
}
|
|
4569
|
+
});
|
|
4570
|
+
}
|
|
4571
|
+
catch (e) {
|
|
4572
|
+
console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
|
|
4573
|
+
}
|
|
4171
4574
|
}
|
|
4172
|
-
|
|
4173
|
-
|
|
4575
|
+
finally {
|
|
4576
|
+
this._debug('#_autoRefreshTokenTick()', 'end');
|
|
4174
4577
|
}
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4580
|
+
catch (e) {
|
|
4581
|
+
if (e instanceof LockAcquireTimeoutError) {
|
|
4582
|
+
this._debug('auto refresh token tick lock not available');
|
|
4175
4583
|
}
|
|
4176
|
-
|
|
4177
|
-
|
|
4584
|
+
else {
|
|
4585
|
+
throw e;
|
|
4178
4586
|
}
|
|
4179
|
-
}
|
|
4587
|
+
}
|
|
4588
|
+
return;
|
|
4180
4589
|
}
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4590
|
+
// Lockless default: skip if a refresh is already in flight.
|
|
4591
|
+
// `_callRefreshToken` also dedupes via the same field; this is just a
|
|
4592
|
+
// fast-path skip to avoid an unnecessary storage read.
|
|
4593
|
+
if (this.refreshingDeferred !== null) {
|
|
4594
|
+
this._debug('#_autoRefreshTokenTick()', 'refresh already in flight, skipping');
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
try {
|
|
4598
|
+
const now = Date.now();
|
|
4599
|
+
try {
|
|
4600
|
+
await this._useSession(async (result) => {
|
|
4601
|
+
const { data: { session }, } = result;
|
|
4602
|
+
if (!session || !session.refresh_token || !session.expires_at) {
|
|
4603
|
+
this._debug('#_autoRefreshTokenTick()', 'no session');
|
|
4604
|
+
return;
|
|
4605
|
+
}
|
|
4606
|
+
// session will expire in this many ticks (or has already expired if <= 0)
|
|
4607
|
+
const expiresInTicks = Math.floor((session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS);
|
|
4608
|
+
this._debug('#_autoRefreshTokenTick()', `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`);
|
|
4609
|
+
if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
|
|
4610
|
+
await this._callRefreshToken(session.refresh_token);
|
|
4611
|
+
}
|
|
4612
|
+
});
|
|
4184
4613
|
}
|
|
4185
|
-
|
|
4186
|
-
|
|
4614
|
+
catch (e) {
|
|
4615
|
+
console.error('Auto refresh tick failed with error. This is likely a transient error.', e);
|
|
4187
4616
|
}
|
|
4188
4617
|
}
|
|
4618
|
+
finally {
|
|
4619
|
+
this._debug('#_autoRefreshTokenTick()', 'end');
|
|
4620
|
+
}
|
|
4189
4621
|
}
|
|
4190
4622
|
/**
|
|
4191
4623
|
* Registers callbacks on the browser / platform, which in-turn run
|
|
@@ -4234,18 +4666,26 @@ class GoTrueClient {
|
|
|
4234
4666
|
if (!calledFromInitialize) {
|
|
4235
4667
|
// called when the visibility has changed, i.e. the browser
|
|
4236
4668
|
// transitioned from hidden -> visible so we need to see if the session
|
|
4237
|
-
// should be recovered
|
|
4238
|
-
// the lock first asynchronously
|
|
4669
|
+
// should be recovered
|
|
4239
4670
|
await this.initializePromise;
|
|
4240
|
-
|
|
4671
|
+
if (this.lock != null) {
|
|
4672
|
+
// TODO(v3): remove legacy lock path
|
|
4673
|
+
await this._acquireLock(this.lockAcquireTimeout, async () => {
|
|
4674
|
+
if (document.visibilityState !== 'visible') {
|
|
4675
|
+
this._debug(methodName, 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting');
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
4678
|
+
await this._recoverAndRefresh();
|
|
4679
|
+
});
|
|
4680
|
+
}
|
|
4681
|
+
else {
|
|
4241
4682
|
if (document.visibilityState !== 'visible') {
|
|
4242
|
-
this._debug(methodName, '
|
|
4243
|
-
// visibility has changed while waiting for the lock, abort
|
|
4683
|
+
this._debug(methodName, 'visibilityState is no longer visible, skipping recovery');
|
|
4244
4684
|
return;
|
|
4245
4685
|
}
|
|
4246
4686
|
// recover the session
|
|
4247
4687
|
await this._recoverAndRefresh();
|
|
4248
|
-
}
|
|
4688
|
+
}
|
|
4249
4689
|
}
|
|
4250
4690
|
}
|
|
4251
4691
|
else if (document.visibilityState === 'hidden') {
|
|
@@ -4341,7 +4781,7 @@ class GoTrueClient {
|
|
|
4341
4781
|
}
|
|
4342
4782
|
}
|
|
4343
4783
|
async _verify(params) {
|
|
4344
|
-
|
|
4784
|
+
const run = async () => {
|
|
4345
4785
|
try {
|
|
4346
4786
|
return await this._useSession(async (result) => {
|
|
4347
4787
|
var _a;
|
|
@@ -4375,10 +4815,15 @@ class GoTrueClient {
|
|
|
4375
4815
|
}
|
|
4376
4816
|
throw error;
|
|
4377
4817
|
}
|
|
4378
|
-
}
|
|
4818
|
+
};
|
|
4819
|
+
if (this.lock != null) {
|
|
4820
|
+
// TODO(v3): remove legacy lock path
|
|
4821
|
+
return this._acquireLock(this.lockAcquireTimeout, run);
|
|
4822
|
+
}
|
|
4823
|
+
return run();
|
|
4379
4824
|
}
|
|
4380
4825
|
async _challenge(params) {
|
|
4381
|
-
|
|
4826
|
+
const run = async () => {
|
|
4382
4827
|
try {
|
|
4383
4828
|
return await this._useSession(async (result) => {
|
|
4384
4829
|
var _a;
|
|
@@ -4418,14 +4863,17 @@ class GoTrueClient {
|
|
|
4418
4863
|
}
|
|
4419
4864
|
throw error;
|
|
4420
4865
|
}
|
|
4421
|
-
}
|
|
4866
|
+
};
|
|
4867
|
+
if (this.lock != null) {
|
|
4868
|
+
// TODO(v3): remove legacy lock path
|
|
4869
|
+
return this._acquireLock(this.lockAcquireTimeout, run);
|
|
4870
|
+
}
|
|
4871
|
+
return run();
|
|
4422
4872
|
}
|
|
4423
4873
|
/**
|
|
4424
|
-
* {@
|
|
4874
|
+
* {@link GoTrueMFAApi#challengeAndVerify}
|
|
4425
4875
|
*/
|
|
4426
4876
|
async _challengeAndVerify(params) {
|
|
4427
|
-
// both _challenge and _verify independently acquire the lock, so no need
|
|
4428
|
-
// to acquire it here
|
|
4429
4877
|
const { data: challengeData, error: challengeError } = await this._challenge({
|
|
4430
4878
|
factorId: params.factorId,
|
|
4431
4879
|
});
|
|
@@ -4439,11 +4887,10 @@ class GoTrueClient {
|
|
|
4439
4887
|
});
|
|
4440
4888
|
}
|
|
4441
4889
|
/**
|
|
4442
|
-
* {@
|
|
4890
|
+
* {@link GoTrueMFAApi#listFactors}
|
|
4443
4891
|
*/
|
|
4444
4892
|
async _listFactors() {
|
|
4445
4893
|
var _a;
|
|
4446
|
-
// use #getUser instead of #_getUser as the former acquires a lock
|
|
4447
4894
|
const { data: { user }, error: userError, } = await this.getUser();
|
|
4448
4895
|
if (userError) {
|
|
4449
4896
|
return { data: null, error: userError };
|
|
@@ -4468,7 +4915,7 @@ class GoTrueClient {
|
|
|
4468
4915
|
};
|
|
4469
4916
|
}
|
|
4470
4917
|
/**
|
|
4471
|
-
* {@
|
|
4918
|
+
* {@link GoTrueMFAApi#getAuthenticatorAssuranceLevel}
|
|
4472
4919
|
*/
|
|
4473
4920
|
async _getAuthenticatorAssuranceLevel(jwt) {
|
|
4474
4921
|
var _a, _b, _c, _d;
|
|
@@ -4719,15 +5166,15 @@ class GoTrueClient {
|
|
|
4719
5166
|
* Extracts the JWT claims present in the access token by first verifying the
|
|
4720
5167
|
* JWT against the server's JSON Web Key Set endpoint
|
|
4721
5168
|
* `/.well-known/jwks.json` which is often cached, resulting in significantly
|
|
4722
|
-
* faster responses. Prefer this method over {@link
|
|
5169
|
+
* faster responses. Prefer this method over {@link GoTrueClient.getUser} which always
|
|
4723
5170
|
* sends a request to the Auth server for each JWT.
|
|
4724
5171
|
*
|
|
4725
5172
|
* If the project is not using an asymmetric JWT signing key (like ECC or
|
|
4726
|
-
* RSA) it always sends a request to the Auth server (similar to
|
|
4727
|
-
*
|
|
5173
|
+
* RSA) it always sends a request to the Auth server (similar to
|
|
5174
|
+
* {@link GoTrueClient.getUser}) to verify the JWT.
|
|
4728
5175
|
*
|
|
4729
5176
|
* @param jwt An optional specific JWT you wish to verify, not the one you
|
|
4730
|
-
* can obtain from {@link
|
|
5177
|
+
* can obtain from {@link GoTrueClient.getSession}.
|
|
4731
5178
|
* @param options Various additional options that allow you to customize the
|
|
4732
5179
|
* behavior of this method.
|
|
4733
5180
|
*
|
|
@@ -4792,8 +5239,14 @@ class GoTrueClient {
|
|
|
4792
5239
|
}
|
|
4793
5240
|
const { header, payload, signature, raw: { header: rawHeader, payload: rawPayload }, } = decodeJWT(token);
|
|
4794
5241
|
if (!(options === null || options === void 0 ? void 0 : options.allowExpired)) {
|
|
4795
|
-
// Reject expired JWTs should only happen if jwt argument was passed
|
|
4796
|
-
|
|
5242
|
+
// Reject expired JWTs should only happen if jwt argument was passed.
|
|
5243
|
+
// Rethrow as AuthInvalidJwtError so the outer catch converts it to { data, error }.
|
|
5244
|
+
try {
|
|
5245
|
+
validateExp(payload.exp);
|
|
5246
|
+
}
|
|
5247
|
+
catch (e) {
|
|
5248
|
+
throw new AuthInvalidJwtError(e instanceof Error ? e.message : 'JWT validation failed');
|
|
5249
|
+
}
|
|
4797
5250
|
}
|
|
4798
5251
|
const signingKey = !header.alg ||
|
|
4799
5252
|
header.alg.startsWith('HS') ||
|
|
@@ -4844,6 +5297,335 @@ class GoTrueClient {
|
|
|
4844
5297
|
throw error;
|
|
4845
5298
|
}
|
|
4846
5299
|
}
|
|
5300
|
+
// --- Passkey Methods ---
|
|
5301
|
+
/**
|
|
5302
|
+
* Sign in with a passkey. Handles the full WebAuthn ceremony:
|
|
5303
|
+
* 1. Fetches authentication challenge from server
|
|
5304
|
+
* 2. Prompts user via navigator.credentials.get()
|
|
5305
|
+
* 3. Verifies credential with server and creates session
|
|
5306
|
+
*
|
|
5307
|
+
* Requires `auth.experimental.passkey: true`.
|
|
5308
|
+
*
|
|
5309
|
+
* @category Auth
|
|
5310
|
+
*/
|
|
5311
|
+
async signInWithPasskey(credentials) {
|
|
5312
|
+
var _a, _b, _c;
|
|
5313
|
+
assertPasskeyExperimentalEnabled(this.experimental);
|
|
5314
|
+
try {
|
|
5315
|
+
if (!browserSupportsWebAuthn()) {
|
|
5316
|
+
return this._returnResult({
|
|
5317
|
+
data: null,
|
|
5318
|
+
error: new AuthUnknownError('Browser does not support WebAuthn', null),
|
|
5319
|
+
});
|
|
5320
|
+
}
|
|
5321
|
+
// 1. Get challenge options from server
|
|
5322
|
+
const { data: options, error: optionsError } = await this._startPasskeyAuthentication({
|
|
5323
|
+
options: { captchaToken: (_a = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _a === void 0 ? void 0 : _a.captchaToken },
|
|
5324
|
+
});
|
|
5325
|
+
if (optionsError || !options) {
|
|
5326
|
+
return this._returnResult({ data: null, error: optionsError });
|
|
5327
|
+
}
|
|
5328
|
+
// 2. Deserialize and prompt user via browser WebAuthn API
|
|
5329
|
+
const publicKeyOptions = deserializeCredentialRequestOptions(options.options);
|
|
5330
|
+
const signal = (_c = (_b = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _b === void 0 ? void 0 : _b.signal) !== null && _c !== void 0 ? _c : webAuthnAbortService.createNewAbortSignal();
|
|
5331
|
+
const { data: credential, error: credentialError } = await getCredential({
|
|
5332
|
+
publicKey: publicKeyOptions,
|
|
5333
|
+
signal,
|
|
5334
|
+
});
|
|
5335
|
+
if (credentialError || !credential) {
|
|
5336
|
+
return this._returnResult({
|
|
5337
|
+
data: null,
|
|
5338
|
+
error: credentialError !== null && credentialError !== void 0 ? credentialError : new AuthUnknownError('WebAuthn ceremony failed', null),
|
|
5339
|
+
});
|
|
5340
|
+
}
|
|
5341
|
+
// 3. Serialize and verify with server
|
|
5342
|
+
const serialized = serializeCredentialRequestResponse(credential);
|
|
5343
|
+
return this._verifyPasskeyAuthentication({
|
|
5344
|
+
challengeId: options.challenge_id,
|
|
5345
|
+
credential: serialized,
|
|
5346
|
+
});
|
|
5347
|
+
}
|
|
5348
|
+
catch (error) {
|
|
5349
|
+
if (isAuthError(error)) {
|
|
5350
|
+
return this._returnResult({ data: null, error });
|
|
5351
|
+
}
|
|
5352
|
+
throw error;
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
/**
|
|
5356
|
+
* Register a passkey for the current authenticated user. Handles the full WebAuthn ceremony:
|
|
5357
|
+
* 1. Fetches registration challenge from server
|
|
5358
|
+
* 2. Prompts user via navigator.credentials.create()
|
|
5359
|
+
* 3. Verifies credential with server
|
|
5360
|
+
*
|
|
5361
|
+
* Requires an active session. Requires `auth.experimental.passkey: true`.
|
|
5362
|
+
*
|
|
5363
|
+
* @category Auth
|
|
5364
|
+
*/
|
|
5365
|
+
async registerPasskey(credentials) {
|
|
5366
|
+
var _a, _b;
|
|
5367
|
+
assertPasskeyExperimentalEnabled(this.experimental);
|
|
5368
|
+
try {
|
|
5369
|
+
if (!browserSupportsWebAuthn()) {
|
|
5370
|
+
return this._returnResult({
|
|
5371
|
+
data: null,
|
|
5372
|
+
error: new AuthUnknownError('Browser does not support WebAuthn', null),
|
|
5373
|
+
});
|
|
5374
|
+
}
|
|
5375
|
+
// 1. Get challenge options from server
|
|
5376
|
+
const { data: options, error: optionsError } = await this._startPasskeyRegistration();
|
|
5377
|
+
if (optionsError || !options) {
|
|
5378
|
+
return this._returnResult({ data: null, error: optionsError });
|
|
5379
|
+
}
|
|
5380
|
+
// 2. Deserialize and prompt user via browser WebAuthn API
|
|
5381
|
+
const publicKeyOptions = deserializeCredentialCreationOptions(options.options);
|
|
5382
|
+
const signal = (_b = (_a = credentials === null || credentials === void 0 ? void 0 : credentials.options) === null || _a === void 0 ? void 0 : _a.signal) !== null && _b !== void 0 ? _b : webAuthnAbortService.createNewAbortSignal();
|
|
5383
|
+
const { data: credential, error: credentialError } = await createCredential({
|
|
5384
|
+
publicKey: publicKeyOptions,
|
|
5385
|
+
signal,
|
|
5386
|
+
});
|
|
5387
|
+
if (credentialError || !credential) {
|
|
5388
|
+
return this._returnResult({
|
|
5389
|
+
data: null,
|
|
5390
|
+
error: credentialError !== null && credentialError !== void 0 ? credentialError : new AuthUnknownError('WebAuthn ceremony failed', null),
|
|
5391
|
+
});
|
|
5392
|
+
}
|
|
5393
|
+
// 3. Serialize and verify with server
|
|
5394
|
+
const serialized = serializeCredentialCreationResponse(credential);
|
|
5395
|
+
return this._verifyPasskeyRegistration({
|
|
5396
|
+
challengeId: options.challenge_id,
|
|
5397
|
+
credential: serialized,
|
|
5398
|
+
});
|
|
5399
|
+
}
|
|
5400
|
+
catch (error) {
|
|
5401
|
+
if (isAuthError(error)) {
|
|
5402
|
+
return this._returnResult({ data: null, error });
|
|
5403
|
+
}
|
|
5404
|
+
throw error;
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
/**
|
|
5408
|
+
* Start passkey registration for the current authenticated user.
|
|
5409
|
+
* Returns WebAuthn credential creation options to pass to navigator.credentials.create().
|
|
5410
|
+
*/
|
|
5411
|
+
async _startPasskeyRegistration() {
|
|
5412
|
+
assertPasskeyExperimentalEnabled(this.experimental);
|
|
5413
|
+
try {
|
|
5414
|
+
return await this._useSession(async (result) => {
|
|
5415
|
+
const { data: { session }, error: sessionError, } = result;
|
|
5416
|
+
if (sessionError) {
|
|
5417
|
+
return this._returnResult({ data: null, error: sessionError });
|
|
5418
|
+
}
|
|
5419
|
+
if (!session) {
|
|
5420
|
+
return this._returnResult({ data: null, error: new AuthSessionMissingError() });
|
|
5421
|
+
}
|
|
5422
|
+
const { data, error } = await _request(this.fetch, 'POST', `${this.url}/passkeys/registration/options`, {
|
|
5423
|
+
headers: this.headers,
|
|
5424
|
+
jwt: session.access_token,
|
|
5425
|
+
body: {},
|
|
5426
|
+
});
|
|
5427
|
+
if (error) {
|
|
5428
|
+
return this._returnResult({ data: null, error });
|
|
5429
|
+
}
|
|
5430
|
+
return this._returnResult({ data, error: null });
|
|
5431
|
+
});
|
|
5432
|
+
}
|
|
5433
|
+
catch (error) {
|
|
5434
|
+
if (isAuthError(error)) {
|
|
5435
|
+
return this._returnResult({ data: null, error });
|
|
5436
|
+
}
|
|
5437
|
+
throw error;
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
5440
|
+
/**
|
|
5441
|
+
* Verify passkey registration with the credential response.
|
|
5442
|
+
* The credentialResponse should be the serialized output of navigator.credentials.create().
|
|
5443
|
+
*/
|
|
5444
|
+
async _verifyPasskeyRegistration(params) {
|
|
5445
|
+
assertPasskeyExperimentalEnabled(this.experimental);
|
|
5446
|
+
try {
|
|
5447
|
+
return await this._useSession(async (result) => {
|
|
5448
|
+
const { data: { session }, error: sessionError, } = result;
|
|
5449
|
+
if (sessionError) {
|
|
5450
|
+
return this._returnResult({ data: null, error: sessionError });
|
|
5451
|
+
}
|
|
5452
|
+
if (!session) {
|
|
5453
|
+
return this._returnResult({ data: null, error: new AuthSessionMissingError() });
|
|
5454
|
+
}
|
|
5455
|
+
const { data, error } = await _request(this.fetch, 'POST', `${this.url}/passkeys/registration/verify`, {
|
|
5456
|
+
headers: this.headers,
|
|
5457
|
+
jwt: session.access_token,
|
|
5458
|
+
body: {
|
|
5459
|
+
challenge_id: params.challengeId,
|
|
5460
|
+
credential: params.credential,
|
|
5461
|
+
},
|
|
5462
|
+
});
|
|
5463
|
+
if (error) {
|
|
5464
|
+
return this._returnResult({ data: null, error });
|
|
5465
|
+
}
|
|
5466
|
+
return this._returnResult({ data, error: null });
|
|
5467
|
+
});
|
|
5468
|
+
}
|
|
5469
|
+
catch (error) {
|
|
5470
|
+
if (isAuthError(error)) {
|
|
5471
|
+
return this._returnResult({ data: null, error });
|
|
5472
|
+
}
|
|
5473
|
+
throw error;
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
/**
|
|
5477
|
+
* Start passkey authentication.
|
|
5478
|
+
* Returns WebAuthn credential request options to pass to navigator.credentials.get().
|
|
5479
|
+
*/
|
|
5480
|
+
async _startPasskeyAuthentication(params) {
|
|
5481
|
+
var _a;
|
|
5482
|
+
assertPasskeyExperimentalEnabled(this.experimental);
|
|
5483
|
+
try {
|
|
5484
|
+
const { data, error } = await _request(this.fetch, 'POST', `${this.url}/passkeys/authentication/options`, {
|
|
5485
|
+
headers: this.headers,
|
|
5486
|
+
body: {
|
|
5487
|
+
gotrue_meta_security: { captcha_token: (_a = params === null || params === void 0 ? void 0 : params.options) === null || _a === void 0 ? void 0 : _a.captchaToken },
|
|
5488
|
+
},
|
|
5489
|
+
});
|
|
5490
|
+
if (error) {
|
|
5491
|
+
return this._returnResult({ data: null, error });
|
|
5492
|
+
}
|
|
5493
|
+
return this._returnResult({ data, error: null });
|
|
5494
|
+
}
|
|
5495
|
+
catch (error) {
|
|
5496
|
+
if (isAuthError(error)) {
|
|
5497
|
+
return this._returnResult({ data: null, error });
|
|
5498
|
+
}
|
|
5499
|
+
throw error;
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
/**
|
|
5503
|
+
* Verify passkey authentication and create a session.
|
|
5504
|
+
* The credential should be the serialized output of navigator.credentials.get().
|
|
5505
|
+
*/
|
|
5506
|
+
async _verifyPasskeyAuthentication(params) {
|
|
5507
|
+
assertPasskeyExperimentalEnabled(this.experimental);
|
|
5508
|
+
try {
|
|
5509
|
+
const { data, error } = await _request(this.fetch, 'POST', `${this.url}/passkeys/authentication/verify`, {
|
|
5510
|
+
headers: this.headers,
|
|
5511
|
+
body: {
|
|
5512
|
+
challenge_id: params.challengeId,
|
|
5513
|
+
credential: params.credential,
|
|
5514
|
+
},
|
|
5515
|
+
xform: _sessionResponse,
|
|
5516
|
+
});
|
|
5517
|
+
if (error) {
|
|
5518
|
+
return this._returnResult({ data: null, error });
|
|
5519
|
+
}
|
|
5520
|
+
if (data.session) {
|
|
5521
|
+
await this._saveSession(data.session);
|
|
5522
|
+
await this._notifyAllSubscribers('SIGNED_IN', data.session);
|
|
5523
|
+
}
|
|
5524
|
+
return this._returnResult({ data, error: null });
|
|
5525
|
+
}
|
|
5526
|
+
catch (error) {
|
|
5527
|
+
if (isAuthError(error)) {
|
|
5528
|
+
return this._returnResult({ data: null, error });
|
|
5529
|
+
}
|
|
5530
|
+
throw error;
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
/**
|
|
5534
|
+
* List all passkeys for the current user.
|
|
5535
|
+
*/
|
|
5536
|
+
async _listPasskeys() {
|
|
5537
|
+
assertPasskeyExperimentalEnabled(this.experimental);
|
|
5538
|
+
try {
|
|
5539
|
+
return await this._useSession(async (result) => {
|
|
5540
|
+
const { data: { session }, error: sessionError, } = result;
|
|
5541
|
+
if (sessionError) {
|
|
5542
|
+
return this._returnResult({ data: null, error: sessionError });
|
|
5543
|
+
}
|
|
5544
|
+
if (!session) {
|
|
5545
|
+
return this._returnResult({ data: null, error: new AuthSessionMissingError() });
|
|
5546
|
+
}
|
|
5547
|
+
const { data, error } = await _request(this.fetch, 'GET', `${this.url}/passkeys`, {
|
|
5548
|
+
headers: this.headers,
|
|
5549
|
+
jwt: session.access_token,
|
|
5550
|
+
xform: (data) => ({ data, error: null }),
|
|
5551
|
+
});
|
|
5552
|
+
if (error) {
|
|
5553
|
+
return this._returnResult({ data: null, error });
|
|
5554
|
+
}
|
|
5555
|
+
return this._returnResult({ data, error: null });
|
|
5556
|
+
});
|
|
5557
|
+
}
|
|
5558
|
+
catch (error) {
|
|
5559
|
+
if (isAuthError(error)) {
|
|
5560
|
+
return this._returnResult({ data: null, error });
|
|
5561
|
+
}
|
|
5562
|
+
throw error;
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
/**
|
|
5566
|
+
* Update a passkey.
|
|
5567
|
+
*/
|
|
5568
|
+
async _updatePasskey(params) {
|
|
5569
|
+
assertPasskeyExperimentalEnabled(this.experimental);
|
|
5570
|
+
try {
|
|
5571
|
+
return await this._useSession(async (result) => {
|
|
5572
|
+
const { data: { session }, error: sessionError, } = result;
|
|
5573
|
+
if (sessionError) {
|
|
5574
|
+
return this._returnResult({ data: null, error: sessionError });
|
|
5575
|
+
}
|
|
5576
|
+
if (!session) {
|
|
5577
|
+
return this._returnResult({ data: null, error: new AuthSessionMissingError() });
|
|
5578
|
+
}
|
|
5579
|
+
const { data, error } = await _request(this.fetch, 'PATCH', `${this.url}/passkeys/${params.passkeyId}`, {
|
|
5580
|
+
headers: this.headers,
|
|
5581
|
+
jwt: session.access_token,
|
|
5582
|
+
body: { friendly_name: params.friendlyName },
|
|
5583
|
+
});
|
|
5584
|
+
if (error) {
|
|
5585
|
+
return this._returnResult({ data: null, error });
|
|
5586
|
+
}
|
|
5587
|
+
return this._returnResult({ data, error: null });
|
|
5588
|
+
});
|
|
5589
|
+
}
|
|
5590
|
+
catch (error) {
|
|
5591
|
+
if (isAuthError(error)) {
|
|
5592
|
+
return this._returnResult({ data: null, error });
|
|
5593
|
+
}
|
|
5594
|
+
throw error;
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
/**
|
|
5598
|
+
* Delete a passkey.
|
|
5599
|
+
*/
|
|
5600
|
+
async _deletePasskey(params) {
|
|
5601
|
+
assertPasskeyExperimentalEnabled(this.experimental);
|
|
5602
|
+
try {
|
|
5603
|
+
return await this._useSession(async (result) => {
|
|
5604
|
+
const { data: { session }, error: sessionError, } = result;
|
|
5605
|
+
if (sessionError) {
|
|
5606
|
+
return this._returnResult({ data: null, error: sessionError });
|
|
5607
|
+
}
|
|
5608
|
+
if (!session) {
|
|
5609
|
+
return this._returnResult({ data: null, error: new AuthSessionMissingError() });
|
|
5610
|
+
}
|
|
5611
|
+
const { error } = await _request(this.fetch, 'DELETE', `${this.url}/passkeys/${params.passkeyId}`, {
|
|
5612
|
+
headers: this.headers,
|
|
5613
|
+
jwt: session.access_token,
|
|
5614
|
+
noResolveJson: true,
|
|
5615
|
+
});
|
|
5616
|
+
if (error) {
|
|
5617
|
+
return this._returnResult({ data: null, error });
|
|
5618
|
+
}
|
|
5619
|
+
return this._returnResult({ data: null, error: null });
|
|
5620
|
+
});
|
|
5621
|
+
}
|
|
5622
|
+
catch (error) {
|
|
5623
|
+
if (isAuthError(error)) {
|
|
5624
|
+
return this._returnResult({ data: null, error });
|
|
5625
|
+
}
|
|
5626
|
+
throw error;
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
4847
5629
|
}
|
|
4848
5630
|
GoTrueClient.nextInstanceID = {};
|
|
4849
5631
|
export default GoTrueClient;
|