@supabase/auth-js 2.61.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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/main/AuthAdminApi.d.ts +4 -0
  4. package/dist/main/AuthAdminApi.d.ts.map +1 -0
  5. package/dist/main/AuthAdminApi.js +9 -0
  6. package/dist/main/AuthAdminApi.js.map +1 -0
  7. package/dist/main/AuthClient.d.ts +4 -0
  8. package/dist/main/AuthClient.d.ts.map +1 -0
  9. package/dist/main/AuthClient.js +9 -0
  10. package/dist/main/AuthClient.js.map +1 -0
  11. package/dist/main/GoTrueAdminApi.d.ts +99 -0
  12. package/dist/main/GoTrueAdminApi.d.ts.map +1 -0
  13. package/dist/main/GoTrueAdminApi.js +268 -0
  14. package/dist/main/GoTrueAdminApi.js.map +1 -0
  15. package/dist/main/GoTrueClient.d.ts +432 -0
  16. package/dist/main/GoTrueClient.d.ts.map +1 -0
  17. package/dist/main/GoTrueClient.js +1889 -0
  18. package/dist/main/GoTrueClient.js.map +1 -0
  19. package/dist/main/index.d.ts +9 -0
  20. package/dist/main/index.d.ts.map +1 -0
  21. package/dist/main/index.js +35 -0
  22. package/dist/main/index.js.map +1 -0
  23. package/dist/main/lib/constants.d.ts +12 -0
  24. package/dist/main/lib/constants.d.ts.map +1 -0
  25. package/dist/main/lib/constants.js +14 -0
  26. package/dist/main/lib/constants.js.map +1 -0
  27. package/dist/main/lib/errors.d.ts +96 -0
  28. package/dist/main/lib/errors.d.ts.map +1 -0
  29. package/dist/main/lib/errors.js +135 -0
  30. package/dist/main/lib/errors.js.map +1 -0
  31. package/dist/main/lib/fetch.d.ts +33 -0
  32. package/dist/main/lib/fetch.d.ts.map +1 -0
  33. package/dist/main/lib/fetch.js +162 -0
  34. package/dist/main/lib/fetch.js.map +1 -0
  35. package/dist/main/lib/helpers.d.ts +48 -0
  36. package/dist/main/lib/helpers.d.ts.map +1 -0
  37. package/dist/main/lib/helpers.js +292 -0
  38. package/dist/main/lib/helpers.js.map +1 -0
  39. package/dist/main/lib/local-storage.d.ts +13 -0
  40. package/dist/main/lib/local-storage.d.ts.map +1 -0
  41. package/dist/main/lib/local-storage.js +46 -0
  42. package/dist/main/lib/local-storage.js.map +1 -0
  43. package/dist/main/lib/locks.d.ts +44 -0
  44. package/dist/main/lib/locks.d.ts.map +1 -0
  45. package/dist/main/lib/locks.js +118 -0
  46. package/dist/main/lib/locks.js.map +1 -0
  47. package/dist/main/lib/polyfills.d.ts +5 -0
  48. package/dist/main/lib/polyfills.d.ts.map +1 -0
  49. package/dist/main/lib/polyfills.js +30 -0
  50. package/dist/main/lib/polyfills.js.map +1 -0
  51. package/dist/main/lib/types.d.ts +948 -0
  52. package/dist/main/lib/types.d.ts.map +1 -0
  53. package/dist/main/lib/types.js +3 -0
  54. package/dist/main/lib/types.js.map +1 -0
  55. package/dist/main/lib/version.d.ts +2 -0
  56. package/dist/main/lib/version.d.ts.map +1 -0
  57. package/dist/main/lib/version.js +6 -0
  58. package/dist/main/lib/version.js.map +1 -0
  59. package/dist/module/AuthAdminApi.d.ts +4 -0
  60. package/dist/module/AuthAdminApi.d.ts.map +1 -0
  61. package/dist/module/AuthAdminApi.js +4 -0
  62. package/dist/module/AuthAdminApi.js.map +1 -0
  63. package/dist/module/AuthClient.d.ts +4 -0
  64. package/dist/module/AuthClient.d.ts.map +1 -0
  65. package/dist/module/AuthClient.js +4 -0
  66. package/dist/module/AuthClient.js.map +1 -0
  67. package/dist/module/GoTrueAdminApi.d.ts +99 -0
  68. package/dist/module/GoTrueAdminApi.d.ts.map +1 -0
  69. package/dist/module/GoTrueAdminApi.js +265 -0
  70. package/dist/module/GoTrueAdminApi.js.map +1 -0
  71. package/dist/module/GoTrueClient.d.ts +432 -0
  72. package/dist/module/GoTrueClient.d.ts.map +1 -0
  73. package/dist/module/GoTrueClient.js +1883 -0
  74. package/dist/module/GoTrueClient.js.map +1 -0
  75. package/dist/module/index.d.ts +9 -0
  76. package/dist/module/index.d.ts.map +1 -0
  77. package/dist/module/index.js +9 -0
  78. package/dist/module/index.js.map +1 -0
  79. package/dist/module/lib/constants.d.ts +12 -0
  80. package/dist/module/lib/constants.d.ts.map +1 -0
  81. package/dist/module/lib/constants.js +11 -0
  82. package/dist/module/lib/constants.js.map +1 -0
  83. package/dist/module/lib/errors.d.ts +96 -0
  84. package/dist/module/lib/errors.d.ts.map +1 -0
  85. package/dist/module/lib/errors.js +117 -0
  86. package/dist/module/lib/errors.js.map +1 -0
  87. package/dist/module/lib/fetch.d.ts +33 -0
  88. package/dist/module/lib/fetch.d.ts.map +1 -0
  89. package/dist/module/lib/fetch.js +152 -0
  90. package/dist/module/lib/fetch.js.map +1 -0
  91. package/dist/module/lib/helpers.d.ts +48 -0
  92. package/dist/module/lib/helpers.d.ts.map +1 -0
  93. package/dist/module/lib/helpers.js +249 -0
  94. package/dist/module/lib/helpers.js.map +1 -0
  95. package/dist/module/lib/local-storage.d.ts +13 -0
  96. package/dist/module/lib/local-storage.d.ts.map +1 -0
  97. package/dist/module/lib/local-storage.js +42 -0
  98. package/dist/module/lib/local-storage.js.map +1 -0
  99. package/dist/module/lib/locks.d.ts +44 -0
  100. package/dist/module/lib/locks.d.ts.map +1 -0
  101. package/dist/module/lib/locks.js +112 -0
  102. package/dist/module/lib/locks.js.map +1 -0
  103. package/dist/module/lib/polyfills.d.ts +5 -0
  104. package/dist/module/lib/polyfills.d.ts.map +1 -0
  105. package/dist/module/lib/polyfills.js +26 -0
  106. package/dist/module/lib/polyfills.js.map +1 -0
  107. package/dist/module/lib/types.d.ts +948 -0
  108. package/dist/module/lib/types.d.ts.map +1 -0
  109. package/dist/module/lib/types.js +2 -0
  110. package/dist/module/lib/types.js.map +1 -0
  111. package/dist/module/lib/version.d.ts +2 -0
  112. package/dist/module/lib/version.d.ts.map +1 -0
  113. package/dist/module/lib/version.js +3 -0
  114. package/dist/module/lib/version.js.map +1 -0
  115. package/package.json +69 -0
  116. package/src/AuthAdminApi.ts +5 -0
  117. package/src/AuthClient.ts +5 -0
  118. package/src/GoTrueAdminApi.ts +333 -0
  119. package/src/GoTrueClient.ts +2470 -0
  120. package/src/index.ts +12 -0
  121. package/src/lib/constants.ts +10 -0
  122. package/src/lib/errors.ts +150 -0
  123. package/src/lib/fetch.ts +238 -0
  124. package/src/lib/helpers.ts +306 -0
  125. package/src/lib/local-storage.ts +49 -0
  126. package/src/lib/locks.ts +140 -0
  127. package/src/lib/polyfills.ts +23 -0
  128. package/src/lib/types.ts +1120 -0
  129. package/src/lib/version.ts +2 -0
@@ -0,0 +1,2470 @@
1
+ import GoTrueAdminApi from './GoTrueAdminApi'
2
+ import { DEFAULT_HEADERS, EXPIRY_MARGIN, GOTRUE_URL, STORAGE_KEY } from './lib/constants'
3
+ import {
4
+ AuthError,
5
+ AuthImplicitGrantRedirectError,
6
+ AuthPKCEGrantCodeExchangeError,
7
+ AuthInvalidCredentialsError,
8
+ AuthSessionMissingError,
9
+ AuthInvalidTokenResponseError,
10
+ AuthUnknownError,
11
+ isAuthApiError,
12
+ isAuthError,
13
+ isAuthRetryableFetchError,
14
+ } from './lib/errors'
15
+ import {
16
+ Fetch,
17
+ _request,
18
+ _sessionResponse,
19
+ _sessionResponsePassword,
20
+ _userResponse,
21
+ _ssoResponse,
22
+ } from './lib/fetch'
23
+ import {
24
+ decodeJWTPayload,
25
+ Deferred,
26
+ getItemAsync,
27
+ isBrowser,
28
+ removeItemAsync,
29
+ resolveFetch,
30
+ setItemAsync,
31
+ uuid,
32
+ retryable,
33
+ sleep,
34
+ generatePKCEVerifier,
35
+ generatePKCEChallenge,
36
+ supportsLocalStorage,
37
+ parseParametersFromURL,
38
+ } from './lib/helpers'
39
+ import { localStorageAdapter, memoryLocalStorageAdapter } from './lib/local-storage'
40
+ import { polyfillGlobalThis } from './lib/polyfills'
41
+ import { version } from './lib/version'
42
+ import { LockAcquireTimeoutError } from './lib/locks'
43
+
44
+ import type {
45
+ AuthChangeEvent,
46
+ AuthResponse,
47
+ AuthResponsePassword,
48
+ AuthTokenResponse,
49
+ AuthTokenResponsePassword,
50
+ AuthOtpResponse,
51
+ CallRefreshTokenResult,
52
+ GoTrueClientOptions,
53
+ InitializeResult,
54
+ OAuthResponse,
55
+ SSOResponse,
56
+ Provider,
57
+ Session,
58
+ SignInWithIdTokenCredentials,
59
+ SignInWithOAuthCredentials,
60
+ SignInWithPasswordCredentials,
61
+ SignInWithPasswordlessCredentials,
62
+ SignUpWithPasswordCredentials,
63
+ SignInWithSSO,
64
+ SignOut,
65
+ Subscription,
66
+ SupportedStorage,
67
+ User,
68
+ UserAttributes,
69
+ UserResponse,
70
+ VerifyOtpParams,
71
+ GoTrueMFAApi,
72
+ MFAEnrollParams,
73
+ AuthMFAEnrollResponse,
74
+ MFAChallengeParams,
75
+ AuthMFAChallengeResponse,
76
+ MFAUnenrollParams,
77
+ AuthMFAUnenrollResponse,
78
+ MFAVerifyParams,
79
+ AuthMFAVerifyResponse,
80
+ AuthMFAListFactorsResponse,
81
+ AMREntry,
82
+ AuthMFAGetAuthenticatorAssuranceLevelResponse,
83
+ AuthenticatorAssuranceLevels,
84
+ Factor,
85
+ MFAChallengeAndVerifyParams,
86
+ ResendParams,
87
+ AuthFlowType,
88
+ LockFunc,
89
+ UserIdentity,
90
+ WeakPassword,
91
+ } from './lib/types'
92
+
93
+ polyfillGlobalThis() // Make "globalThis" available
94
+
95
+ const DEFAULT_OPTIONS: Omit<Required<GoTrueClientOptions>, 'fetch' | 'storage' | 'lock'> = {
96
+ url: GOTRUE_URL,
97
+ storageKey: STORAGE_KEY,
98
+ autoRefreshToken: true,
99
+ persistSession: true,
100
+ detectSessionInUrl: true,
101
+ headers: DEFAULT_HEADERS,
102
+ flowType: 'implicit',
103
+ debug: false,
104
+ }
105
+
106
+ /** Current session will be checked for refresh at this interval. */
107
+ const AUTO_REFRESH_TICK_DURATION = 30 * 1000
108
+
109
+ /**
110
+ * A token refresh will be attempted this many ticks before the current session expires. */
111
+ const AUTO_REFRESH_TICK_THRESHOLD = 3
112
+
113
+ async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
114
+ return await fn()
115
+ }
116
+
117
+ export default class GoTrueClient {
118
+ private static nextInstanceID = 0
119
+
120
+ private instanceID: number
121
+
122
+ /**
123
+ * Namespace for the GoTrue admin methods.
124
+ * These methods should only be used in a trusted server-side environment.
125
+ */
126
+ admin: GoTrueAdminApi
127
+ /**
128
+ * Namespace for the MFA methods.
129
+ */
130
+ mfa: GoTrueMFAApi
131
+ /**
132
+ * The storage key used to identify the values saved in localStorage
133
+ */
134
+ protected storageKey: string
135
+
136
+ protected flowType: AuthFlowType
137
+
138
+ protected autoRefreshToken: boolean
139
+ protected persistSession: boolean
140
+ protected storage: SupportedStorage
141
+ protected memoryStorage: { [key: string]: string } | null = null
142
+ protected stateChangeEmitters: Map<string, Subscription> = new Map()
143
+ protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
144
+ protected visibilityChangedCallback: (() => Promise<any>) | null = null
145
+ protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
146
+ /**
147
+ * Keeps track of the async client initialization.
148
+ * When null or not yet resolved the auth state is `unknown`
149
+ * Once resolved the the auth state is known and it's save to call any further client methods.
150
+ * Keep extra care to never reject or throw uncaught errors
151
+ */
152
+ protected initializePromise: Promise<InitializeResult> | null = null
153
+ protected detectSessionInUrl = true
154
+ protected url: string
155
+ protected headers: {
156
+ [key: string]: string
157
+ }
158
+ protected fetch: Fetch
159
+ protected lock: LockFunc
160
+ protected lockAcquired = false
161
+ protected pendingInLock: Promise<any>[] = []
162
+
163
+ /**
164
+ * Used to broadcast state change events to other tabs listening.
165
+ */
166
+ protected broadcastChannel: BroadcastChannel | null = null
167
+
168
+ protected logDebugMessages: boolean
169
+ protected logger: (message: string, ...args: any[]) => void = console.log
170
+
171
+ /**
172
+ * Create a new client for use in the browser.
173
+ */
174
+ constructor(options: GoTrueClientOptions) {
175
+ this.instanceID = GoTrueClient.nextInstanceID
176
+ GoTrueClient.nextInstanceID += 1
177
+
178
+ if (this.instanceID > 0 && isBrowser()) {
179
+ console.warn(
180
+ 'Multiple GoTrueClient instances detected in the same browser context. It is not an error, but this should be avoided as it may produce undefined behavior when used concurrently under the same storage key.'
181
+ )
182
+ }
183
+
184
+ const settings = { ...DEFAULT_OPTIONS, ...options }
185
+
186
+ this.logDebugMessages = !!settings.debug
187
+ if (typeof settings.debug === 'function') {
188
+ this.logger = settings.debug
189
+ }
190
+
191
+ this.persistSession = settings.persistSession
192
+ this.storageKey = settings.storageKey
193
+ this.autoRefreshToken = settings.autoRefreshToken
194
+ this.admin = new GoTrueAdminApi({
195
+ url: settings.url,
196
+ headers: settings.headers,
197
+ fetch: settings.fetch,
198
+ })
199
+
200
+ this.url = settings.url
201
+ this.headers = settings.headers
202
+ this.fetch = resolveFetch(settings.fetch)
203
+ this.lock = settings.lock || lockNoOp
204
+ this.detectSessionInUrl = settings.detectSessionInUrl
205
+ this.flowType = settings.flowType
206
+
207
+ this.mfa = {
208
+ verify: this._verify.bind(this),
209
+ enroll: this._enroll.bind(this),
210
+ unenroll: this._unenroll.bind(this),
211
+ challenge: this._challenge.bind(this),
212
+ listFactors: this._listFactors.bind(this),
213
+ challengeAndVerify: this._challengeAndVerify.bind(this),
214
+ getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
215
+ }
216
+
217
+ if (this.persistSession) {
218
+ if (settings.storage) {
219
+ this.storage = settings.storage
220
+ } else {
221
+ if (supportsLocalStorage()) {
222
+ this.storage = localStorageAdapter
223
+ } else {
224
+ this.memoryStorage = {}
225
+ this.storage = memoryLocalStorageAdapter(this.memoryStorage)
226
+ }
227
+ }
228
+ } else {
229
+ this.memoryStorage = {}
230
+ this.storage = memoryLocalStorageAdapter(this.memoryStorage)
231
+ }
232
+
233
+ if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) {
234
+ try {
235
+ this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey)
236
+ } catch (e: any) {
237
+ console.error(
238
+ 'Failed to create a new BroadcastChannel, multi-tab state changes will not be available',
239
+ e
240
+ )
241
+ }
242
+
243
+ this.broadcastChannel?.addEventListener('message', async (event) => {
244
+ this._debug('received broadcast notification from other tab or client', event)
245
+
246
+ await this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
247
+ })
248
+ }
249
+
250
+ this.initialize()
251
+ }
252
+
253
+ private _debug(...args: any[]): GoTrueClient {
254
+ if (this.logDebugMessages) {
255
+ this.logger(
256
+ `GoTrueClient@${this.instanceID} (${version}) ${new Date().toISOString()}`,
257
+ ...args
258
+ )
259
+ }
260
+
261
+ return this
262
+ }
263
+
264
+ /**
265
+ * Initializes the client session either from the url or from storage.
266
+ * This method is automatically called when instantiating the client, but should also be called
267
+ * manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc).
268
+ */
269
+ async initialize(): Promise<InitializeResult> {
270
+ if (this.initializePromise) {
271
+ return await this.initializePromise
272
+ }
273
+
274
+ this.initializePromise = (async () => {
275
+ return await this._acquireLock(-1, async () => {
276
+ return await this._initialize()
277
+ })
278
+ })()
279
+
280
+ return await this.initializePromise
281
+ }
282
+
283
+ /**
284
+ * IMPORTANT:
285
+ * 1. Never throw in this method, as it is called from the constructor
286
+ * 2. Never return a session from this method as it would be cached over
287
+ * the whole lifetime of the client
288
+ */
289
+ private async _initialize(): Promise<InitializeResult> {
290
+ try {
291
+ const isPKCEFlow = isBrowser() ? await this._isPKCEFlow() : false
292
+ this._debug('#_initialize()', 'begin', 'is PKCE flow', isPKCEFlow)
293
+
294
+ if (isPKCEFlow || (this.detectSessionInUrl && this._isImplicitGrantFlow())) {
295
+ const { data, error } = await this._getSessionFromURL(isPKCEFlow)
296
+ if (error) {
297
+ this._debug('#_initialize()', 'error detecting session from URL', error)
298
+
299
+ // hacky workaround to keep the existing session if there's an error returned from identity linking
300
+ // TODO: once error codes are ready, we should match against it instead of the message
301
+ if (
302
+ error?.message === 'Identity is already linked' ||
303
+ error?.message === 'Identity is already linked to another user'
304
+ ) {
305
+ return { error }
306
+ }
307
+
308
+ // failed login attempt via url,
309
+ // remove old session as in verifyOtp, signUp and signInWith*
310
+ await this._removeSession()
311
+
312
+ return { error }
313
+ }
314
+
315
+ const { session, redirectType } = data
316
+
317
+ this._debug(
318
+ '#_initialize()',
319
+ 'detected session in URL',
320
+ session,
321
+ 'redirect type',
322
+ redirectType
323
+ )
324
+
325
+ await this._saveSession(session)
326
+
327
+ setTimeout(async () => {
328
+ if (redirectType === 'recovery') {
329
+ await this._notifyAllSubscribers('PASSWORD_RECOVERY', session)
330
+ } else {
331
+ await this._notifyAllSubscribers('SIGNED_IN', session)
332
+ }
333
+ }, 0)
334
+
335
+ return { error: null }
336
+ }
337
+ // no login attempt via callback url try to recover session from storage
338
+ await this._recoverAndRefresh()
339
+ return { error: null }
340
+ } catch (error) {
341
+ if (isAuthError(error)) {
342
+ return { error }
343
+ }
344
+
345
+ return {
346
+ error: new AuthUnknownError('Unexpected error during initialization', error),
347
+ }
348
+ } finally {
349
+ await this._handleVisibilityChange()
350
+ this._debug('#_initialize()', 'end')
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Creates a new user.
356
+ *
357
+ * Be aware that if a user account exists in the system you may get back an
358
+ * error message that attempts to hide this information from the user.
359
+ * This method has support for PKCE via email signups. The PKCE flow cannot be used when autoconfirm is enabled.
360
+ *
361
+ * @returns A logged-in session if the server has "autoconfirm" ON
362
+ * @returns A user if the server has "autoconfirm" OFF
363
+ */
364
+ async signUp(credentials: SignUpWithPasswordCredentials): Promise<AuthResponse> {
365
+ try {
366
+ await this._removeSession()
367
+
368
+ let res: AuthResponse
369
+ if ('email' in credentials) {
370
+ const { email, password, options } = credentials
371
+ let codeChallenge: string | null = null
372
+ let codeChallengeMethod: string | null = null
373
+ if (this.flowType === 'pkce') {
374
+ const codeVerifier = generatePKCEVerifier()
375
+ await setItemAsync(this.storage, `${this.storageKey}-code-verifier`, codeVerifier)
376
+ codeChallenge = await generatePKCEChallenge(codeVerifier)
377
+ codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'
378
+ }
379
+ res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
380
+ headers: this.headers,
381
+ redirectTo: options?.emailRedirectTo,
382
+ body: {
383
+ email,
384
+ password,
385
+ data: options?.data ?? {},
386
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
387
+ code_challenge: codeChallenge,
388
+ code_challenge_method: codeChallengeMethod,
389
+ },
390
+ xform: _sessionResponse,
391
+ })
392
+ } else if ('phone' in credentials) {
393
+ const { phone, password, options } = credentials
394
+ res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
395
+ headers: this.headers,
396
+ body: {
397
+ phone,
398
+ password,
399
+ data: options?.data ?? {},
400
+ channel: options?.channel ?? 'sms',
401
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
402
+ },
403
+ xform: _sessionResponse,
404
+ })
405
+ } else {
406
+ throw new AuthInvalidCredentialsError(
407
+ 'You must provide either an email or phone number and a password'
408
+ )
409
+ }
410
+
411
+ const { data, error } = res
412
+
413
+ if (error || !data) {
414
+ return { data: { user: null, session: null }, error: error }
415
+ }
416
+
417
+ const session: Session | null = data.session
418
+ const user: User | null = data.user
419
+
420
+ if (data.session) {
421
+ await this._saveSession(data.session)
422
+ await this._notifyAllSubscribers('SIGNED_IN', session)
423
+ }
424
+
425
+ return { data: { user, session }, error: null }
426
+ } catch (error) {
427
+ if (isAuthError(error)) {
428
+ return { data: { user: null, session: null }, error }
429
+ }
430
+
431
+ throw error
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Log in an existing user with an email and password or phone and password.
437
+ *
438
+ * Be aware that you may get back an error message that will not distinguish
439
+ * between the cases where the account does not exist or that the
440
+ * email/phone and password combination is wrong or that the account can only
441
+ * be accessed via social login.
442
+ */
443
+ async signInWithPassword(
444
+ credentials: SignInWithPasswordCredentials
445
+ ): Promise<AuthTokenResponsePassword> {
446
+ try {
447
+ await this._removeSession()
448
+
449
+ let res: AuthResponsePassword
450
+ if ('email' in credentials) {
451
+ const { email, password, options } = credentials
452
+ res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
453
+ headers: this.headers,
454
+ body: {
455
+ email,
456
+ password,
457
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
458
+ },
459
+ xform: _sessionResponsePassword,
460
+ })
461
+ } else if ('phone' in credentials) {
462
+ const { phone, password, options } = credentials
463
+ res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
464
+ headers: this.headers,
465
+ body: {
466
+ phone,
467
+ password,
468
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
469
+ },
470
+ xform: _sessionResponsePassword,
471
+ })
472
+ } else {
473
+ throw new AuthInvalidCredentialsError(
474
+ 'You must provide either an email or phone number and a password'
475
+ )
476
+ }
477
+ const { data, error } = res
478
+
479
+ if (error) {
480
+ return { data: { user: null, session: null }, error }
481
+ } else if (!data || !data.session || !data.user) {
482
+ return { data: { user: null, session: null }, error: new AuthInvalidTokenResponseError() }
483
+ }
484
+ if (data.session) {
485
+ await this._saveSession(data.session)
486
+ await this._notifyAllSubscribers('SIGNED_IN', data.session)
487
+ }
488
+ return {
489
+ data: {
490
+ user: data.user,
491
+ session: data.session,
492
+ ...(data.weak_password ? { weakPassword: data.weak_password } : null),
493
+ },
494
+ error,
495
+ }
496
+ } catch (error) {
497
+ if (isAuthError(error)) {
498
+ return { data: { user: null, session: null }, error }
499
+ }
500
+ throw error
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Log in an existing user via a third-party provider.
506
+ * This method supports the PKCE flow.
507
+ */
508
+ async signInWithOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
509
+ await this._removeSession()
510
+
511
+ return await this._handleProviderSignIn(credentials.provider, {
512
+ redirectTo: credentials.options?.redirectTo,
513
+ scopes: credentials.options?.scopes,
514
+ queryParams: credentials.options?.queryParams,
515
+ skipBrowserRedirect: credentials.options?.skipBrowserRedirect,
516
+ })
517
+ }
518
+
519
+ /**
520
+ * Log in an existing user by exchanging an Auth Code issued during the PKCE flow.
521
+ */
522
+ async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
523
+ await this.initializePromise
524
+
525
+ return this._acquireLock(-1, async () => {
526
+ return this._exchangeCodeForSession(authCode)
527
+ })
528
+ }
529
+
530
+ private async _exchangeCodeForSession(authCode: string): Promise<
531
+ | {
532
+ data: { session: Session; user: User; redirectType: string | null }
533
+ error: null
534
+ }
535
+ | { data: { session: null; user: null; redirectType: null }; error: AuthError }
536
+ > {
537
+ const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
538
+ const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')
539
+ const { data, error } = await _request(
540
+ this.fetch,
541
+ 'POST',
542
+ `${this.url}/token?grant_type=pkce`,
543
+ {
544
+ headers: this.headers,
545
+ body: {
546
+ auth_code: authCode,
547
+ code_verifier: codeVerifier,
548
+ },
549
+ xform: _sessionResponse,
550
+ }
551
+ )
552
+ await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
553
+ if (error) {
554
+ return { data: { user: null, session: null, redirectType: null }, error }
555
+ } else if (!data || !data.session || !data.user) {
556
+ return {
557
+ data: { user: null, session: null, redirectType: null },
558
+ error: new AuthInvalidTokenResponseError(),
559
+ }
560
+ }
561
+ if (data.session) {
562
+ await this._saveSession(data.session)
563
+ await this._notifyAllSubscribers('SIGNED_IN', data.session)
564
+ }
565
+ return { data: { ...data, redirectType: redirectType ?? null }, error }
566
+ }
567
+
568
+ /**
569
+ * Allows signing in with an OIDC ID token. The authentication provider used
570
+ * should be enabled and configured.
571
+ */
572
+ async signInWithIdToken(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse> {
573
+ await this._removeSession()
574
+
575
+ try {
576
+ const { options, provider, token, access_token, nonce } = credentials
577
+
578
+ const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
579
+ headers: this.headers,
580
+ body: {
581
+ provider,
582
+ id_token: token,
583
+ access_token,
584
+ nonce,
585
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
586
+ },
587
+ xform: _sessionResponse,
588
+ })
589
+
590
+ const { data, error } = res
591
+ if (error) {
592
+ return { data: { user: null, session: null }, error }
593
+ } else if (!data || !data.session || !data.user) {
594
+ return {
595
+ data: { user: null, session: null },
596
+ error: new AuthInvalidTokenResponseError(),
597
+ }
598
+ }
599
+ if (data.session) {
600
+ await this._saveSession(data.session)
601
+ await this._notifyAllSubscribers('SIGNED_IN', data.session)
602
+ }
603
+ return { data, error }
604
+ } catch (error) {
605
+ if (isAuthError(error)) {
606
+ return { data: { user: null, session: null }, error }
607
+ }
608
+ throw error
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Log in a user using magiclink or a one-time password (OTP).
614
+ *
615
+ * If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
616
+ * If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
617
+ * If you're using phone sign-ins, only an OTP will be sent. You won't be able to send a magiclink for phone sign-ins.
618
+ *
619
+ * Be aware that you may get back an error message that will not distinguish
620
+ * between the cases where the account does not exist or, that the account
621
+ * can only be accessed via social login.
622
+ *
623
+ * Do note that you will need to configure a Whatsapp sender on Twilio
624
+ * if you are using phone sign in with the 'whatsapp' channel. The whatsapp
625
+ * channel is not supported on other providers
626
+ * at this time.
627
+ * This method supports PKCE when an email is passed.
628
+ */
629
+ async signInWithOtp(credentials: SignInWithPasswordlessCredentials): Promise<AuthOtpResponse> {
630
+ try {
631
+ await this._removeSession()
632
+
633
+ if ('email' in credentials) {
634
+ const { email, options } = credentials
635
+ let codeChallenge: string | null = null
636
+ let codeChallengeMethod: string | null = null
637
+ if (this.flowType === 'pkce') {
638
+ const codeVerifier = generatePKCEVerifier()
639
+ await setItemAsync(this.storage, `${this.storageKey}-code-verifier`, codeVerifier)
640
+ codeChallenge = await generatePKCEChallenge(codeVerifier)
641
+ codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'
642
+ }
643
+ const { error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
644
+ headers: this.headers,
645
+ body: {
646
+ email,
647
+ data: options?.data ?? {},
648
+ create_user: options?.shouldCreateUser ?? true,
649
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
650
+ code_challenge: codeChallenge,
651
+ code_challenge_method: codeChallengeMethod,
652
+ },
653
+ redirectTo: options?.emailRedirectTo,
654
+ })
655
+ return { data: { user: null, session: null }, error }
656
+ }
657
+ if ('phone' in credentials) {
658
+ const { phone, options } = credentials
659
+ const { data, error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
660
+ headers: this.headers,
661
+ body: {
662
+ phone,
663
+ data: options?.data ?? {},
664
+ create_user: options?.shouldCreateUser ?? true,
665
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
666
+ channel: options?.channel ?? 'sms',
667
+ },
668
+ })
669
+ return { data: { user: null, session: null, messageId: data?.message_id }, error }
670
+ }
671
+ throw new AuthInvalidCredentialsError('You must provide either an email or phone number.')
672
+ } catch (error) {
673
+ if (isAuthError(error)) {
674
+ return { data: { user: null, session: null }, error }
675
+ }
676
+
677
+ throw error
678
+ }
679
+ }
680
+
681
+ /**
682
+ * Log in a user given a User supplied OTP or TokenHash received through mobile or email.
683
+ */
684
+ async verifyOtp(params: VerifyOtpParams): Promise<AuthResponse> {
685
+ try {
686
+ if (params.type !== 'email_change' && params.type !== 'phone_change') {
687
+ // we don't want to remove the authenticated session if the user is performing an email_change or phone_change verification
688
+ await this._removeSession()
689
+ }
690
+
691
+ let redirectTo = undefined
692
+ let captchaToken = undefined
693
+ if ('options' in params) {
694
+ redirectTo = params.options?.redirectTo
695
+ captchaToken = params.options?.captchaToken
696
+ }
697
+ const { data, error } = await _request(this.fetch, 'POST', `${this.url}/verify`, {
698
+ headers: this.headers,
699
+ body: {
700
+ ...params,
701
+ gotrue_meta_security: { captcha_token: captchaToken },
702
+ },
703
+ redirectTo,
704
+ xform: _sessionResponse,
705
+ })
706
+
707
+ if (error) {
708
+ throw error
709
+ }
710
+
711
+ if (!data) {
712
+ throw new Error('An error occurred on token verification.')
713
+ }
714
+
715
+ const session: Session | null = data.session
716
+ const user: User = data.user
717
+
718
+ if (session?.access_token) {
719
+ await this._saveSession(session as Session)
720
+ await this._notifyAllSubscribers('SIGNED_IN', session)
721
+ }
722
+
723
+ return { data: { user, session }, error: null }
724
+ } catch (error) {
725
+ if (isAuthError(error)) {
726
+ return { data: { user: null, session: null }, error }
727
+ }
728
+
729
+ throw error
730
+ }
731
+ }
732
+
733
+ /**
734
+ * Attempts a single-sign on using an enterprise Identity Provider. A
735
+ * successful SSO attempt will redirect the current page to the identity
736
+ * provider authorization page. The redirect URL is implementation and SSO
737
+ * protocol specific.
738
+ *
739
+ * You can use it by providing a SSO domain. Typically you can extract this
740
+ * domain by asking users for their email address. If this domain is
741
+ * registered on the Auth instance the redirect will use that organization's
742
+ * currently active SSO Identity Provider for the login.
743
+ *
744
+ * If you have built an organization-specific login page, you can use the
745
+ * organization's SSO Identity Provider UUID directly instead.
746
+ */
747
+ async signInWithSSO(params: SignInWithSSO): Promise<SSOResponse> {
748
+ try {
749
+ await this._removeSession()
750
+ let codeChallenge: string | null = null
751
+ let codeChallengeMethod: string | null = null
752
+ if (this.flowType === 'pkce') {
753
+ const codeVerifier = generatePKCEVerifier()
754
+ await setItemAsync(this.storage, `${this.storageKey}-code-verifier`, codeVerifier)
755
+ codeChallenge = await generatePKCEChallenge(codeVerifier)
756
+ codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'
757
+ }
758
+
759
+ return await _request(this.fetch, 'POST', `${this.url}/sso`, {
760
+ body: {
761
+ ...('providerId' in params ? { provider_id: params.providerId } : null),
762
+ ...('domain' in params ? { domain: params.domain } : null),
763
+ redirect_to: params.options?.redirectTo ?? undefined,
764
+ ...(params?.options?.captchaToken
765
+ ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
766
+ : null),
767
+ skip_http_redirect: true, // fetch does not handle redirects
768
+ code_challenge: codeChallenge,
769
+ code_challenge_method: codeChallengeMethod,
770
+ },
771
+ headers: this.headers,
772
+ xform: _ssoResponse,
773
+ })
774
+ } catch (error) {
775
+ if (isAuthError(error)) {
776
+ return { data: null, error }
777
+ }
778
+ throw error
779
+ }
780
+ }
781
+
782
+ /**
783
+ * Sends a reauthentication OTP to the user's email or phone number.
784
+ * Requires the user to be signed-in.
785
+ */
786
+ async reauthenticate(): Promise<AuthResponse> {
787
+ await this.initializePromise
788
+
789
+ return await this._acquireLock(-1, async () => {
790
+ return await this._reauthenticate()
791
+ })
792
+ }
793
+
794
+ private async _reauthenticate(): Promise<AuthResponse> {
795
+ try {
796
+ return await this._useSession(async (result) => {
797
+ const {
798
+ data: { session },
799
+ error: sessionError,
800
+ } = result
801
+ if (sessionError) throw sessionError
802
+ if (!session) throw new AuthSessionMissingError()
803
+
804
+ const { error } = await _request(this.fetch, 'GET', `${this.url}/reauthenticate`, {
805
+ headers: this.headers,
806
+ jwt: session.access_token,
807
+ })
808
+ return { data: { user: null, session: null }, error }
809
+ })
810
+ } catch (error) {
811
+ if (isAuthError(error)) {
812
+ return { data: { user: null, session: null }, error }
813
+ }
814
+ throw error
815
+ }
816
+ }
817
+
818
+ /**
819
+ * Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP.
820
+ */
821
+ async resend(credentials: ResendParams): Promise<AuthOtpResponse> {
822
+ try {
823
+ if (credentials.type != 'email_change' && credentials.type != 'phone_change') {
824
+ await this._removeSession()
825
+ }
826
+
827
+ const endpoint = `${this.url}/resend`
828
+ if ('email' in credentials) {
829
+ const { email, type, options } = credentials
830
+ const { error } = await _request(this.fetch, 'POST', endpoint, {
831
+ headers: this.headers,
832
+ body: {
833
+ email,
834
+ type,
835
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
836
+ },
837
+ redirectTo: options?.emailRedirectTo,
838
+ })
839
+ return { data: { user: null, session: null }, error }
840
+ } else if ('phone' in credentials) {
841
+ const { phone, type, options } = credentials
842
+ const { data, error } = await _request(this.fetch, 'POST', endpoint, {
843
+ headers: this.headers,
844
+ body: {
845
+ phone,
846
+ type,
847
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
848
+ },
849
+ })
850
+ return { data: { user: null, session: null, messageId: data?.message_id }, error }
851
+ }
852
+ throw new AuthInvalidCredentialsError(
853
+ 'You must provide either an email or phone number and a type'
854
+ )
855
+ } catch (error) {
856
+ if (isAuthError(error)) {
857
+ return { data: { user: null, session: null }, error }
858
+ }
859
+ throw error
860
+ }
861
+ }
862
+
863
+ /**
864
+ * Returns the session, refreshing it if necessary.
865
+ * The session returned can be null if the session is not detected which can happen in the event a user is not signed-in or has logged out.
866
+ */
867
+ async getSession() {
868
+ await this.initializePromise
869
+
870
+ return this._acquireLock(-1, async () => {
871
+ return this._useSession(async (result) => {
872
+ return result
873
+ })
874
+ })
875
+ }
876
+
877
+ /**
878
+ * Acquires a global lock based on the storage key.
879
+ */
880
+ private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
881
+ this._debug('#_acquireLock', 'begin', acquireTimeout)
882
+
883
+ try {
884
+ if (this.lockAcquired) {
885
+ const last = this.pendingInLock.length
886
+ ? this.pendingInLock[this.pendingInLock.length - 1]
887
+ : Promise.resolve()
888
+
889
+ const result = (async () => {
890
+ await last
891
+ return await fn()
892
+ })()
893
+
894
+ this.pendingInLock.push(
895
+ (async () => {
896
+ try {
897
+ await result
898
+ } catch (e: any) {
899
+ // we just care if it finished
900
+ }
901
+ })()
902
+ )
903
+
904
+ return result
905
+ }
906
+
907
+ return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
908
+ this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
909
+
910
+ try {
911
+ this.lockAcquired = true
912
+
913
+ const result = fn()
914
+
915
+ this.pendingInLock.push(
916
+ (async () => {
917
+ try {
918
+ await result
919
+ } catch (e: any) {
920
+ // we just care if it finished
921
+ }
922
+ })()
923
+ )
924
+
925
+ await result
926
+
927
+ // keep draining the queue until there's nothing to wait on
928
+ while (this.pendingInLock.length) {
929
+ const waitOn = [...this.pendingInLock]
930
+
931
+ await Promise.all(waitOn)
932
+
933
+ this.pendingInLock.splice(0, waitOn.length)
934
+ }
935
+
936
+ return await result
937
+ } finally {
938
+ this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
939
+
940
+ this.lockAcquired = false
941
+ }
942
+ })
943
+ } finally {
944
+ this._debug('#_acquireLock', 'end')
945
+ }
946
+ }
947
+
948
+ /**
949
+ * Use instead of {@link #getSession} inside the library. It is
950
+ * semantically usually what you want, as getting a session involves some
951
+ * processing afterwards that requires only one client operating on the
952
+ * session at once across multiple tabs or processes.
953
+ */
954
+ private async _useSession<R>(
955
+ fn: (
956
+ result:
957
+ | {
958
+ data: {
959
+ session: Session
960
+ }
961
+ error: null
962
+ }
963
+ | {
964
+ data: {
965
+ session: null
966
+ }
967
+ error: AuthError
968
+ }
969
+ | {
970
+ data: {
971
+ session: null
972
+ }
973
+ error: null
974
+ }
975
+ ) => Promise<R>
976
+ ): Promise<R> {
977
+ this._debug('#_useSession', 'begin')
978
+
979
+ try {
980
+ // the use of __loadSession here is the only correct use of the function!
981
+ const result = await this.__loadSession()
982
+
983
+ return await fn(result)
984
+ } finally {
985
+ this._debug('#_useSession', 'end')
986
+ }
987
+ }
988
+
989
+ /**
990
+ * NEVER USE DIRECTLY!
991
+ *
992
+ * Always use {@link #_useSession}.
993
+ */
994
+ private async __loadSession(): Promise<
995
+ | {
996
+ data: {
997
+ session: Session
998
+ }
999
+ error: null
1000
+ }
1001
+ | {
1002
+ data: {
1003
+ session: null
1004
+ }
1005
+ error: AuthError
1006
+ }
1007
+ | {
1008
+ data: {
1009
+ session: null
1010
+ }
1011
+ error: null
1012
+ }
1013
+ > {
1014
+ this._debug('#__loadSession()', 'begin')
1015
+
1016
+ if (!this.lockAcquired) {
1017
+ this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
1018
+ }
1019
+
1020
+ try {
1021
+ let currentSession: Session | null = null
1022
+
1023
+ const maybeSession = await getItemAsync(this.storage, this.storageKey)
1024
+
1025
+ this._debug('#getSession()', 'session from storage', maybeSession)
1026
+
1027
+ if (maybeSession !== null) {
1028
+ if (this._isValidSession(maybeSession)) {
1029
+ currentSession = maybeSession
1030
+ } else {
1031
+ this._debug('#getSession()', 'session from storage is not valid')
1032
+ await this._removeSession()
1033
+ }
1034
+ }
1035
+
1036
+ if (!currentSession) {
1037
+ return { data: { session: null }, error: null }
1038
+ }
1039
+
1040
+ const hasExpired = currentSession.expires_at
1041
+ ? currentSession.expires_at <= Date.now() / 1000
1042
+ : false
1043
+
1044
+ this._debug(
1045
+ '#__loadSession()',
1046
+ `session has${hasExpired ? '' : ' not'} expired`,
1047
+ 'expires_at',
1048
+ currentSession.expires_at
1049
+ )
1050
+
1051
+ if (!hasExpired) {
1052
+ return { data: { session: currentSession }, error: null }
1053
+ }
1054
+
1055
+ const { session, error } = await this._callRefreshToken(currentSession.refresh_token)
1056
+ if (error) {
1057
+ return { data: { session: null }, error }
1058
+ }
1059
+
1060
+ return { data: { session }, error: null }
1061
+ } finally {
1062
+ this._debug('#__loadSession()', 'end')
1063
+ }
1064
+ }
1065
+
1066
+ /**
1067
+ * Gets the current user details if there is an existing session.
1068
+ * @param jwt Takes in an optional access token jwt. If no jwt is provided, getUser() will attempt to get the jwt from the current session.
1069
+ */
1070
+ async getUser(jwt?: string): Promise<UserResponse> {
1071
+ if (jwt) {
1072
+ return await this._getUser(jwt)
1073
+ }
1074
+
1075
+ await this.initializePromise
1076
+
1077
+ return this._acquireLock(-1, async () => {
1078
+ return await this._getUser()
1079
+ })
1080
+ }
1081
+
1082
+ private async _getUser(jwt?: string): Promise<UserResponse> {
1083
+ try {
1084
+ if (jwt) {
1085
+ return await _request(this.fetch, 'GET', `${this.url}/user`, {
1086
+ headers: this.headers,
1087
+ jwt: jwt,
1088
+ xform: _userResponse,
1089
+ })
1090
+ }
1091
+
1092
+ return await this._useSession(async (result) => {
1093
+ const { data, error } = result
1094
+ if (error) {
1095
+ throw error
1096
+ }
1097
+
1098
+ return await _request(this.fetch, 'GET', `${this.url}/user`, {
1099
+ headers: this.headers,
1100
+ jwt: data.session?.access_token ?? undefined,
1101
+ xform: _userResponse,
1102
+ })
1103
+ })
1104
+ } catch (error) {
1105
+ if (isAuthError(error)) {
1106
+ return { data: { user: null }, error }
1107
+ }
1108
+
1109
+ throw error
1110
+ }
1111
+ }
1112
+
1113
+ /**
1114
+ * Updates user data for a logged in user.
1115
+ */
1116
+ async updateUser(
1117
+ attributes: UserAttributes,
1118
+ options: {
1119
+ emailRedirectTo?: string | undefined
1120
+ } = {}
1121
+ ): Promise<UserResponse> {
1122
+ await this.initializePromise
1123
+
1124
+ return await this._acquireLock(-1, async () => {
1125
+ return await this._updateUser(attributes, options)
1126
+ })
1127
+ }
1128
+
1129
+ protected async _updateUser(
1130
+ attributes: UserAttributes,
1131
+ options: {
1132
+ emailRedirectTo?: string | undefined
1133
+ } = {}
1134
+ ): Promise<UserResponse> {
1135
+ try {
1136
+ return await this._useSession(async (result) => {
1137
+ const { data: sessionData, error: sessionError } = result
1138
+ if (sessionError) {
1139
+ throw sessionError
1140
+ }
1141
+ if (!sessionData.session) {
1142
+ throw new AuthSessionMissingError()
1143
+ }
1144
+ const session: Session = sessionData.session
1145
+ let codeChallenge: string | null = null
1146
+ let codeChallengeMethod: string | null = null
1147
+ if (this.flowType === 'pkce' && attributes.email != null) {
1148
+ const codeVerifier = generatePKCEVerifier()
1149
+ await setItemAsync(this.storage, `${this.storageKey}-code-verifier`, codeVerifier)
1150
+ codeChallenge = await generatePKCEChallenge(codeVerifier)
1151
+ codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'
1152
+ }
1153
+
1154
+ const { data, error: userError } = await _request(this.fetch, 'PUT', `${this.url}/user`, {
1155
+ headers: this.headers,
1156
+ redirectTo: options?.emailRedirectTo,
1157
+ body: {
1158
+ ...attributes,
1159
+ code_challenge: codeChallenge,
1160
+ code_challenge_method: codeChallengeMethod,
1161
+ },
1162
+ jwt: session.access_token,
1163
+ xform: _userResponse,
1164
+ })
1165
+ if (userError) throw userError
1166
+ session.user = data.user as User
1167
+ await this._saveSession(session)
1168
+ await this._notifyAllSubscribers('USER_UPDATED', session)
1169
+ return { data: { user: session.user }, error: null }
1170
+ })
1171
+ } catch (error) {
1172
+ if (isAuthError(error)) {
1173
+ return { data: { user: null }, error }
1174
+ }
1175
+
1176
+ throw error
1177
+ }
1178
+ }
1179
+
1180
+ /**
1181
+ * Decodes a JWT (without performing any validation).
1182
+ */
1183
+ private _decodeJWT(jwt: string): {
1184
+ exp?: number
1185
+ aal?: AuthenticatorAssuranceLevels | null
1186
+ amr?: AMREntry[] | null
1187
+ } {
1188
+ return decodeJWTPayload(jwt)
1189
+ }
1190
+
1191
+ /**
1192
+ * Sets the session data from the current session. If the current session is expired, setSession will take care of refreshing it to obtain a new session.
1193
+ * If the refresh token or access token in the current session is invalid, an error will be thrown.
1194
+ * @param currentSession The current session that minimally contains an access token and refresh token.
1195
+ */
1196
+ async setSession(currentSession: {
1197
+ access_token: string
1198
+ refresh_token: string
1199
+ }): Promise<AuthResponse> {
1200
+ await this.initializePromise
1201
+
1202
+ return await this._acquireLock(-1, async () => {
1203
+ return await this._setSession(currentSession)
1204
+ })
1205
+ }
1206
+
1207
+ protected async _setSession(currentSession: {
1208
+ access_token: string
1209
+ refresh_token: string
1210
+ }): Promise<AuthResponse> {
1211
+ try {
1212
+ if (!currentSession.access_token || !currentSession.refresh_token) {
1213
+ throw new AuthSessionMissingError()
1214
+ }
1215
+
1216
+ const timeNow = Date.now() / 1000
1217
+ let expiresAt = timeNow
1218
+ let hasExpired = true
1219
+ let session: Session | null = null
1220
+ const payload = decodeJWTPayload(currentSession.access_token)
1221
+ if (payload.exp) {
1222
+ expiresAt = payload.exp
1223
+ hasExpired = expiresAt <= timeNow
1224
+ }
1225
+
1226
+ if (hasExpired) {
1227
+ const { session: refreshedSession, error } = await this._callRefreshToken(
1228
+ currentSession.refresh_token
1229
+ )
1230
+ if (error) {
1231
+ return { data: { user: null, session: null }, error: error }
1232
+ }
1233
+
1234
+ if (!refreshedSession) {
1235
+ return { data: { user: null, session: null }, error: null }
1236
+ }
1237
+ session = refreshedSession
1238
+ } else {
1239
+ const { data, error } = await this._getUser(currentSession.access_token)
1240
+ if (error) {
1241
+ throw error
1242
+ }
1243
+ session = {
1244
+ access_token: currentSession.access_token,
1245
+ refresh_token: currentSession.refresh_token,
1246
+ user: data.user,
1247
+ token_type: 'bearer',
1248
+ expires_in: expiresAt - timeNow,
1249
+ expires_at: expiresAt,
1250
+ }
1251
+ await this._saveSession(session)
1252
+ await this._notifyAllSubscribers('SIGNED_IN', session)
1253
+ }
1254
+
1255
+ return { data: { user: session.user, session }, error: null }
1256
+ } catch (error) {
1257
+ if (isAuthError(error)) {
1258
+ return { data: { session: null, user: null }, error }
1259
+ }
1260
+
1261
+ throw error
1262
+ }
1263
+ }
1264
+
1265
+ /**
1266
+ * Returns a new session, regardless of expiry status.
1267
+ * Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession().
1268
+ * If the current session's refresh token is invalid, an error will be thrown.
1269
+ * @param currentSession The current session. If passed in, it must contain a refresh token.
1270
+ */
1271
+ async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
1272
+ await this.initializePromise
1273
+
1274
+ return await this._acquireLock(-1, async () => {
1275
+ return await this._refreshSession(currentSession)
1276
+ })
1277
+ }
1278
+
1279
+ protected async _refreshSession(currentSession?: {
1280
+ refresh_token: string
1281
+ }): Promise<AuthResponse> {
1282
+ try {
1283
+ return await this._useSession(async (result) => {
1284
+ if (!currentSession) {
1285
+ const { data, error } = result
1286
+ if (error) {
1287
+ throw error
1288
+ }
1289
+
1290
+ currentSession = data.session ?? undefined
1291
+ }
1292
+
1293
+ if (!currentSession?.refresh_token) {
1294
+ throw new AuthSessionMissingError()
1295
+ }
1296
+
1297
+ const { session, error } = await this._callRefreshToken(currentSession.refresh_token)
1298
+ if (error) {
1299
+ return { data: { user: null, session: null }, error: error }
1300
+ }
1301
+
1302
+ if (!session) {
1303
+ return { data: { user: null, session: null }, error: null }
1304
+ }
1305
+
1306
+ return { data: { user: session.user, session }, error: null }
1307
+ })
1308
+ } catch (error) {
1309
+ if (isAuthError(error)) {
1310
+ return { data: { user: null, session: null }, error }
1311
+ }
1312
+
1313
+ throw error
1314
+ }
1315
+ }
1316
+
1317
+ /**
1318
+ * Gets the session data from a URL string
1319
+ */
1320
+ private async _getSessionFromURL(isPKCEFlow: boolean): Promise<
1321
+ | {
1322
+ data: { session: Session; redirectType: string | null }
1323
+ error: null
1324
+ }
1325
+ | { data: { session: null; redirectType: null }; error: AuthError }
1326
+ > {
1327
+ try {
1328
+ if (!isBrowser()) throw new AuthImplicitGrantRedirectError('No browser detected.')
1329
+ if (this.flowType === 'implicit' && !this._isImplicitGrantFlow()) {
1330
+ throw new AuthImplicitGrantRedirectError('Not a valid implicit grant flow url.')
1331
+ } else if (this.flowType == 'pkce' && !isPKCEFlow) {
1332
+ throw new AuthPKCEGrantCodeExchangeError('Not a valid PKCE flow url.')
1333
+ }
1334
+
1335
+ const params = parseParametersFromURL(window.location.href)
1336
+
1337
+ if (isPKCEFlow) {
1338
+ if (!params.code) throw new AuthPKCEGrantCodeExchangeError('No code detected.')
1339
+ const { data, error } = await this._exchangeCodeForSession(params.code)
1340
+ if (error) throw error
1341
+
1342
+ const url = new URL(window.location.href)
1343
+ url.searchParams.delete('code')
1344
+
1345
+ window.history.replaceState(window.history.state, '', url.toString())
1346
+
1347
+ return { data: { session: data.session, redirectType: null }, error: null }
1348
+ }
1349
+
1350
+ if (params.error || params.error_description || params.error_code) {
1351
+ throw new AuthImplicitGrantRedirectError(
1352
+ params.error_description || 'Error in URL with unspecified error_description',
1353
+ {
1354
+ error: params.error || 'unspecified_error',
1355
+ code: params.error_code || 'unspecified_code',
1356
+ }
1357
+ )
1358
+ }
1359
+
1360
+ const {
1361
+ provider_token,
1362
+ provider_refresh_token,
1363
+ access_token,
1364
+ refresh_token,
1365
+ expires_in,
1366
+ expires_at,
1367
+ token_type,
1368
+ } = params
1369
+
1370
+ if (!access_token || !expires_in || !refresh_token || !token_type) {
1371
+ throw new AuthImplicitGrantRedirectError('No session defined in URL')
1372
+ }
1373
+
1374
+ const timeNow = Math.round(Date.now() / 1000)
1375
+ const expiresIn = parseInt(expires_in)
1376
+ let expiresAt = timeNow + expiresIn
1377
+
1378
+ if (expires_at) {
1379
+ expiresAt = parseInt(expires_at)
1380
+ }
1381
+
1382
+ const actuallyExpiresIn = expiresAt - timeNow
1383
+ if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION) {
1384
+ console.warn(
1385
+ `@supabase/gotrue-js: Session as retrieved from URL expires in ${actuallyExpiresIn}s, should have been closer to ${expiresIn}s`
1386
+ )
1387
+ }
1388
+
1389
+ const issuedAt = expiresAt - expiresIn
1390
+ if (timeNow - issuedAt >= 120) {
1391
+ console.warn(
1392
+ '@supabase/gotrue-js: Session as retrieved from URL was issued over 120s ago, URL could be stale',
1393
+ issuedAt,
1394
+ expiresAt,
1395
+ timeNow
1396
+ )
1397
+ } else if (timeNow - issuedAt < 0) {
1398
+ console.warn(
1399
+ '@supabase/gotrue-js: Session as retrieved from URL was issued in the future? Check the device clok for skew',
1400
+ issuedAt,
1401
+ expiresAt,
1402
+ timeNow
1403
+ )
1404
+ }
1405
+
1406
+ const { data, error } = await this._getUser(access_token)
1407
+ if (error) throw error
1408
+
1409
+ const session: Session = {
1410
+ provider_token,
1411
+ provider_refresh_token,
1412
+ access_token,
1413
+ expires_in: expiresIn,
1414
+ expires_at: expiresAt,
1415
+ refresh_token,
1416
+ token_type,
1417
+ user: data.user,
1418
+ }
1419
+
1420
+ // Remove tokens from URL
1421
+ window.location.hash = ''
1422
+ this._debug('#_getSessionFromURL()', 'clearing window.location.hash')
1423
+
1424
+ return { data: { session, redirectType: params.type }, error: null }
1425
+ } catch (error) {
1426
+ if (isAuthError(error)) {
1427
+ return { data: { session: null, redirectType: null }, error }
1428
+ }
1429
+
1430
+ throw error
1431
+ }
1432
+ }
1433
+
1434
+ /**
1435
+ * Checks if the current URL contains parameters given by an implicit oauth grant flow (https://www.rfc-editor.org/rfc/rfc6749.html#section-4.2)
1436
+ */
1437
+ private _isImplicitGrantFlow(): boolean {
1438
+ const params = parseParametersFromURL(window.location.href)
1439
+
1440
+ return !!(isBrowser() && (params.access_token || params.error_description))
1441
+ }
1442
+
1443
+ /**
1444
+ * Checks if the current URL and backing storage contain parameters given by a PKCE flow
1445
+ */
1446
+ private async _isPKCEFlow(): Promise<boolean> {
1447
+ const params = parseParametersFromURL(window.location.href)
1448
+
1449
+ const currentStorageContent = await getItemAsync(
1450
+ this.storage,
1451
+ `${this.storageKey}-code-verifier`
1452
+ )
1453
+
1454
+ return !!(params.code && currentStorageContent)
1455
+ }
1456
+
1457
+ /**
1458
+ * Inside a browser context, `signOut()` will remove the logged in user from the browser session and log them out - removing all items from localstorage and then trigger a `"SIGNED_OUT"` event.
1459
+ *
1460
+ * For server-side management, you can revoke all refresh tokens for a user by passing a user's JWT through to `auth.api.signOut(JWT: string)`.
1461
+ * There is no way to revoke a user's access token jwt until it expires. It is recommended to set a shorter expiry on the jwt for this reason.
1462
+ *
1463
+ * If using `others` scope, no `SIGNED_OUT` event is fired!
1464
+ */
1465
+ async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
1466
+ await this.initializePromise
1467
+
1468
+ return await this._acquireLock(-1, async () => {
1469
+ return await this._signOut(options)
1470
+ })
1471
+ }
1472
+
1473
+ protected async _signOut(
1474
+ { scope }: SignOut = { scope: 'global' }
1475
+ ): Promise<{ error: AuthError | null }> {
1476
+ return await this._useSession(async (result) => {
1477
+ const { data, error: sessionError } = result
1478
+ if (sessionError) {
1479
+ return { error: sessionError }
1480
+ }
1481
+ const accessToken = data.session?.access_token
1482
+ if (accessToken) {
1483
+ const { error } = await this.admin.signOut(accessToken, scope)
1484
+ if (error) {
1485
+ // ignore 404s since user might not exist anymore
1486
+ // ignore 401s since an invalid or expired JWT should sign out the current session
1487
+ if (!(isAuthApiError(error) && (error.status === 404 || error.status === 401))) {
1488
+ return { error }
1489
+ }
1490
+ }
1491
+ }
1492
+ if (scope !== 'others') {
1493
+ await this._removeSession()
1494
+ await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
1495
+ await this._notifyAllSubscribers('SIGNED_OUT', null)
1496
+ }
1497
+ return { error: null }
1498
+ })
1499
+ }
1500
+
1501
+ /**
1502
+ * Receive a notification every time an auth event happens.
1503
+ * @param callback A callback function to be invoked when an auth event happens.
1504
+ */
1505
+ onAuthStateChange(
1506
+ callback: (event: AuthChangeEvent, session: Session | null) => void | Promise<void>
1507
+ ): {
1508
+ data: { subscription: Subscription }
1509
+ } {
1510
+ const id: string = uuid()
1511
+ const subscription: Subscription = {
1512
+ id,
1513
+ callback,
1514
+ unsubscribe: () => {
1515
+ this._debug('#unsubscribe()', 'state change callback with id removed', id)
1516
+
1517
+ this.stateChangeEmitters.delete(id)
1518
+ },
1519
+ }
1520
+
1521
+ this._debug('#onAuthStateChange()', 'registered callback with id', id)
1522
+
1523
+ this.stateChangeEmitters.set(id, subscription)
1524
+ ;(async () => {
1525
+ await this.initializePromise
1526
+
1527
+ await this._acquireLock(-1, async () => {
1528
+ this._emitInitialSession(id)
1529
+ })
1530
+ })()
1531
+
1532
+ return { data: { subscription } }
1533
+ }
1534
+
1535
+ private async _emitInitialSession(id: string): Promise<void> {
1536
+ return await this._useSession(async (result) => {
1537
+ try {
1538
+ const {
1539
+ data: { session },
1540
+ error,
1541
+ } = result
1542
+ if (error) throw error
1543
+
1544
+ await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', session)
1545
+ this._debug('INITIAL_SESSION', 'callback id', id, 'session', session)
1546
+ } catch (err) {
1547
+ await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
1548
+ this._debug('INITIAL_SESSION', 'callback id', id, 'error', err)
1549
+ console.error(err)
1550
+ }
1551
+ })
1552
+ }
1553
+
1554
+ /**
1555
+ * Sends a password reset request to an email address. This method supports the PKCE flow.
1556
+ *
1557
+ * @param email The email address of the user.
1558
+ * @param options.redirectTo The URL to send the user to after they click the password reset link.
1559
+ * @param options.captchaToken Verification token received when the user completes the captcha on the site.
1560
+ */
1561
+ async resetPasswordForEmail(
1562
+ email: string,
1563
+ options: {
1564
+ redirectTo?: string
1565
+ captchaToken?: string
1566
+ } = {}
1567
+ ): Promise<
1568
+ | {
1569
+ data: {}
1570
+ error: null
1571
+ }
1572
+ | { data: null; error: AuthError }
1573
+ > {
1574
+ let codeChallenge: string | null = null
1575
+ let codeChallengeMethod: string | null = null
1576
+ if (this.flowType === 'pkce') {
1577
+ const codeVerifier = generatePKCEVerifier()
1578
+ await setItemAsync(
1579
+ this.storage,
1580
+ `${this.storageKey}-code-verifier`,
1581
+ `${codeVerifier}/PASSWORD_RECOVERY`
1582
+ )
1583
+ codeChallenge = await generatePKCEChallenge(codeVerifier)
1584
+ codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'
1585
+ }
1586
+ try {
1587
+ return await _request(this.fetch, 'POST', `${this.url}/recover`, {
1588
+ body: {
1589
+ email,
1590
+ code_challenge: codeChallenge,
1591
+ code_challenge_method: codeChallengeMethod,
1592
+ gotrue_meta_security: { captcha_token: options.captchaToken },
1593
+ },
1594
+ headers: this.headers,
1595
+ redirectTo: options.redirectTo,
1596
+ })
1597
+ } catch (error) {
1598
+ if (isAuthError(error)) {
1599
+ return { data: null, error }
1600
+ }
1601
+
1602
+ throw error
1603
+ }
1604
+ }
1605
+
1606
+ /**
1607
+ * Gets all the identities linked to a user.
1608
+ */
1609
+ async getUserIdentities(): Promise<
1610
+ | {
1611
+ data: {
1612
+ identities: UserIdentity[]
1613
+ }
1614
+ error: null
1615
+ }
1616
+ | { data: null; error: AuthError }
1617
+ > {
1618
+ try {
1619
+ const { data, error } = await this.getUser()
1620
+ if (error) throw error
1621
+ return { data: { identities: data.user.identities ?? [] }, error: null }
1622
+ } catch (error) {
1623
+ if (isAuthError(error)) {
1624
+ return { data: null, error }
1625
+ }
1626
+ throw error
1627
+ }
1628
+ }
1629
+ /**
1630
+ * Links an oauth identity to an existing user.
1631
+ * This method supports the PKCE flow.
1632
+ */
1633
+ async linkIdentity(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
1634
+ try {
1635
+ const { data, error } = await this._useSession(async (result) => {
1636
+ const { data, error } = result
1637
+ if (error) throw error
1638
+ const url: string = await this._getUrlForProvider(
1639
+ `${this.url}/user/identities/authorize`,
1640
+ credentials.provider,
1641
+ {
1642
+ redirectTo: credentials.options?.redirectTo,
1643
+ scopes: credentials.options?.scopes,
1644
+ queryParams: credentials.options?.queryParams,
1645
+ skipBrowserRedirect: true,
1646
+ }
1647
+ )
1648
+ return await _request(this.fetch, 'GET', url, {
1649
+ headers: this.headers,
1650
+ jwt: data.session?.access_token ?? undefined,
1651
+ })
1652
+ })
1653
+ if (error) throw error
1654
+ if (isBrowser() && !credentials.options?.skipBrowserRedirect) {
1655
+ window.location.assign(data?.url)
1656
+ }
1657
+ return { data: { provider: credentials.provider, url: data?.url }, error: null }
1658
+ } catch (error) {
1659
+ if (isAuthError(error)) {
1660
+ return { data: { provider: credentials.provider, url: null }, error }
1661
+ }
1662
+ throw error
1663
+ }
1664
+ }
1665
+
1666
+ /**
1667
+ * Unlinks an identity from a user by deleting it. The user will no longer be able to sign in with that identity once it's unlinked.
1668
+ */
1669
+ async unlinkIdentity(identity: UserIdentity): Promise<
1670
+ | {
1671
+ data: {}
1672
+ error: null
1673
+ }
1674
+ | { data: null; error: AuthError }
1675
+ > {
1676
+ try {
1677
+ return await this._useSession(async (result) => {
1678
+ const { data, error } = result
1679
+ if (error) {
1680
+ throw error
1681
+ }
1682
+ return await _request(
1683
+ this.fetch,
1684
+ 'DELETE',
1685
+ `${this.url}/user/identities/${identity.identity_id}`,
1686
+ {
1687
+ headers: this.headers,
1688
+ jwt: data.session?.access_token ?? undefined,
1689
+ }
1690
+ )
1691
+ })
1692
+ } catch (error) {
1693
+ if (isAuthError(error)) {
1694
+ return { data: null, error }
1695
+ }
1696
+ throw error
1697
+ }
1698
+ }
1699
+
1700
+ /**
1701
+ * Generates a new JWT.
1702
+ * @param refreshToken A valid refresh token that was returned on login.
1703
+ */
1704
+ private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
1705
+ const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`
1706
+ this._debug(debugName, 'begin')
1707
+
1708
+ try {
1709
+ const startedAt = Date.now()
1710
+
1711
+ // will attempt to refresh the token with exponential backoff
1712
+ return await retryable(
1713
+ async (attempt) => {
1714
+ await sleep(attempt * 200) // 0, 200, 400, 800, ...
1715
+
1716
+ this._debug(debugName, 'refreshing attempt', attempt)
1717
+
1718
+ return await _request(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, {
1719
+ body: { refresh_token: refreshToken },
1720
+ headers: this.headers,
1721
+ xform: _sessionResponse,
1722
+ })
1723
+ },
1724
+ (attempt, _, result) =>
1725
+ result &&
1726
+ result.error &&
1727
+ isAuthRetryableFetchError(result.error) &&
1728
+ // retryable only if the request can be sent before the backoff overflows the tick duration
1729
+ Date.now() + (attempt + 1) * 200 - startedAt < AUTO_REFRESH_TICK_DURATION
1730
+ )
1731
+ } catch (error) {
1732
+ this._debug(debugName, 'error', error)
1733
+
1734
+ if (isAuthError(error)) {
1735
+ return { data: { session: null, user: null }, error }
1736
+ }
1737
+ throw error
1738
+ } finally {
1739
+ this._debug(debugName, 'end')
1740
+ }
1741
+ }
1742
+
1743
+ private _isValidSession(maybeSession: unknown): maybeSession is Session {
1744
+ const isValidSession =
1745
+ typeof maybeSession === 'object' &&
1746
+ maybeSession !== null &&
1747
+ 'access_token' in maybeSession &&
1748
+ 'refresh_token' in maybeSession &&
1749
+ 'expires_at' in maybeSession
1750
+
1751
+ return isValidSession
1752
+ }
1753
+
1754
+ private async _handleProviderSignIn(
1755
+ provider: Provider,
1756
+ options: {
1757
+ redirectTo?: string
1758
+ scopes?: string
1759
+ queryParams?: { [key: string]: string }
1760
+ skipBrowserRedirect?: boolean
1761
+ }
1762
+ ) {
1763
+ const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
1764
+ redirectTo: options.redirectTo,
1765
+ scopes: options.scopes,
1766
+ queryParams: options.queryParams,
1767
+ })
1768
+
1769
+ this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url)
1770
+
1771
+ // try to open on the browser
1772
+ if (isBrowser() && !options.skipBrowserRedirect) {
1773
+ window.location.assign(url)
1774
+ }
1775
+
1776
+ return { data: { provider, url }, error: null }
1777
+ }
1778
+
1779
+ /**
1780
+ * Recovers the session from LocalStorage and refreshes
1781
+ * Note: this method is async to accommodate for AsyncStorage e.g. in React native.
1782
+ */
1783
+ private async _recoverAndRefresh() {
1784
+ const debugName = '#_recoverAndRefresh()'
1785
+ this._debug(debugName, 'begin')
1786
+
1787
+ try {
1788
+ const currentSession = await getItemAsync(this.storage, this.storageKey)
1789
+ this._debug(debugName, 'session from storage', currentSession)
1790
+
1791
+ if (!this._isValidSession(currentSession)) {
1792
+ this._debug(debugName, 'session is not valid')
1793
+ if (currentSession !== null) {
1794
+ await this._removeSession()
1795
+ }
1796
+
1797
+ return
1798
+ }
1799
+
1800
+ const timeNow = Math.round(Date.now() / 1000)
1801
+ const expiresWithMargin = (currentSession.expires_at ?? Infinity) < timeNow + EXPIRY_MARGIN
1802
+
1803
+ this._debug(
1804
+ debugName,
1805
+ `session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN}s`
1806
+ )
1807
+
1808
+ if (expiresWithMargin) {
1809
+ if (this.autoRefreshToken && currentSession.refresh_token) {
1810
+ const { error } = await this._callRefreshToken(currentSession.refresh_token)
1811
+
1812
+ if (error) {
1813
+ console.error(error)
1814
+
1815
+ if (!isAuthRetryableFetchError(error)) {
1816
+ this._debug(
1817
+ debugName,
1818
+ 'refresh failed with a non-retryable error, removing the session',
1819
+ error
1820
+ )
1821
+ await this._removeSession()
1822
+ }
1823
+ }
1824
+ }
1825
+ } else {
1826
+ // no need to persist currentSession again, as we just loaded it from
1827
+ // local storage; persisting it again may overwrite a value saved by
1828
+ // another client with access to the same local storage
1829
+ await this._notifyAllSubscribers('SIGNED_IN', currentSession)
1830
+ }
1831
+ } catch (err) {
1832
+ this._debug(debugName, 'error', err)
1833
+
1834
+ console.error(err)
1835
+ return
1836
+ } finally {
1837
+ this._debug(debugName, 'end')
1838
+ }
1839
+ }
1840
+
1841
+ private async _callRefreshToken(refreshToken: string): Promise<CallRefreshTokenResult> {
1842
+ if (!refreshToken) {
1843
+ throw new AuthSessionMissingError()
1844
+ }
1845
+
1846
+ // refreshing is already in progress
1847
+ if (this.refreshingDeferred) {
1848
+ return this.refreshingDeferred.promise
1849
+ }
1850
+
1851
+ const debugName = `#_callRefreshToken(${refreshToken.substring(0, 5)}...)`
1852
+
1853
+ this._debug(debugName, 'begin')
1854
+
1855
+ try {
1856
+ this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
1857
+
1858
+ const { data, error } = await this._refreshAccessToken(refreshToken)
1859
+ if (error) throw error
1860
+ if (!data.session) throw new AuthSessionMissingError()
1861
+
1862
+ await this._saveSession(data.session)
1863
+ await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
1864
+
1865
+ const result = { session: data.session, error: null }
1866
+
1867
+ this.refreshingDeferred.resolve(result)
1868
+
1869
+ return result
1870
+ } catch (error) {
1871
+ this._debug(debugName, 'error', error)
1872
+
1873
+ if (isAuthError(error)) {
1874
+ const result = { session: null, error }
1875
+
1876
+ if (!isAuthRetryableFetchError(error)) {
1877
+ await this._removeSession()
1878
+ await this._notifyAllSubscribers('SIGNED_OUT', null)
1879
+ }
1880
+
1881
+ this.refreshingDeferred?.resolve(result)
1882
+
1883
+ return result
1884
+ }
1885
+
1886
+ this.refreshingDeferred?.reject(error)
1887
+ throw error
1888
+ } finally {
1889
+ this.refreshingDeferred = null
1890
+ this._debug(debugName, 'end')
1891
+ }
1892
+ }
1893
+
1894
+ private async _notifyAllSubscribers(
1895
+ event: AuthChangeEvent,
1896
+ session: Session | null,
1897
+ broadcast = true
1898
+ ) {
1899
+ const debugName = `#_notifyAllSubscribers(${event})`
1900
+ this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
1901
+
1902
+ try {
1903
+ if (this.broadcastChannel && broadcast) {
1904
+ this.broadcastChannel.postMessage({ event, session })
1905
+ }
1906
+
1907
+ const errors: any[] = []
1908
+ const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => {
1909
+ try {
1910
+ await x.callback(event, session)
1911
+ } catch (e: any) {
1912
+ errors.push(e)
1913
+ }
1914
+ })
1915
+
1916
+ await Promise.all(promises)
1917
+
1918
+ if (errors.length > 0) {
1919
+ for (let i = 0; i < errors.length; i += 1) {
1920
+ console.error(errors[i])
1921
+ }
1922
+
1923
+ throw errors[0]
1924
+ }
1925
+ } finally {
1926
+ this._debug(debugName, 'end')
1927
+ }
1928
+ }
1929
+
1930
+ /**
1931
+ * set currentSession and currentUser
1932
+ * process to _startAutoRefreshToken if possible
1933
+ */
1934
+ private async _saveSession(session: Session) {
1935
+ this._debug('#_saveSession()', session)
1936
+
1937
+ await setItemAsync(this.storage, this.storageKey, session)
1938
+ }
1939
+
1940
+ private async _removeSession() {
1941
+ this._debug('#_removeSession()')
1942
+
1943
+ await removeItemAsync(this.storage, this.storageKey)
1944
+ }
1945
+
1946
+ /**
1947
+ * Removes any registered visibilitychange callback.
1948
+ *
1949
+ * {@see #startAutoRefresh}
1950
+ * {@see #stopAutoRefresh}
1951
+ */
1952
+ private _removeVisibilityChangedCallback() {
1953
+ this._debug('#_removeVisibilityChangedCallback()')
1954
+
1955
+ const callback = this.visibilityChangedCallback
1956
+ this.visibilityChangedCallback = null
1957
+
1958
+ try {
1959
+ if (callback && isBrowser() && window?.removeEventListener) {
1960
+ window.removeEventListener('visibilitychange', callback)
1961
+ }
1962
+ } catch (e) {
1963
+ console.error('removing visibilitychange callback failed', e)
1964
+ }
1965
+ }
1966
+
1967
+ /**
1968
+ * This is the private implementation of {@link #startAutoRefresh}. Use this
1969
+ * within the library.
1970
+ */
1971
+ private async _startAutoRefresh() {
1972
+ await this._stopAutoRefresh()
1973
+
1974
+ this._debug('#_startAutoRefresh()')
1975
+
1976
+ const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION)
1977
+ this.autoRefreshTicker = ticker
1978
+
1979
+ if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') {
1980
+ // ticker is a NodeJS Timeout object that has an `unref` method
1981
+ // https://nodejs.org/api/timers.html#timeoutunref
1982
+ // When auto refresh is used in NodeJS (like for testing) the
1983
+ // `setInterval` is preventing the process from being marked as
1984
+ // finished and tests run endlessly. This can be prevented by calling
1985
+ // `unref()` on the returned object.
1986
+ ticker.unref()
1987
+ // @ts-ignore
1988
+ } else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') {
1989
+ // similar like for NodeJS, but with the Deno API
1990
+ // https://deno.land/api@latest?unstable&s=Deno.unrefTimer
1991
+ // @ts-ignore
1992
+ Deno.unrefTimer(ticker)
1993
+ }
1994
+
1995
+ // run the tick immediately, but in the next pass of the event loop so that
1996
+ // #_initialize can be allowed to complete without recursively waiting on
1997
+ // itself
1998
+ setTimeout(async () => {
1999
+ await this.initializePromise
2000
+ await this._autoRefreshTokenTick()
2001
+ }, 0)
2002
+ }
2003
+
2004
+ /**
2005
+ * This is the private implementation of {@link #stopAutoRefresh}. Use this
2006
+ * within the library.
2007
+ */
2008
+ private async _stopAutoRefresh() {
2009
+ this._debug('#_stopAutoRefresh()')
2010
+
2011
+ const ticker = this.autoRefreshTicker
2012
+ this.autoRefreshTicker = null
2013
+
2014
+ if (ticker) {
2015
+ clearInterval(ticker)
2016
+ }
2017
+ }
2018
+
2019
+ /**
2020
+ * Starts an auto-refresh process in the background. The session is checked
2021
+ * every few seconds. Close to the time of expiration a process is started to
2022
+ * refresh the session. If refreshing fails it will be retried for as long as
2023
+ * necessary.
2024
+ *
2025
+ * If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need
2026
+ * to call this function, it will be called for you.
2027
+ *
2028
+ * On browsers the refresh process works only when the tab/window is in the
2029
+ * foreground to conserve resources as well as prevent race conditions and
2030
+ * flooding auth with requests. If you call this method any managed
2031
+ * visibility change callback will be removed and you must manage visibility
2032
+ * changes on your own.
2033
+ *
2034
+ * On non-browser platforms the refresh process works *continuously* in the
2035
+ * background, which may not be desirable. You should hook into your
2036
+ * platform's foreground indication mechanism and call these methods
2037
+ * appropriately to conserve resources.
2038
+ *
2039
+ * {@see #stopAutoRefresh}
2040
+ */
2041
+ async startAutoRefresh() {
2042
+ this._removeVisibilityChangedCallback()
2043
+ await this._startAutoRefresh()
2044
+ }
2045
+
2046
+ /**
2047
+ * Stops an active auto refresh process running in the background (if any).
2048
+ *
2049
+ * If you call this method any managed visibility change callback will be
2050
+ * removed and you must manage visibility changes on your own.
2051
+ *
2052
+ * See {@link #startAutoRefresh} for more details.
2053
+ */
2054
+ async stopAutoRefresh() {
2055
+ this._removeVisibilityChangedCallback()
2056
+ await this._stopAutoRefresh()
2057
+ }
2058
+
2059
+ /**
2060
+ * Runs the auto refresh token tick.
2061
+ */
2062
+ private async _autoRefreshTokenTick() {
2063
+ this._debug('#_autoRefreshTokenTick()', 'begin')
2064
+
2065
+ try {
2066
+ await this._acquireLock(0, async () => {
2067
+ try {
2068
+ const now = Date.now()
2069
+
2070
+ try {
2071
+ return await this._useSession(async (result) => {
2072
+ const {
2073
+ data: { session },
2074
+ } = result
2075
+
2076
+ if (!session || !session.refresh_token || !session.expires_at) {
2077
+ this._debug('#_autoRefreshTokenTick()', 'no session')
2078
+ return
2079
+ }
2080
+
2081
+ // session will expire in this many ticks (or has already expired if <= 0)
2082
+ const expiresInTicks = Math.floor(
2083
+ (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION
2084
+ )
2085
+
2086
+ this._debug(
2087
+ '#_autoRefreshTokenTick()',
2088
+ `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
2089
+ )
2090
+
2091
+ if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
2092
+ await this._callRefreshToken(session.refresh_token)
2093
+ }
2094
+ })
2095
+ } catch (e: any) {
2096
+ console.error(
2097
+ 'Auto refresh tick failed with error. This is likely a transient error.',
2098
+ e
2099
+ )
2100
+ }
2101
+ } finally {
2102
+ this._debug('#_autoRefreshTokenTick()', 'end')
2103
+ }
2104
+ })
2105
+ } catch (e: any) {
2106
+ if (e.isAcquireTimeout || e instanceof LockAcquireTimeoutError) {
2107
+ this._debug('auto refresh token tick lock not available')
2108
+ } else {
2109
+ throw e
2110
+ }
2111
+ }
2112
+ }
2113
+
2114
+ /**
2115
+ * Registers callbacks on the browser / platform, which in-turn run
2116
+ * algorithms when the browser window/tab are in foreground. On non-browser
2117
+ * platforms it assumes always foreground.
2118
+ */
2119
+ private async _handleVisibilityChange() {
2120
+ this._debug('#_handleVisibilityChange()')
2121
+
2122
+ if (!isBrowser() || !window?.addEventListener) {
2123
+ if (this.autoRefreshToken) {
2124
+ // in non-browser environments the refresh token ticker runs always
2125
+ this.startAutoRefresh()
2126
+ }
2127
+
2128
+ return false
2129
+ }
2130
+
2131
+ try {
2132
+ this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false)
2133
+
2134
+ window?.addEventListener('visibilitychange', this.visibilityChangedCallback)
2135
+
2136
+ // now immediately call the visbility changed callback to setup with the
2137
+ // current visbility state
2138
+ await this._onVisibilityChanged(true) // initial call
2139
+ } catch (error) {
2140
+ console.error('_handleVisibilityChange', error)
2141
+ }
2142
+ }
2143
+
2144
+ /**
2145
+ * Callback registered with `window.addEventListener('visibilitychange')`.
2146
+ */
2147
+ private async _onVisibilityChanged(calledFromInitialize: boolean) {
2148
+ const methodName = `#_onVisibilityChanged(${calledFromInitialize})`
2149
+ this._debug(methodName, 'visibilityState', document.visibilityState)
2150
+
2151
+ if (document.visibilityState === 'visible') {
2152
+ if (this.autoRefreshToken) {
2153
+ // in browser environments the refresh token ticker runs only on focused tabs
2154
+ // which prevents race conditions
2155
+ this._startAutoRefresh()
2156
+ }
2157
+
2158
+ if (!calledFromInitialize) {
2159
+ // called when the visibility has changed, i.e. the browser
2160
+ // transitioned from hidden -> visible so we need to see if the session
2161
+ // should be recovered immediately... but to do that we need to acquire
2162
+ // the lock first asynchronously
2163
+ await this.initializePromise
2164
+
2165
+ await this._acquireLock(-1, async () => {
2166
+ if (document.visibilityState !== 'visible') {
2167
+ this._debug(
2168
+ methodName,
2169
+ 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
2170
+ )
2171
+
2172
+ // visibility has changed while waiting for the lock, abort
2173
+ return
2174
+ }
2175
+
2176
+ // recover the session
2177
+ await this._recoverAndRefresh()
2178
+ })
2179
+ }
2180
+ } else if (document.visibilityState === 'hidden') {
2181
+ if (this.autoRefreshToken) {
2182
+ this._stopAutoRefresh()
2183
+ }
2184
+ }
2185
+ }
2186
+
2187
+ /**
2188
+ * Generates the relevant login URL for a third-party provider.
2189
+ * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
2190
+ * @param options.scopes A space-separated list of scopes granted to the OAuth application.
2191
+ * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
2192
+ */
2193
+ private async _getUrlForProvider(
2194
+ url: string,
2195
+ provider: Provider,
2196
+ options: {
2197
+ redirectTo?: string
2198
+ scopes?: string
2199
+ queryParams?: { [key: string]: string }
2200
+ skipBrowserRedirect?: boolean
2201
+ }
2202
+ ) {
2203
+ const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
2204
+ if (options?.redirectTo) {
2205
+ urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
2206
+ }
2207
+ if (options?.scopes) {
2208
+ urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
2209
+ }
2210
+ if (this.flowType === 'pkce') {
2211
+ const codeVerifier = generatePKCEVerifier()
2212
+ await setItemAsync(this.storage, `${this.storageKey}-code-verifier`, codeVerifier)
2213
+ const codeChallenge = await generatePKCEChallenge(codeVerifier)
2214
+ const codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'
2215
+
2216
+ this._debug(
2217
+ 'PKCE',
2218
+ 'code verifier',
2219
+ `${codeVerifier.substring(0, 5)}...`,
2220
+ 'code challenge',
2221
+ codeChallenge,
2222
+ 'method',
2223
+ codeChallengeMethod
2224
+ )
2225
+
2226
+ const flowParams = new URLSearchParams({
2227
+ code_challenge: `${encodeURIComponent(codeChallenge)}`,
2228
+ code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
2229
+ })
2230
+ urlParams.push(flowParams.toString())
2231
+ }
2232
+ if (options?.queryParams) {
2233
+ const query = new URLSearchParams(options.queryParams)
2234
+ urlParams.push(query.toString())
2235
+ }
2236
+ if (options?.skipBrowserRedirect) {
2237
+ urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
2238
+ }
2239
+
2240
+ return `${url}?${urlParams.join('&')}`
2241
+ }
2242
+
2243
+ private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
2244
+ try {
2245
+ return await this._useSession(async (result) => {
2246
+ const { data: sessionData, error: sessionError } = result
2247
+ if (sessionError) {
2248
+ return { data: null, error: sessionError }
2249
+ }
2250
+
2251
+ return await _request(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
2252
+ headers: this.headers,
2253
+ jwt: sessionData?.session?.access_token,
2254
+ })
2255
+ })
2256
+ } catch (error) {
2257
+ if (isAuthError(error)) {
2258
+ return { data: null, error }
2259
+ }
2260
+ throw error
2261
+ }
2262
+ }
2263
+
2264
+ /**
2265
+ * {@see GoTrueMFAApi#enroll}
2266
+ */
2267
+ private async _enroll(params: MFAEnrollParams): Promise<AuthMFAEnrollResponse> {
2268
+ try {
2269
+ return await this._useSession(async (result) => {
2270
+ const { data: sessionData, error: sessionError } = result
2271
+ if (sessionError) {
2272
+ return { data: null, error: sessionError }
2273
+ }
2274
+
2275
+ const { data, error } = await _request(this.fetch, 'POST', `${this.url}/factors`, {
2276
+ body: {
2277
+ friendly_name: params.friendlyName,
2278
+ factor_type: params.factorType,
2279
+ issuer: params.issuer,
2280
+ },
2281
+ headers: this.headers,
2282
+ jwt: sessionData?.session?.access_token,
2283
+ })
2284
+
2285
+ if (error) {
2286
+ return { data: null, error }
2287
+ }
2288
+
2289
+ if (data?.totp?.qr_code) {
2290
+ data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`
2291
+ }
2292
+
2293
+ return { data, error: null }
2294
+ })
2295
+ } catch (error) {
2296
+ if (isAuthError(error)) {
2297
+ return { data: null, error }
2298
+ }
2299
+ throw error
2300
+ }
2301
+ }
2302
+
2303
+ /**
2304
+ * {@see GoTrueMFAApi#verify}
2305
+ */
2306
+ private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
2307
+ return this._acquireLock(-1, async () => {
2308
+ try {
2309
+ return await this._useSession(async (result) => {
2310
+ const { data: sessionData, error: sessionError } = result
2311
+ if (sessionError) {
2312
+ return { data: null, error: sessionError }
2313
+ }
2314
+
2315
+ const { data, error } = await _request(
2316
+ this.fetch,
2317
+ 'POST',
2318
+ `${this.url}/factors/${params.factorId}/verify`,
2319
+ {
2320
+ body: { code: params.code, challenge_id: params.challengeId },
2321
+ headers: this.headers,
2322
+ jwt: sessionData?.session?.access_token,
2323
+ }
2324
+ )
2325
+ if (error) {
2326
+ return { data: null, error }
2327
+ }
2328
+
2329
+ await this._saveSession({
2330
+ expires_at: Math.round(Date.now() / 1000) + data.expires_in,
2331
+ ...data,
2332
+ })
2333
+ await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data)
2334
+
2335
+ return { data, error }
2336
+ })
2337
+ } catch (error) {
2338
+ if (isAuthError(error)) {
2339
+ return { data: null, error }
2340
+ }
2341
+ throw error
2342
+ }
2343
+ })
2344
+ }
2345
+
2346
+ /**
2347
+ * {@see GoTrueMFAApi#challenge}
2348
+ */
2349
+ private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
2350
+ return this._acquireLock(-1, async () => {
2351
+ try {
2352
+ return await this._useSession(async (result) => {
2353
+ const { data: sessionData, error: sessionError } = result
2354
+ if (sessionError) {
2355
+ return { data: null, error: sessionError }
2356
+ }
2357
+
2358
+ return await _request(
2359
+ this.fetch,
2360
+ 'POST',
2361
+ `${this.url}/factors/${params.factorId}/challenge`,
2362
+ {
2363
+ headers: this.headers,
2364
+ jwt: sessionData?.session?.access_token,
2365
+ }
2366
+ )
2367
+ })
2368
+ } catch (error) {
2369
+ if (isAuthError(error)) {
2370
+ return { data: null, error }
2371
+ }
2372
+ throw error
2373
+ }
2374
+ })
2375
+ }
2376
+
2377
+ /**
2378
+ * {@see GoTrueMFAApi#challengeAndVerify}
2379
+ */
2380
+ private async _challengeAndVerify(
2381
+ params: MFAChallengeAndVerifyParams
2382
+ ): Promise<AuthMFAVerifyResponse> {
2383
+ // both _challenge and _verify independently acquire the lock, so no need
2384
+ // to acquire it here
2385
+
2386
+ const { data: challengeData, error: challengeError } = await this._challenge({
2387
+ factorId: params.factorId,
2388
+ })
2389
+ if (challengeError) {
2390
+ return { data: null, error: challengeError }
2391
+ }
2392
+
2393
+ return await this._verify({
2394
+ factorId: params.factorId,
2395
+ challengeId: challengeData.id,
2396
+ code: params.code,
2397
+ })
2398
+ }
2399
+
2400
+ /**
2401
+ * {@see GoTrueMFAApi#listFactors}
2402
+ */
2403
+ private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
2404
+ // use #getUser instead of #_getUser as the former acquires a lock
2405
+ const {
2406
+ data: { user },
2407
+ error: userError,
2408
+ } = await this.getUser()
2409
+ if (userError) {
2410
+ return { data: null, error: userError }
2411
+ }
2412
+
2413
+ const factors = user?.factors || []
2414
+ const totp = factors.filter(
2415
+ (factor) => factor.factor_type === 'totp' && factor.status === 'verified'
2416
+ )
2417
+
2418
+ return {
2419
+ data: {
2420
+ all: factors,
2421
+ totp,
2422
+ },
2423
+ error: null,
2424
+ }
2425
+ }
2426
+
2427
+ /**
2428
+ * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
2429
+ */
2430
+ private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
2431
+ return this._acquireLock(-1, async () => {
2432
+ return await this._useSession(async (result) => {
2433
+ const {
2434
+ data: { session },
2435
+ error: sessionError,
2436
+ } = result
2437
+ if (sessionError) {
2438
+ return { data: null, error: sessionError }
2439
+ }
2440
+ if (!session) {
2441
+ return {
2442
+ data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] },
2443
+ error: null,
2444
+ }
2445
+ }
2446
+
2447
+ const payload = this._decodeJWT(session.access_token)
2448
+
2449
+ let currentLevel: AuthenticatorAssuranceLevels | null = null
2450
+
2451
+ if (payload.aal) {
2452
+ currentLevel = payload.aal
2453
+ }
2454
+
2455
+ let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
2456
+
2457
+ const verifiedFactors =
2458
+ session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
2459
+
2460
+ if (verifiedFactors.length > 0) {
2461
+ nextLevel = 'aal2'
2462
+ }
2463
+
2464
+ const currentAuthenticationMethods = payload.amr || []
2465
+
2466
+ return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
2467
+ })
2468
+ })
2469
+ }
2470
+ }