@supabase/auth-js 2.58.1-canary.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 (192) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -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 +278 -0
  14. package/dist/main/GoTrueAdminApi.js.map +1 -0
  15. package/dist/main/GoTrueClient.d.ts +531 -0
  16. package/dist/main/GoTrueClient.d.ts.map +1 -0
  17. package/dist/main/GoTrueClient.js +2564 -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 +36 -0
  22. package/dist/main/index.js.map +1 -0
  23. package/dist/main/lib/base64url.d.ts +76 -0
  24. package/dist/main/lib/base64url.d.ts.map +1 -0
  25. package/dist/main/lib/base64url.js +269 -0
  26. package/dist/main/lib/base64url.js.map +1 -0
  27. package/dist/main/lib/constants.d.ts +26 -0
  28. package/dist/main/lib/constants.d.ts.map +1 -0
  29. package/dist/main/lib/constants.js +31 -0
  30. package/dist/main/lib/constants.js.map +1 -0
  31. package/dist/main/lib/error-codes.d.ts +7 -0
  32. package/dist/main/lib/error-codes.d.ts.map +1 -0
  33. package/dist/main/lib/error-codes.js +3 -0
  34. package/dist/main/lib/error-codes.js.map +1 -0
  35. package/dist/main/lib/errors.d.ts +100 -0
  36. package/dist/main/lib/errors.d.ts.map +1 -0
  37. package/dist/main/lib/errors.js +137 -0
  38. package/dist/main/lib/errors.js.map +1 -0
  39. package/dist/main/lib/fetch.d.ts +34 -0
  40. package/dist/main/lib/fetch.d.ts.map +1 -0
  41. package/dist/main/lib/fetch.js +194 -0
  42. package/dist/main/lib/fetch.js.map +1 -0
  43. package/dist/main/lib/helpers.d.ts +67 -0
  44. package/dist/main/lib/helpers.d.ts.map +1 -0
  45. package/dist/main/lib/helpers.js +388 -0
  46. package/dist/main/lib/helpers.js.map +1 -0
  47. package/dist/main/lib/local-storage.d.ts +9 -0
  48. package/dist/main/lib/local-storage.d.ts.map +1 -0
  49. package/dist/main/lib/local-storage.js +21 -0
  50. package/dist/main/lib/local-storage.js.map +1 -0
  51. package/dist/main/lib/locks.d.ts +64 -0
  52. package/dist/main/lib/locks.d.ts.map +1 -0
  53. package/dist/main/lib/locks.js +187 -0
  54. package/dist/main/lib/locks.js.map +1 -0
  55. package/dist/main/lib/polyfills.d.ts +5 -0
  56. package/dist/main/lib/polyfills.d.ts.map +1 -0
  57. package/dist/main/lib/polyfills.js +29 -0
  58. package/dist/main/lib/polyfills.js.map +1 -0
  59. package/dist/main/lib/types.d.ts +1130 -0
  60. package/dist/main/lib/types.d.ts.map +1 -0
  61. package/dist/main/lib/types.js +22 -0
  62. package/dist/main/lib/types.js.map +1 -0
  63. package/dist/main/lib/version.d.ts +2 -0
  64. package/dist/main/lib/version.d.ts.map +1 -0
  65. package/dist/main/lib/version.js +11 -0
  66. package/dist/main/lib/version.js.map +1 -0
  67. package/dist/main/lib/web3/ethereum.d.ts +96 -0
  68. package/dist/main/lib/web3/ethereum.d.ts.map +1 -0
  69. package/dist/main/lib/web3/ethereum.js +66 -0
  70. package/dist/main/lib/web3/ethereum.js.map +1 -0
  71. package/dist/main/lib/web3/solana.d.ts +160 -0
  72. package/dist/main/lib/web3/solana.d.ts.map +1 -0
  73. package/dist/main/lib/web3/solana.js +4 -0
  74. package/dist/main/lib/web3/solana.js.map +1 -0
  75. package/dist/main/lib/webauthn.d.ts +274 -0
  76. package/dist/main/lib/webauthn.d.ts.map +1 -0
  77. package/dist/main/lib/webauthn.dom.d.ts +583 -0
  78. package/dist/main/lib/webauthn.dom.d.ts.map +1 -0
  79. package/dist/main/lib/webauthn.dom.js +4 -0
  80. package/dist/main/lib/webauthn.dom.js.map +1 -0
  81. package/dist/main/lib/webauthn.errors.d.ts +80 -0
  82. package/dist/main/lib/webauthn.errors.d.ts.map +1 -0
  83. package/dist/main/lib/webauthn.errors.js +265 -0
  84. package/dist/main/lib/webauthn.errors.js.map +1 -0
  85. package/dist/main/lib/webauthn.js +702 -0
  86. package/dist/main/lib/webauthn.js.map +1 -0
  87. package/dist/module/AuthAdminApi.d.ts +4 -0
  88. package/dist/module/AuthAdminApi.d.ts.map +1 -0
  89. package/dist/module/AuthAdminApi.js +4 -0
  90. package/dist/module/AuthAdminApi.js.map +1 -0
  91. package/dist/module/AuthClient.d.ts +4 -0
  92. package/dist/module/AuthClient.d.ts.map +1 -0
  93. package/dist/module/AuthClient.js +4 -0
  94. package/dist/module/AuthClient.js.map +1 -0
  95. package/dist/module/GoTrueAdminApi.d.ts +99 -0
  96. package/dist/module/GoTrueAdminApi.d.ts.map +1 -0
  97. package/dist/module/GoTrueAdminApi.js +275 -0
  98. package/dist/module/GoTrueAdminApi.js.map +1 -0
  99. package/dist/module/GoTrueClient.d.ts +531 -0
  100. package/dist/module/GoTrueClient.d.ts.map +1 -0
  101. package/dist/module/GoTrueClient.js +2559 -0
  102. package/dist/module/GoTrueClient.js.map +1 -0
  103. package/dist/module/index.d.ts +9 -0
  104. package/dist/module/index.d.ts.map +1 -0
  105. package/dist/module/index.js +9 -0
  106. package/dist/module/index.js.map +1 -0
  107. package/dist/module/lib/base64url.d.ts +76 -0
  108. package/dist/module/lib/base64url.d.ts.map +1 -0
  109. package/dist/module/lib/base64url.js +257 -0
  110. package/dist/module/lib/base64url.js.map +1 -0
  111. package/dist/module/lib/constants.d.ts +26 -0
  112. package/dist/module/lib/constants.d.ts.map +1 -0
  113. package/dist/module/lib/constants.js +28 -0
  114. package/dist/module/lib/constants.js.map +1 -0
  115. package/dist/module/lib/error-codes.d.ts +7 -0
  116. package/dist/module/lib/error-codes.d.ts.map +1 -0
  117. package/dist/module/lib/error-codes.js +2 -0
  118. package/dist/module/lib/error-codes.js.map +1 -0
  119. package/dist/module/lib/errors.d.ts +100 -0
  120. package/dist/module/lib/errors.d.ts.map +1 -0
  121. package/dist/module/lib/errors.js +116 -0
  122. package/dist/module/lib/errors.js.map +1 -0
  123. package/dist/module/lib/fetch.d.ts +34 -0
  124. package/dist/module/lib/fetch.d.ts.map +1 -0
  125. package/dist/module/lib/fetch.js +184 -0
  126. package/dist/module/lib/fetch.js.map +1 -0
  127. package/dist/module/lib/helpers.d.ts +67 -0
  128. package/dist/module/lib/helpers.d.ts.map +1 -0
  129. package/dist/module/lib/helpers.js +329 -0
  130. package/dist/module/lib/helpers.js.map +1 -0
  131. package/dist/module/lib/local-storage.d.ts +9 -0
  132. package/dist/module/lib/local-storage.d.ts.map +1 -0
  133. package/dist/module/lib/local-storage.js +18 -0
  134. package/dist/module/lib/local-storage.js.map +1 -0
  135. package/dist/module/lib/locks.d.ts +64 -0
  136. package/dist/module/lib/locks.d.ts.map +1 -0
  137. package/dist/module/lib/locks.js +179 -0
  138. package/dist/module/lib/locks.js.map +1 -0
  139. package/dist/module/lib/polyfills.d.ts +5 -0
  140. package/dist/module/lib/polyfills.d.ts.map +1 -0
  141. package/dist/module/lib/polyfills.js +26 -0
  142. package/dist/module/lib/polyfills.js.map +1 -0
  143. package/dist/module/lib/types.d.ts +1130 -0
  144. package/dist/module/lib/types.d.ts.map +1 -0
  145. package/dist/module/lib/types.js +19 -0
  146. package/dist/module/lib/types.js.map +1 -0
  147. package/dist/module/lib/version.d.ts +2 -0
  148. package/dist/module/lib/version.d.ts.map +1 -0
  149. package/dist/module/lib/version.js +8 -0
  150. package/dist/module/lib/version.js.map +1 -0
  151. package/dist/module/lib/web3/ethereum.d.ts +96 -0
  152. package/dist/module/lib/web3/ethereum.d.ts.map +1 -0
  153. package/dist/module/lib/web3/ethereum.js +60 -0
  154. package/dist/module/lib/web3/ethereum.js.map +1 -0
  155. package/dist/module/lib/web3/solana.d.ts +160 -0
  156. package/dist/module/lib/web3/solana.d.ts.map +1 -0
  157. package/dist/module/lib/web3/solana.js +3 -0
  158. package/dist/module/lib/web3/solana.js.map +1 -0
  159. package/dist/module/lib/webauthn.d.ts +274 -0
  160. package/dist/module/lib/webauthn.d.ts.map +1 -0
  161. package/dist/module/lib/webauthn.dom.d.ts +583 -0
  162. package/dist/module/lib/webauthn.dom.d.ts.map +1 -0
  163. package/dist/module/lib/webauthn.dom.js +3 -0
  164. package/dist/module/lib/webauthn.dom.js.map +1 -0
  165. package/dist/module/lib/webauthn.errors.d.ts +80 -0
  166. package/dist/module/lib/webauthn.errors.d.ts.map +1 -0
  167. package/dist/module/lib/webauthn.errors.js +257 -0
  168. package/dist/module/lib/webauthn.errors.js.map +1 -0
  169. package/dist/module/lib/webauthn.js +685 -0
  170. package/dist/module/lib/webauthn.js.map +1 -0
  171. package/package.json +49 -0
  172. package/src/AuthAdminApi.ts +5 -0
  173. package/src/AuthClient.ts +5 -0
  174. package/src/GoTrueAdminApi.ts +352 -0
  175. package/src/GoTrueClient.ts +3483 -0
  176. package/src/index.ts +13 -0
  177. package/src/lib/base64url.ts +308 -0
  178. package/src/lib/constants.ts +34 -0
  179. package/src/lib/error-codes.ts +90 -0
  180. package/src/lib/errors.ts +165 -0
  181. package/src/lib/fetch.ts +283 -0
  182. package/src/lib/helpers.ts +416 -0
  183. package/src/lib/local-storage.ts +21 -0
  184. package/src/lib/locks.ts +225 -0
  185. package/src/lib/polyfills.ts +23 -0
  186. package/src/lib/types.ts +1450 -0
  187. package/src/lib/version.ts +7 -0
  188. package/src/lib/web3/ethereum.ts +184 -0
  189. package/src/lib/web3/solana.ts +186 -0
  190. package/src/lib/webauthn.dom.ts +636 -0
  191. package/src/lib/webauthn.errors.ts +317 -0
  192. package/src/lib/webauthn.ts +929 -0
@@ -0,0 +1,3483 @@
1
+ import GoTrueAdminApi from './GoTrueAdminApi'
2
+ import {
3
+ AUTO_REFRESH_TICK_DURATION_MS,
4
+ AUTO_REFRESH_TICK_THRESHOLD,
5
+ DEFAULT_HEADERS,
6
+ EXPIRY_MARGIN_MS,
7
+ GOTRUE_URL,
8
+ JWKS_TTL,
9
+ STORAGE_KEY,
10
+ } from './lib/constants'
11
+ import {
12
+ AuthError,
13
+ AuthImplicitGrantRedirectError,
14
+ AuthInvalidCredentialsError,
15
+ AuthInvalidJwtError,
16
+ AuthInvalidTokenResponseError,
17
+ AuthPKCEGrantCodeExchangeError,
18
+ AuthSessionMissingError,
19
+ AuthUnknownError,
20
+ isAuthApiError,
21
+ isAuthError,
22
+ isAuthImplicitGrantRedirectError,
23
+ isAuthRetryableFetchError,
24
+ isAuthSessionMissingError,
25
+ } from './lib/errors'
26
+ import {
27
+ Fetch,
28
+ _request,
29
+ _sessionResponse,
30
+ _sessionResponsePassword,
31
+ _ssoResponse,
32
+ _userResponse,
33
+ } from './lib/fetch'
34
+ import {
35
+ decodeJWT,
36
+ deepClone,
37
+ Deferred,
38
+ getAlgorithm,
39
+ getCodeChallengeAndMethod,
40
+ getItemAsync,
41
+ isBrowser,
42
+ parseParametersFromURL,
43
+ removeItemAsync,
44
+ resolveFetch,
45
+ retryable,
46
+ setItemAsync,
47
+ sleep,
48
+ supportsLocalStorage,
49
+ userNotAvailableProxy,
50
+ uuid,
51
+ validateExp,
52
+ } from './lib/helpers'
53
+ import { memoryLocalStorageAdapter } from './lib/local-storage'
54
+ import { LockAcquireTimeoutError, navigatorLock } from './lib/locks'
55
+ import { polyfillGlobalThis } from './lib/polyfills'
56
+ import { version } from './lib/version'
57
+
58
+ import { bytesToBase64URL, stringToUint8Array } from './lib/base64url'
59
+ import type {
60
+ AuthChangeEvent,
61
+ AuthenticatorAssuranceLevels,
62
+ AuthFlowType,
63
+ AuthMFAChallengePhoneResponse,
64
+ AuthMFAChallengeResponse,
65
+ AuthMFAChallengeTOTPResponse,
66
+ AuthMFAChallengeWebauthnResponse,
67
+ AuthMFAChallengeWebauthnServerResponse,
68
+ AuthMFAEnrollPhoneResponse,
69
+ AuthMFAEnrollResponse,
70
+ AuthMFAEnrollTOTPResponse,
71
+ AuthMFAEnrollWebauthnResponse,
72
+ AuthMFAGetAuthenticatorAssuranceLevelResponse,
73
+ AuthMFAListFactorsResponse,
74
+ AuthMFAUnenrollResponse,
75
+ AuthMFAVerifyResponse,
76
+ AuthOtpResponse,
77
+ AuthResponse,
78
+ AuthResponsePassword,
79
+ AuthTokenResponse,
80
+ AuthTokenResponsePassword,
81
+ CallRefreshTokenResult,
82
+ EthereumWallet,
83
+ EthereumWeb3Credentials,
84
+ Factor,
85
+ GoTrueClientOptions,
86
+ GoTrueMFAApi,
87
+ InitializeResult,
88
+ JWK,
89
+ JwtHeader,
90
+ JwtPayload,
91
+ LockFunc,
92
+ MFAChallengeAndVerifyParams,
93
+ MFAChallengeParams,
94
+ MFAChallengePhoneParams,
95
+ MFAChallengeTOTPParams,
96
+ MFAChallengeWebauthnParams,
97
+ MFAEnrollParams,
98
+ MFAEnrollPhoneParams,
99
+ MFAEnrollTOTPParams,
100
+ MFAEnrollWebauthnParams,
101
+ MFAUnenrollParams,
102
+ MFAVerifyParams,
103
+ MFAVerifyPhoneParams,
104
+ MFAVerifyTOTPParams,
105
+ MFAVerifyWebauthnParamFields,
106
+ MFAVerifyWebauthnParams,
107
+ OAuthResponse,
108
+ Prettify,
109
+ Provider,
110
+ ResendParams,
111
+ Session,
112
+ SignInAnonymouslyCredentials,
113
+ SignInWithIdTokenCredentials,
114
+ SignInWithOAuthCredentials,
115
+ SignInWithPasswordCredentials,
116
+ SignInWithPasswordlessCredentials,
117
+ SignInWithSSO,
118
+ SignOut,
119
+ SignUpWithPasswordCredentials,
120
+ SolanaWallet,
121
+ SolanaWeb3Credentials,
122
+ SSOResponse,
123
+ StrictOmit,
124
+ Subscription,
125
+ SupportedStorage,
126
+ User,
127
+ UserAttributes,
128
+ UserIdentity,
129
+ UserResponse,
130
+ VerifyOtpParams,
131
+ Web3Credentials,
132
+ } from './lib/types'
133
+ import {
134
+ createSiweMessage,
135
+ fromHex,
136
+ getAddress,
137
+ Hex,
138
+ SiweMessage,
139
+ toHex,
140
+ } from './lib/web3/ethereum'
141
+ import {
142
+ deserializeCredentialCreationOptions,
143
+ deserializeCredentialRequestOptions,
144
+ serializeCredentialCreationResponse,
145
+ serializeCredentialRequestResponse,
146
+ WebAuthnApi,
147
+ } from './lib/webauthn'
148
+ import {
149
+ AuthenticationCredential,
150
+ PublicKeyCredentialJSON,
151
+ RegistrationCredential,
152
+ } from './lib/webauthn.dom'
153
+
154
+ polyfillGlobalThis() // Make "globalThis" available
155
+
156
+ const DEFAULT_OPTIONS: Omit<
157
+ Required<GoTrueClientOptions>,
158
+ 'fetch' | 'storage' | 'userStorage' | 'lock'
159
+ > = {
160
+ url: GOTRUE_URL,
161
+ storageKey: STORAGE_KEY,
162
+ autoRefreshToken: true,
163
+ persistSession: true,
164
+ detectSessionInUrl: true,
165
+ headers: DEFAULT_HEADERS,
166
+ flowType: 'implicit',
167
+ debug: false,
168
+ hasCustomAuthorizationHeader: false,
169
+ }
170
+
171
+ async function lockNoOp<R>(name: string, acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
172
+ return await fn()
173
+ }
174
+
175
+ /**
176
+ * Caches JWKS values for all clients created in the same environment. This is
177
+ * especially useful for shared-memory execution environments such as Vercel's
178
+ * Fluid Compute, AWS Lambda or Supabase's Edge Functions. Regardless of how
179
+ * many clients are created, if they share the same storage key they will use
180
+ * the same JWKS cache, significantly speeding up getClaims() with asymmetric
181
+ * JWTs.
182
+ */
183
+ const GLOBAL_JWKS: { [storageKey: string]: { cachedAt: number; jwks: { keys: JWK[] } } } = {}
184
+
185
+ export default class GoTrueClient {
186
+ private static nextInstanceID = 0
187
+
188
+ private instanceID: number
189
+
190
+ /**
191
+ * Namespace for the GoTrue admin methods.
192
+ * These methods should only be used in a trusted server-side environment.
193
+ */
194
+ admin: GoTrueAdminApi
195
+ /**
196
+ * Namespace for the MFA methods.
197
+ */
198
+ mfa: GoTrueMFAApi
199
+ /**
200
+ * The storage key used to identify the values saved in localStorage
201
+ */
202
+ protected storageKey: string
203
+
204
+ protected flowType: AuthFlowType
205
+
206
+ /**
207
+ * The JWKS used for verifying asymmetric JWTs
208
+ */
209
+ protected get jwks() {
210
+ return GLOBAL_JWKS[this.storageKey]?.jwks ?? { keys: [] }
211
+ }
212
+
213
+ protected set jwks(value: { keys: JWK[] }) {
214
+ GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], jwks: value }
215
+ }
216
+
217
+ protected get jwks_cached_at() {
218
+ return GLOBAL_JWKS[this.storageKey]?.cachedAt ?? Number.MIN_SAFE_INTEGER
219
+ }
220
+
221
+ protected set jwks_cached_at(value: number) {
222
+ GLOBAL_JWKS[this.storageKey] = { ...GLOBAL_JWKS[this.storageKey], cachedAt: value }
223
+ }
224
+
225
+ protected autoRefreshToken: boolean
226
+ protected persistSession: boolean
227
+ protected storage: SupportedStorage
228
+ /**
229
+ * @experimental
230
+ */
231
+ protected userStorage: SupportedStorage | null = null
232
+ protected memoryStorage: { [key: string]: string } | null = null
233
+ protected stateChangeEmitters: Map<string, Subscription> = new Map()
234
+ protected autoRefreshTicker: ReturnType<typeof setInterval> | null = null
235
+ protected visibilityChangedCallback: (() => Promise<any>) | null = null
236
+ protected refreshingDeferred: Deferred<CallRefreshTokenResult> | null = null
237
+ /**
238
+ * Keeps track of the async client initialization.
239
+ * When null or not yet resolved the auth state is `unknown`
240
+ * Once resolved the auth state is known and it's safe to call any further client methods.
241
+ * Keep extra care to never reject or throw uncaught errors
242
+ */
243
+ protected initializePromise: Promise<InitializeResult> | null = null
244
+ protected detectSessionInUrl = true
245
+ protected url: string
246
+ protected headers: {
247
+ [key: string]: string
248
+ }
249
+ protected hasCustomAuthorizationHeader = false
250
+ protected suppressGetSessionWarning = false
251
+ protected fetch: Fetch
252
+ protected lock: LockFunc
253
+ protected lockAcquired = false
254
+ protected pendingInLock: Promise<any>[] = []
255
+
256
+ /**
257
+ * Used to broadcast state change events to other tabs listening.
258
+ */
259
+ protected broadcastChannel: BroadcastChannel | null = null
260
+
261
+ protected logDebugMessages: boolean
262
+ protected logger: (message: string, ...args: any[]) => void = console.log
263
+
264
+ /**
265
+ * Create a new client for use in the browser.
266
+ */
267
+ constructor(options: GoTrueClientOptions) {
268
+ this.instanceID = GoTrueClient.nextInstanceID
269
+ GoTrueClient.nextInstanceID += 1
270
+
271
+ if (this.instanceID > 0 && isBrowser()) {
272
+ console.warn(
273
+ '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.'
274
+ )
275
+ }
276
+
277
+ const settings = { ...DEFAULT_OPTIONS, ...options }
278
+
279
+ this.logDebugMessages = !!settings.debug
280
+ if (typeof settings.debug === 'function') {
281
+ this.logger = settings.debug
282
+ }
283
+
284
+ this.persistSession = settings.persistSession
285
+ this.storageKey = settings.storageKey
286
+ this.autoRefreshToken = settings.autoRefreshToken
287
+ this.admin = new GoTrueAdminApi({
288
+ url: settings.url,
289
+ headers: settings.headers,
290
+ fetch: settings.fetch,
291
+ })
292
+
293
+ this.url = settings.url
294
+ this.headers = settings.headers
295
+ this.fetch = resolveFetch(settings.fetch)
296
+ this.lock = settings.lock || lockNoOp
297
+ this.detectSessionInUrl = settings.detectSessionInUrl
298
+ this.flowType = settings.flowType
299
+ this.hasCustomAuthorizationHeader = settings.hasCustomAuthorizationHeader
300
+
301
+ if (settings.lock) {
302
+ this.lock = settings.lock
303
+ } else if (isBrowser() && globalThis?.navigator?.locks) {
304
+ this.lock = navigatorLock
305
+ } else {
306
+ this.lock = lockNoOp
307
+ }
308
+
309
+ if (!this.jwks) {
310
+ this.jwks = { keys: [] }
311
+ this.jwks_cached_at = Number.MIN_SAFE_INTEGER
312
+ }
313
+
314
+ this.mfa = {
315
+ verify: this._verify.bind(this),
316
+ enroll: this._enroll.bind(this),
317
+ unenroll: this._unenroll.bind(this),
318
+ challenge: this._challenge.bind(this),
319
+ listFactors: this._listFactors.bind(this),
320
+ challengeAndVerify: this._challengeAndVerify.bind(this),
321
+ getAuthenticatorAssuranceLevel: this._getAuthenticatorAssuranceLevel.bind(this),
322
+ webauthn: new WebAuthnApi(this),
323
+ }
324
+
325
+ if (this.persistSession) {
326
+ if (settings.storage) {
327
+ this.storage = settings.storage
328
+ } else {
329
+ if (supportsLocalStorage()) {
330
+ this.storage = globalThis.localStorage
331
+ } else {
332
+ this.memoryStorage = {}
333
+ this.storage = memoryLocalStorageAdapter(this.memoryStorage)
334
+ }
335
+ }
336
+
337
+ if (settings.userStorage) {
338
+ this.userStorage = settings.userStorage
339
+ }
340
+ } else {
341
+ this.memoryStorage = {}
342
+ this.storage = memoryLocalStorageAdapter(this.memoryStorage)
343
+ }
344
+
345
+ if (isBrowser() && globalThis.BroadcastChannel && this.persistSession && this.storageKey) {
346
+ try {
347
+ this.broadcastChannel = new globalThis.BroadcastChannel(this.storageKey)
348
+ } catch (e: any) {
349
+ console.error(
350
+ 'Failed to create a new BroadcastChannel, multi-tab state changes will not be available',
351
+ e
352
+ )
353
+ }
354
+
355
+ this.broadcastChannel?.addEventListener('message', async (event) => {
356
+ this._debug('received broadcast notification from other tab or client', event)
357
+
358
+ await this._notifyAllSubscribers(event.data.event, event.data.session, false) // broadcast = false so we don't get an endless loop of messages
359
+ })
360
+ }
361
+
362
+ this.initialize()
363
+ }
364
+
365
+ private _debug(...args: any[]): GoTrueClient {
366
+ if (this.logDebugMessages) {
367
+ this.logger(
368
+ `GoTrueClient@${this.instanceID} (${version}) ${new Date().toISOString()}`,
369
+ ...args
370
+ )
371
+ }
372
+
373
+ return this
374
+ }
375
+
376
+ /**
377
+ * Initializes the client session either from the url or from storage.
378
+ * This method is automatically called when instantiating the client, but should also be called
379
+ * manually when checking for an error from an auth redirect (oauth, magiclink, password recovery, etc).
380
+ */
381
+ async initialize(): Promise<InitializeResult> {
382
+ if (this.initializePromise) {
383
+ return await this.initializePromise
384
+ }
385
+
386
+ this.initializePromise = (async () => {
387
+ return await this._acquireLock(-1, async () => {
388
+ return await this._initialize()
389
+ })
390
+ })()
391
+
392
+ return await this.initializePromise
393
+ }
394
+
395
+ /**
396
+ * IMPORTANT:
397
+ * 1. Never throw in this method, as it is called from the constructor
398
+ * 2. Never return a session from this method as it would be cached over
399
+ * the whole lifetime of the client
400
+ */
401
+ private async _initialize(): Promise<InitializeResult> {
402
+ try {
403
+ const params = parseParametersFromURL(window.location.href)
404
+ let callbackUrlType = 'none'
405
+ if (this._isImplicitGrantCallback(params)) {
406
+ callbackUrlType = 'implicit'
407
+ } else if (await this._isPKCECallback(params)) {
408
+ callbackUrlType = 'pkce'
409
+ }
410
+
411
+ /**
412
+ * Attempt to get the session from the URL only if these conditions are fulfilled
413
+ *
414
+ * Note: If the URL isn't one of the callback url types (implicit or pkce),
415
+ * then there could be an existing session so we don't want to prematurely remove it
416
+ */
417
+ if (isBrowser() && this.detectSessionInUrl && callbackUrlType !== 'none') {
418
+ const { data, error } = await this._getSessionFromURL(params, callbackUrlType)
419
+ if (error) {
420
+ this._debug('#_initialize()', 'error detecting session from URL', error)
421
+
422
+ if (isAuthImplicitGrantRedirectError(error)) {
423
+ const errorCode = error.details?.code
424
+ if (
425
+ errorCode === 'identity_already_exists' ||
426
+ errorCode === 'identity_not_found' ||
427
+ errorCode === 'single_identity_not_deletable'
428
+ ) {
429
+ return { error }
430
+ }
431
+ }
432
+
433
+ // failed login attempt via url,
434
+ // remove old session as in verifyOtp, signUp and signInWith*
435
+ await this._removeSession()
436
+
437
+ return { error }
438
+ }
439
+
440
+ const { session, redirectType } = data
441
+
442
+ this._debug(
443
+ '#_initialize()',
444
+ 'detected session in URL',
445
+ session,
446
+ 'redirect type',
447
+ redirectType
448
+ )
449
+
450
+ await this._saveSession(session)
451
+
452
+ setTimeout(async () => {
453
+ if (redirectType === 'recovery') {
454
+ await this._notifyAllSubscribers('PASSWORD_RECOVERY', session)
455
+ } else {
456
+ await this._notifyAllSubscribers('SIGNED_IN', session)
457
+ }
458
+ }, 0)
459
+
460
+ return { error: null }
461
+ }
462
+ // no login attempt via callback url try to recover session from storage
463
+ await this._recoverAndRefresh()
464
+ return { error: null }
465
+ } catch (error) {
466
+ if (isAuthError(error)) {
467
+ return { error }
468
+ }
469
+
470
+ return {
471
+ error: new AuthUnknownError('Unexpected error during initialization', error),
472
+ }
473
+ } finally {
474
+ await this._handleVisibilityChange()
475
+ this._debug('#_initialize()', 'end')
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Creates a new anonymous user.
481
+ *
482
+ * @returns A session where the is_anonymous claim in the access token JWT set to true
483
+ */
484
+ async signInAnonymously(credentials?: SignInAnonymouslyCredentials): Promise<AuthResponse> {
485
+ try {
486
+ const res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
487
+ headers: this.headers,
488
+ body: {
489
+ data: credentials?.options?.data ?? {},
490
+ gotrue_meta_security: { captcha_token: credentials?.options?.captchaToken },
491
+ },
492
+ xform: _sessionResponse,
493
+ })
494
+ const { data, error } = res
495
+
496
+ if (error || !data) {
497
+ return { data: { user: null, session: null }, error: error }
498
+ }
499
+ const session: Session | null = data.session
500
+ const user: User | null = data.user
501
+
502
+ if (data.session) {
503
+ await this._saveSession(data.session)
504
+ await this._notifyAllSubscribers('SIGNED_IN', session)
505
+ }
506
+
507
+ return { data: { user, session }, error: null }
508
+ } catch (error) {
509
+ if (isAuthError(error)) {
510
+ return { data: { user: null, session: null }, error }
511
+ }
512
+
513
+ throw error
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Creates a new user.
519
+ *
520
+ * Be aware that if a user account exists in the system you may get back an
521
+ * error message that attempts to hide this information from the user.
522
+ * This method has support for PKCE via email signups. The PKCE flow cannot be used when autoconfirm is enabled.
523
+ *
524
+ * @returns A logged-in session if the server has "autoconfirm" ON
525
+ * @returns A user if the server has "autoconfirm" OFF
526
+ */
527
+ async signUp(credentials: SignUpWithPasswordCredentials): Promise<AuthResponse> {
528
+ try {
529
+ let res: AuthResponse
530
+ if ('email' in credentials) {
531
+ const { email, password, options } = credentials
532
+ let codeChallenge: string | null = null
533
+ let codeChallengeMethod: string | null = null
534
+ if (this.flowType === 'pkce') {
535
+ ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
536
+ this.storage,
537
+ this.storageKey
538
+ )
539
+ }
540
+ res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
541
+ headers: this.headers,
542
+ redirectTo: options?.emailRedirectTo,
543
+ body: {
544
+ email,
545
+ password,
546
+ data: options?.data ?? {},
547
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
548
+ code_challenge: codeChallenge,
549
+ code_challenge_method: codeChallengeMethod,
550
+ },
551
+ xform: _sessionResponse,
552
+ })
553
+ } else if ('phone' in credentials) {
554
+ const { phone, password, options } = credentials
555
+ res = await _request(this.fetch, 'POST', `${this.url}/signup`, {
556
+ headers: this.headers,
557
+ body: {
558
+ phone,
559
+ password,
560
+ data: options?.data ?? {},
561
+ channel: options?.channel ?? 'sms',
562
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
563
+ },
564
+ xform: _sessionResponse,
565
+ })
566
+ } else {
567
+ throw new AuthInvalidCredentialsError(
568
+ 'You must provide either an email or phone number and a password'
569
+ )
570
+ }
571
+
572
+ const { data, error } = res
573
+
574
+ if (error || !data) {
575
+ return { data: { user: null, session: null }, error: error }
576
+ }
577
+
578
+ const session: Session | null = data.session
579
+ const user: User | null = data.user
580
+
581
+ if (data.session) {
582
+ await this._saveSession(data.session)
583
+ await this._notifyAllSubscribers('SIGNED_IN', session)
584
+ }
585
+
586
+ return { data: { user, session }, error: null }
587
+ } catch (error) {
588
+ if (isAuthError(error)) {
589
+ return { data: { user: null, session: null }, error }
590
+ }
591
+
592
+ throw error
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Log in an existing user with an email and password or phone and password.
598
+ *
599
+ * Be aware that you may get back an error message that will not distinguish
600
+ * between the cases where the account does not exist or that the
601
+ * email/phone and password combination is wrong or that the account can only
602
+ * be accessed via social login.
603
+ */
604
+ async signInWithPassword(
605
+ credentials: SignInWithPasswordCredentials
606
+ ): Promise<AuthTokenResponsePassword> {
607
+ try {
608
+ let res: AuthResponsePassword
609
+ if ('email' in credentials) {
610
+ const { email, password, options } = credentials
611
+ res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
612
+ headers: this.headers,
613
+ body: {
614
+ email,
615
+ password,
616
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
617
+ },
618
+ xform: _sessionResponsePassword,
619
+ })
620
+ } else if ('phone' in credentials) {
621
+ const { phone, password, options } = credentials
622
+ res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=password`, {
623
+ headers: this.headers,
624
+ body: {
625
+ phone,
626
+ password,
627
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
628
+ },
629
+ xform: _sessionResponsePassword,
630
+ })
631
+ } else {
632
+ throw new AuthInvalidCredentialsError(
633
+ 'You must provide either an email or phone number and a password'
634
+ )
635
+ }
636
+ const { data, error } = res
637
+
638
+ if (error) {
639
+ return { data: { user: null, session: null }, error }
640
+ } else if (!data || !data.session || !data.user) {
641
+ return { data: { user: null, session: null }, error: new AuthInvalidTokenResponseError() }
642
+ }
643
+ if (data.session) {
644
+ await this._saveSession(data.session)
645
+ await this._notifyAllSubscribers('SIGNED_IN', data.session)
646
+ }
647
+ return {
648
+ data: {
649
+ user: data.user,
650
+ session: data.session,
651
+ ...(data.weak_password ? { weakPassword: data.weak_password } : null),
652
+ },
653
+ error,
654
+ }
655
+ } catch (error) {
656
+ if (isAuthError(error)) {
657
+ return { data: { user: null, session: null }, error }
658
+ }
659
+ throw error
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Log in an existing user via a third-party provider.
665
+ * This method supports the PKCE flow.
666
+ */
667
+ async signInWithOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
668
+ return await this._handleProviderSignIn(credentials.provider, {
669
+ redirectTo: credentials.options?.redirectTo,
670
+ scopes: credentials.options?.scopes,
671
+ queryParams: credentials.options?.queryParams,
672
+ skipBrowserRedirect: credentials.options?.skipBrowserRedirect,
673
+ })
674
+ }
675
+
676
+ /**
677
+ * Log in an existing user by exchanging an Auth Code issued during the PKCE flow.
678
+ */
679
+ async exchangeCodeForSession(authCode: string): Promise<AuthTokenResponse> {
680
+ await this.initializePromise
681
+
682
+ return this._acquireLock(-1, async () => {
683
+ return this._exchangeCodeForSession(authCode)
684
+ })
685
+ }
686
+
687
+ /**
688
+ * Signs in a user by verifying a message signed by the user's private key.
689
+ * Supports Ethereum (via Sign-In-With-Ethereum) & Solana (Sign-In-With-Solana) standards,
690
+ * both of which derive from the EIP-4361 standard
691
+ * With slight variation on Solana's side.
692
+ * @reference https://eips.ethereum.org/EIPS/eip-4361
693
+ */
694
+ async signInWithWeb3(credentials: Web3Credentials): Promise<
695
+ | {
696
+ data: { session: Session; user: User }
697
+ error: null
698
+ }
699
+ | { data: { session: null; user: null }; error: AuthError }
700
+ > {
701
+ const { chain } = credentials
702
+
703
+ switch (chain) {
704
+ case 'ethereum':
705
+ return await this.signInWithEthereum(credentials)
706
+ case 'solana':
707
+ return await this.signInWithSolana(credentials)
708
+ default:
709
+ throw new Error(`@supabase/auth-js: Unsupported chain "${chain}"`)
710
+ }
711
+ }
712
+
713
+ private async signInWithEthereum(
714
+ credentials: EthereumWeb3Credentials
715
+ ): Promise<
716
+ | { data: { session: Session; user: User }; error: null }
717
+ | { data: { session: null; user: null }; error: AuthError }
718
+ > {
719
+ // TODO: flatten type
720
+ let message: string
721
+ let signature: Hex
722
+
723
+ if ('message' in credentials) {
724
+ message = credentials.message
725
+ signature = credentials.signature
726
+ } else {
727
+ const { chain, wallet, statement, options } = credentials
728
+
729
+ let resolvedWallet: EthereumWallet
730
+
731
+ if (!isBrowser()) {
732
+ if (typeof wallet !== 'object' || !options?.url) {
733
+ throw new Error(
734
+ '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
735
+ )
736
+ }
737
+
738
+ resolvedWallet = wallet
739
+ } else if (typeof wallet === 'object') {
740
+ resolvedWallet = wallet
741
+ } else {
742
+ const windowAny = window as any
743
+
744
+ if (
745
+ 'ethereum' in windowAny &&
746
+ typeof windowAny.ethereum === 'object' &&
747
+ 'request' in windowAny.ethereum &&
748
+ typeof windowAny.ethereum.request === 'function'
749
+ ) {
750
+ resolvedWallet = windowAny.ethereum
751
+ } else {
752
+ throw new Error(
753
+ `@supabase/auth-js: No compatible Ethereum wallet interface on the window object (window.ethereum) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'ethereum', wallet: resolvedUserWallet }) instead.`
754
+ )
755
+ }
756
+ }
757
+
758
+ const url = new URL(options?.url ?? window.location.href)
759
+
760
+ const accounts = await resolvedWallet
761
+ .request({
762
+ method: 'eth_requestAccounts',
763
+ })
764
+ .then((accs) => accs as string[])
765
+ .catch(() => {
766
+ throw new Error(
767
+ `@supabase/auth-js: Wallet method eth_requestAccounts is missing or invalid`
768
+ )
769
+ })
770
+
771
+ if (!accounts || accounts.length === 0) {
772
+ throw new Error(
773
+ `@supabase/auth-js: No accounts available. Please ensure the wallet is connected.`
774
+ )
775
+ }
776
+
777
+ const address = getAddress(accounts[0])
778
+
779
+ let chainId = options?.signInWithEthereum?.chainId
780
+ if (!chainId) {
781
+ const chainIdHex = await resolvedWallet.request({
782
+ method: 'eth_chainId',
783
+ })
784
+ chainId = fromHex(chainIdHex as Hex)
785
+ }
786
+
787
+ const siweMessage: SiweMessage = {
788
+ domain: url.host,
789
+ address: address,
790
+ statement: statement,
791
+ uri: url.href,
792
+ version: '1',
793
+ chainId: chainId,
794
+ nonce: options?.signInWithEthereum?.nonce,
795
+ issuedAt: options?.signInWithEthereum?.issuedAt ?? new Date(),
796
+ expirationTime: options?.signInWithEthereum?.expirationTime,
797
+ notBefore: options?.signInWithEthereum?.notBefore,
798
+ requestId: options?.signInWithEthereum?.requestId,
799
+ resources: options?.signInWithEthereum?.resources,
800
+ }
801
+
802
+ message = createSiweMessage(siweMessage)
803
+
804
+ // Sign message
805
+ signature = (await resolvedWallet.request({
806
+ method: 'personal_sign',
807
+ params: [toHex(message), address],
808
+ })) as Hex
809
+ }
810
+
811
+ try {
812
+ const { data, error } = await _request(
813
+ this.fetch,
814
+ 'POST',
815
+ `${this.url}/token?grant_type=web3`,
816
+ {
817
+ headers: this.headers,
818
+ body: {
819
+ chain: 'ethereum',
820
+ message,
821
+ signature,
822
+ ...(credentials.options?.captchaToken
823
+ ? { gotrue_meta_security: { captcha_token: credentials.options?.captchaToken } }
824
+ : null),
825
+ },
826
+ xform: _sessionResponse,
827
+ }
828
+ )
829
+ if (error) {
830
+ throw error
831
+ }
832
+ if (!data || !data.session || !data.user) {
833
+ return {
834
+ data: { user: null, session: null },
835
+ error: new AuthInvalidTokenResponseError(),
836
+ }
837
+ }
838
+ if (data.session) {
839
+ await this._saveSession(data.session)
840
+ await this._notifyAllSubscribers('SIGNED_IN', data.session)
841
+ }
842
+ return { data: { ...data }, error }
843
+ } catch (error) {
844
+ if (isAuthError(error)) {
845
+ return { data: { user: null, session: null }, error }
846
+ }
847
+
848
+ throw error
849
+ }
850
+ }
851
+
852
+ private async signInWithSolana(credentials: SolanaWeb3Credentials) {
853
+ let message: string
854
+ let signature: Uint8Array
855
+
856
+ if ('message' in credentials) {
857
+ message = credentials.message
858
+ signature = credentials.signature
859
+ } else {
860
+ const { chain, wallet, statement, options } = credentials
861
+
862
+ let resolvedWallet: SolanaWallet
863
+
864
+ if (!isBrowser()) {
865
+ if (typeof wallet !== 'object' || !options?.url) {
866
+ throw new Error(
867
+ '@supabase/auth-js: Both wallet and url must be specified in non-browser environments.'
868
+ )
869
+ }
870
+
871
+ resolvedWallet = wallet
872
+ } else if (typeof wallet === 'object') {
873
+ resolvedWallet = wallet
874
+ } else {
875
+ const windowAny = window as any
876
+
877
+ if (
878
+ 'solana' in windowAny &&
879
+ typeof windowAny.solana === 'object' &&
880
+ (('signIn' in windowAny.solana && typeof windowAny.solana.signIn === 'function') ||
881
+ ('signMessage' in windowAny.solana &&
882
+ typeof windowAny.solana.signMessage === 'function'))
883
+ ) {
884
+ resolvedWallet = windowAny.solana
885
+ } else {
886
+ throw new Error(
887
+ `@supabase/auth-js: No compatible Solana wallet interface on the window object (window.solana) detected. Make sure the user already has a wallet installed and connected for this app. Prefer passing the wallet interface object directly to signInWithWeb3({ chain: 'solana', wallet: resolvedUserWallet }) instead.`
888
+ )
889
+ }
890
+ }
891
+
892
+ const url = new URL(options?.url ?? window.location.href)
893
+
894
+ if ('signIn' in resolvedWallet && resolvedWallet.signIn) {
895
+ const output = await resolvedWallet.signIn({
896
+ issuedAt: new Date().toISOString(),
897
+
898
+ ...options?.signInWithSolana,
899
+
900
+ // non-overridable properties
901
+ version: '1',
902
+ domain: url.host,
903
+ uri: url.href,
904
+
905
+ ...(statement ? { statement } : null),
906
+ })
907
+
908
+ let outputToProcess: any
909
+
910
+ if (Array.isArray(output) && output[0] && typeof output[0] === 'object') {
911
+ outputToProcess = output[0]
912
+ } else if (
913
+ output &&
914
+ typeof output === 'object' &&
915
+ 'signedMessage' in output &&
916
+ 'signature' in output
917
+ ) {
918
+ outputToProcess = output
919
+ } else {
920
+ throw new Error('@supabase/auth-js: Wallet method signIn() returned unrecognized value')
921
+ }
922
+
923
+ if (
924
+ 'signedMessage' in outputToProcess &&
925
+ 'signature' in outputToProcess &&
926
+ (typeof outputToProcess.signedMessage === 'string' ||
927
+ outputToProcess.signedMessage instanceof Uint8Array) &&
928
+ outputToProcess.signature instanceof Uint8Array
929
+ ) {
930
+ message =
931
+ typeof outputToProcess.signedMessage === 'string'
932
+ ? outputToProcess.signedMessage
933
+ : new TextDecoder().decode(outputToProcess.signedMessage)
934
+ signature = outputToProcess.signature
935
+ } else {
936
+ throw new Error(
937
+ '@supabase/auth-js: Wallet method signIn() API returned object without signedMessage and signature fields'
938
+ )
939
+ }
940
+ } else {
941
+ if (
942
+ !('signMessage' in resolvedWallet) ||
943
+ typeof resolvedWallet.signMessage !== 'function' ||
944
+ !('publicKey' in resolvedWallet) ||
945
+ typeof resolvedWallet !== 'object' ||
946
+ !resolvedWallet.publicKey ||
947
+ !('toBase58' in resolvedWallet.publicKey) ||
948
+ typeof resolvedWallet.publicKey.toBase58 !== 'function'
949
+ ) {
950
+ throw new Error(
951
+ '@supabase/auth-js: Wallet does not have a compatible signMessage() and publicKey.toBase58() API'
952
+ )
953
+ }
954
+
955
+ message = [
956
+ `${url.host} wants you to sign in with your Solana account:`,
957
+ resolvedWallet.publicKey.toBase58(),
958
+ ...(statement ? ['', statement, ''] : ['']),
959
+ 'Version: 1',
960
+ `URI: ${url.href}`,
961
+ `Issued At: ${options?.signInWithSolana?.issuedAt ?? new Date().toISOString()}`,
962
+ ...(options?.signInWithSolana?.notBefore
963
+ ? [`Not Before: ${options.signInWithSolana.notBefore}`]
964
+ : []),
965
+ ...(options?.signInWithSolana?.expirationTime
966
+ ? [`Expiration Time: ${options.signInWithSolana.expirationTime}`]
967
+ : []),
968
+ ...(options?.signInWithSolana?.chainId
969
+ ? [`Chain ID: ${options.signInWithSolana.chainId}`]
970
+ : []),
971
+ ...(options?.signInWithSolana?.nonce ? [`Nonce: ${options.signInWithSolana.nonce}`] : []),
972
+ ...(options?.signInWithSolana?.requestId
973
+ ? [`Request ID: ${options.signInWithSolana.requestId}`]
974
+ : []),
975
+ ...(options?.signInWithSolana?.resources?.length
976
+ ? [
977
+ 'Resources',
978
+ ...options.signInWithSolana.resources.map((resource) => `- ${resource}`),
979
+ ]
980
+ : []),
981
+ ].join('\n')
982
+
983
+ const maybeSignature = await resolvedWallet.signMessage(
984
+ new TextEncoder().encode(message),
985
+ 'utf8'
986
+ )
987
+
988
+ if (!maybeSignature || !(maybeSignature instanceof Uint8Array)) {
989
+ throw new Error(
990
+ '@supabase/auth-js: Wallet signMessage() API returned an recognized value'
991
+ )
992
+ }
993
+
994
+ signature = maybeSignature
995
+ }
996
+ }
997
+
998
+ try {
999
+ const { data, error } = await _request(
1000
+ this.fetch,
1001
+ 'POST',
1002
+ `${this.url}/token?grant_type=web3`,
1003
+ {
1004
+ headers: this.headers,
1005
+ body: {
1006
+ chain: 'solana',
1007
+ message,
1008
+ signature: bytesToBase64URL(signature),
1009
+
1010
+ ...(credentials.options?.captchaToken
1011
+ ? { gotrue_meta_security: { captcha_token: credentials.options?.captchaToken } }
1012
+ : null),
1013
+ },
1014
+ xform: _sessionResponse,
1015
+ }
1016
+ )
1017
+ if (error) {
1018
+ throw error
1019
+ }
1020
+ if (!data || !data.session || !data.user) {
1021
+ return {
1022
+ data: { user: null, session: null },
1023
+ error: new AuthInvalidTokenResponseError(),
1024
+ }
1025
+ }
1026
+ if (data.session) {
1027
+ await this._saveSession(data.session)
1028
+ await this._notifyAllSubscribers('SIGNED_IN', data.session)
1029
+ }
1030
+ return { data: { ...data }, error }
1031
+ } catch (error) {
1032
+ if (isAuthError(error)) {
1033
+ return { data: { user: null, session: null }, error }
1034
+ }
1035
+
1036
+ throw error
1037
+ }
1038
+ }
1039
+
1040
+ private async _exchangeCodeForSession(authCode: string): Promise<
1041
+ | {
1042
+ data: { session: Session; user: User; redirectType: string | null }
1043
+ error: null
1044
+ }
1045
+ | { data: { session: null; user: null; redirectType: null }; error: AuthError }
1046
+ > {
1047
+ const storageItem = await getItemAsync(this.storage, `${this.storageKey}-code-verifier`)
1048
+ const [codeVerifier, redirectType] = ((storageItem ?? '') as string).split('/')
1049
+
1050
+ try {
1051
+ const { data, error } = await _request(
1052
+ this.fetch,
1053
+ 'POST',
1054
+ `${this.url}/token?grant_type=pkce`,
1055
+ {
1056
+ headers: this.headers,
1057
+ body: {
1058
+ auth_code: authCode,
1059
+ code_verifier: codeVerifier,
1060
+ },
1061
+ xform: _sessionResponse,
1062
+ }
1063
+ )
1064
+ await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
1065
+ if (error) {
1066
+ throw error
1067
+ }
1068
+ if (!data || !data.session || !data.user) {
1069
+ return {
1070
+ data: { user: null, session: null, redirectType: null },
1071
+ error: new AuthInvalidTokenResponseError(),
1072
+ }
1073
+ }
1074
+ if (data.session) {
1075
+ await this._saveSession(data.session)
1076
+ await this._notifyAllSubscribers('SIGNED_IN', data.session)
1077
+ }
1078
+ return { data: { ...data, redirectType: redirectType ?? null }, error }
1079
+ } catch (error) {
1080
+ if (isAuthError(error)) {
1081
+ return { data: { user: null, session: null, redirectType: null }, error }
1082
+ }
1083
+
1084
+ throw error
1085
+ }
1086
+ }
1087
+
1088
+ /**
1089
+ * Allows signing in with an OIDC ID token. The authentication provider used
1090
+ * should be enabled and configured.
1091
+ */
1092
+ async signInWithIdToken(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse> {
1093
+ try {
1094
+ const { options, provider, token, access_token, nonce } = credentials
1095
+
1096
+ const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
1097
+ headers: this.headers,
1098
+ body: {
1099
+ provider,
1100
+ id_token: token,
1101
+ access_token,
1102
+ nonce,
1103
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
1104
+ },
1105
+ xform: _sessionResponse,
1106
+ })
1107
+
1108
+ const { data, error } = res
1109
+ if (error) {
1110
+ return { data: { user: null, session: null }, error }
1111
+ } else if (!data || !data.session || !data.user) {
1112
+ return {
1113
+ data: { user: null, session: null },
1114
+ error: new AuthInvalidTokenResponseError(),
1115
+ }
1116
+ }
1117
+ if (data.session) {
1118
+ await this._saveSession(data.session)
1119
+ await this._notifyAllSubscribers('SIGNED_IN', data.session)
1120
+ }
1121
+ return { data, error }
1122
+ } catch (error) {
1123
+ if (isAuthError(error)) {
1124
+ return { data: { user: null, session: null }, error }
1125
+ }
1126
+ throw error
1127
+ }
1128
+ }
1129
+
1130
+ /**
1131
+ * Log in a user using magiclink or a one-time password (OTP).
1132
+ *
1133
+ * If the `{{ .ConfirmationURL }}` variable is specified in the email template, a magiclink will be sent.
1134
+ * If the `{{ .Token }}` variable is specified in the email template, an OTP will be sent.
1135
+ * 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.
1136
+ *
1137
+ * Be aware that you may get back an error message that will not distinguish
1138
+ * between the cases where the account does not exist or, that the account
1139
+ * can only be accessed via social login.
1140
+ *
1141
+ * Do note that you will need to configure a Whatsapp sender on Twilio
1142
+ * if you are using phone sign in with the 'whatsapp' channel. The whatsapp
1143
+ * channel is not supported on other providers
1144
+ * at this time.
1145
+ * This method supports PKCE when an email is passed.
1146
+ */
1147
+ async signInWithOtp(credentials: SignInWithPasswordlessCredentials): Promise<AuthOtpResponse> {
1148
+ try {
1149
+ if ('email' in credentials) {
1150
+ const { email, options } = credentials
1151
+ let codeChallenge: string | null = null
1152
+ let codeChallengeMethod: string | null = null
1153
+ if (this.flowType === 'pkce') {
1154
+ ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
1155
+ this.storage,
1156
+ this.storageKey
1157
+ )
1158
+ }
1159
+ const { error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
1160
+ headers: this.headers,
1161
+ body: {
1162
+ email,
1163
+ data: options?.data ?? {},
1164
+ create_user: options?.shouldCreateUser ?? true,
1165
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
1166
+ code_challenge: codeChallenge,
1167
+ code_challenge_method: codeChallengeMethod,
1168
+ },
1169
+ redirectTo: options?.emailRedirectTo,
1170
+ })
1171
+ return { data: { user: null, session: null }, error }
1172
+ }
1173
+ if ('phone' in credentials) {
1174
+ const { phone, options } = credentials
1175
+ const { data, error } = await _request(this.fetch, 'POST', `${this.url}/otp`, {
1176
+ headers: this.headers,
1177
+ body: {
1178
+ phone,
1179
+ data: options?.data ?? {},
1180
+ create_user: options?.shouldCreateUser ?? true,
1181
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
1182
+ channel: options?.channel ?? 'sms',
1183
+ },
1184
+ })
1185
+ return { data: { user: null, session: null, messageId: data?.message_id }, error }
1186
+ }
1187
+ throw new AuthInvalidCredentialsError('You must provide either an email or phone number.')
1188
+ } catch (error) {
1189
+ if (isAuthError(error)) {
1190
+ return { data: { user: null, session: null }, error }
1191
+ }
1192
+
1193
+ throw error
1194
+ }
1195
+ }
1196
+
1197
+ /**
1198
+ * Log in a user given a User supplied OTP or TokenHash received through mobile or email.
1199
+ */
1200
+ async verifyOtp(params: VerifyOtpParams): Promise<AuthResponse> {
1201
+ try {
1202
+ let redirectTo: string | undefined = undefined
1203
+ let captchaToken: string | undefined = undefined
1204
+ if ('options' in params) {
1205
+ redirectTo = params.options?.redirectTo
1206
+ captchaToken = params.options?.captchaToken
1207
+ }
1208
+ const { data, error } = await _request(this.fetch, 'POST', `${this.url}/verify`, {
1209
+ headers: this.headers,
1210
+ body: {
1211
+ ...params,
1212
+ gotrue_meta_security: { captcha_token: captchaToken },
1213
+ },
1214
+ redirectTo,
1215
+ xform: _sessionResponse,
1216
+ })
1217
+
1218
+ if (error) {
1219
+ throw error
1220
+ }
1221
+
1222
+ if (!data) {
1223
+ throw new Error('An error occurred on token verification.')
1224
+ }
1225
+
1226
+ const session: Session | null = data.session
1227
+ const user: User = data.user
1228
+
1229
+ if (session?.access_token) {
1230
+ await this._saveSession(session as Session)
1231
+ await this._notifyAllSubscribers(
1232
+ params.type == 'recovery' ? 'PASSWORD_RECOVERY' : 'SIGNED_IN',
1233
+ session
1234
+ )
1235
+ }
1236
+
1237
+ return { data: { user, session }, error: null }
1238
+ } catch (error) {
1239
+ if (isAuthError(error)) {
1240
+ return { data: { user: null, session: null }, error }
1241
+ }
1242
+
1243
+ throw error
1244
+ }
1245
+ }
1246
+
1247
+ /**
1248
+ * Attempts a single-sign on using an enterprise Identity Provider. A
1249
+ * successful SSO attempt will redirect the current page to the identity
1250
+ * provider authorization page. The redirect URL is implementation and SSO
1251
+ * protocol specific.
1252
+ *
1253
+ * You can use it by providing a SSO domain. Typically you can extract this
1254
+ * domain by asking users for their email address. If this domain is
1255
+ * registered on the Auth instance the redirect will use that organization's
1256
+ * currently active SSO Identity Provider for the login.
1257
+ *
1258
+ * If you have built an organization-specific login page, you can use the
1259
+ * organization's SSO Identity Provider UUID directly instead.
1260
+ */
1261
+ async signInWithSSO(params: SignInWithSSO): Promise<SSOResponse> {
1262
+ try {
1263
+ let codeChallenge: string | null = null
1264
+ let codeChallengeMethod: string | null = null
1265
+ if (this.flowType === 'pkce') {
1266
+ ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
1267
+ this.storage,
1268
+ this.storageKey
1269
+ )
1270
+ }
1271
+
1272
+ return await _request(this.fetch, 'POST', `${this.url}/sso`, {
1273
+ body: {
1274
+ ...('providerId' in params ? { provider_id: params.providerId } : null),
1275
+ ...('domain' in params ? { domain: params.domain } : null),
1276
+ redirect_to: params.options?.redirectTo ?? undefined,
1277
+ ...(params?.options?.captchaToken
1278
+ ? { gotrue_meta_security: { captcha_token: params.options.captchaToken } }
1279
+ : null),
1280
+ skip_http_redirect: true, // fetch does not handle redirects
1281
+ code_challenge: codeChallenge,
1282
+ code_challenge_method: codeChallengeMethod,
1283
+ },
1284
+ headers: this.headers,
1285
+ xform: _ssoResponse,
1286
+ })
1287
+ } catch (error) {
1288
+ if (isAuthError(error)) {
1289
+ return { data: null, error }
1290
+ }
1291
+ throw error
1292
+ }
1293
+ }
1294
+
1295
+ /**
1296
+ * Sends a reauthentication OTP to the user's email or phone number.
1297
+ * Requires the user to be signed-in.
1298
+ */
1299
+ async reauthenticate(): Promise<AuthResponse> {
1300
+ await this.initializePromise
1301
+
1302
+ return await this._acquireLock(-1, async () => {
1303
+ return await this._reauthenticate()
1304
+ })
1305
+ }
1306
+
1307
+ private async _reauthenticate(): Promise<AuthResponse> {
1308
+ try {
1309
+ return await this._useSession(async (result) => {
1310
+ const {
1311
+ data: { session },
1312
+ error: sessionError,
1313
+ } = result
1314
+ if (sessionError) throw sessionError
1315
+ if (!session) throw new AuthSessionMissingError()
1316
+
1317
+ const { error } = await _request(this.fetch, 'GET', `${this.url}/reauthenticate`, {
1318
+ headers: this.headers,
1319
+ jwt: session.access_token,
1320
+ })
1321
+ return { data: { user: null, session: null }, error }
1322
+ })
1323
+ } catch (error) {
1324
+ if (isAuthError(error)) {
1325
+ return { data: { user: null, session: null }, error }
1326
+ }
1327
+ throw error
1328
+ }
1329
+ }
1330
+
1331
+ /**
1332
+ * Resends an existing signup confirmation email, email change email, SMS OTP or phone change OTP.
1333
+ */
1334
+ async resend(credentials: ResendParams): Promise<AuthOtpResponse> {
1335
+ try {
1336
+ const endpoint = `${this.url}/resend`
1337
+ if ('email' in credentials) {
1338
+ const { email, type, options } = credentials
1339
+ const { error } = await _request(this.fetch, 'POST', endpoint, {
1340
+ headers: this.headers,
1341
+ body: {
1342
+ email,
1343
+ type,
1344
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
1345
+ },
1346
+ redirectTo: options?.emailRedirectTo,
1347
+ })
1348
+ return { data: { user: null, session: null }, error }
1349
+ } else if ('phone' in credentials) {
1350
+ const { phone, type, options } = credentials
1351
+ const { data, error } = await _request(this.fetch, 'POST', endpoint, {
1352
+ headers: this.headers,
1353
+ body: {
1354
+ phone,
1355
+ type,
1356
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
1357
+ },
1358
+ })
1359
+ return { data: { user: null, session: null, messageId: data?.message_id }, error }
1360
+ }
1361
+ throw new AuthInvalidCredentialsError(
1362
+ 'You must provide either an email or phone number and a type'
1363
+ )
1364
+ } catch (error) {
1365
+ if (isAuthError(error)) {
1366
+ return { data: { user: null, session: null }, error }
1367
+ }
1368
+ throw error
1369
+ }
1370
+ }
1371
+
1372
+ /**
1373
+ * Returns the session, refreshing it if necessary.
1374
+ *
1375
+ * 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.
1376
+ *
1377
+ * **IMPORTANT:** This method loads values directly from the storage attached
1378
+ * to the client. If that storage is based on request cookies for example,
1379
+ * the values in it may not be authentic and therefore it's strongly advised
1380
+ * against using this method and its results in such circumstances. A warning
1381
+ * will be emitted if this is detected. Use {@link #getUser()} instead.
1382
+ */
1383
+ async getSession() {
1384
+ await this.initializePromise
1385
+
1386
+ const result = await this._acquireLock(-1, async () => {
1387
+ return this._useSession(async (result) => {
1388
+ return result
1389
+ })
1390
+ })
1391
+
1392
+ return result
1393
+ }
1394
+
1395
+ /**
1396
+ * Acquires a global lock based on the storage key.
1397
+ */
1398
+ private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
1399
+ this._debug('#_acquireLock', 'begin', acquireTimeout)
1400
+
1401
+ try {
1402
+ if (this.lockAcquired) {
1403
+ const last = this.pendingInLock.length
1404
+ ? this.pendingInLock[this.pendingInLock.length - 1]
1405
+ : Promise.resolve()
1406
+
1407
+ const result = (async () => {
1408
+ await last
1409
+ return await fn()
1410
+ })()
1411
+
1412
+ this.pendingInLock.push(
1413
+ (async () => {
1414
+ try {
1415
+ await result
1416
+ } catch (e: any) {
1417
+ // we just care if it finished
1418
+ }
1419
+ })()
1420
+ )
1421
+
1422
+ return result
1423
+ }
1424
+
1425
+ return await this.lock(`lock:${this.storageKey}`, acquireTimeout, async () => {
1426
+ this._debug('#_acquireLock', 'lock acquired for storage key', this.storageKey)
1427
+
1428
+ try {
1429
+ this.lockAcquired = true
1430
+
1431
+ const result = fn()
1432
+
1433
+ this.pendingInLock.push(
1434
+ (async () => {
1435
+ try {
1436
+ await result
1437
+ } catch (e: any) {
1438
+ // we just care if it finished
1439
+ }
1440
+ })()
1441
+ )
1442
+
1443
+ await result
1444
+
1445
+ // keep draining the queue until there's nothing to wait on
1446
+ while (this.pendingInLock.length) {
1447
+ const waitOn = [...this.pendingInLock]
1448
+
1449
+ await Promise.all(waitOn)
1450
+
1451
+ this.pendingInLock.splice(0, waitOn.length)
1452
+ }
1453
+
1454
+ return await result
1455
+ } finally {
1456
+ this._debug('#_acquireLock', 'lock released for storage key', this.storageKey)
1457
+
1458
+ this.lockAcquired = false
1459
+ }
1460
+ })
1461
+ } finally {
1462
+ this._debug('#_acquireLock', 'end')
1463
+ }
1464
+ }
1465
+
1466
+ /**
1467
+ * Use instead of {@link #getSession} inside the library. It is
1468
+ * semantically usually what you want, as getting a session involves some
1469
+ * processing afterwards that requires only one client operating on the
1470
+ * session at once across multiple tabs or processes.
1471
+ */
1472
+ private async _useSession<R>(
1473
+ fn: (
1474
+ result:
1475
+ | {
1476
+ data: {
1477
+ session: Session
1478
+ }
1479
+ error: null
1480
+ }
1481
+ | {
1482
+ data: {
1483
+ session: null
1484
+ }
1485
+ error: AuthError
1486
+ }
1487
+ | {
1488
+ data: {
1489
+ session: null
1490
+ }
1491
+ error: null
1492
+ }
1493
+ ) => Promise<R>
1494
+ ): Promise<R> {
1495
+ this._debug('#_useSession', 'begin')
1496
+
1497
+ try {
1498
+ // the use of __loadSession here is the only correct use of the function!
1499
+ const result = await this.__loadSession()
1500
+
1501
+ return await fn(result)
1502
+ } finally {
1503
+ this._debug('#_useSession', 'end')
1504
+ }
1505
+ }
1506
+
1507
+ /**
1508
+ * NEVER USE DIRECTLY!
1509
+ *
1510
+ * Always use {@link #_useSession}.
1511
+ */
1512
+ private async __loadSession(): Promise<
1513
+ | {
1514
+ data: {
1515
+ session: Session
1516
+ }
1517
+ error: null
1518
+ }
1519
+ | {
1520
+ data: {
1521
+ session: null
1522
+ }
1523
+ error: AuthError
1524
+ }
1525
+ | {
1526
+ data: {
1527
+ session: null
1528
+ }
1529
+ error: null
1530
+ }
1531
+ > {
1532
+ this._debug('#__loadSession()', 'begin')
1533
+
1534
+ if (!this.lockAcquired) {
1535
+ this._debug('#__loadSession()', 'used outside of an acquired lock!', new Error().stack)
1536
+ }
1537
+
1538
+ try {
1539
+ let currentSession: Session | null = null
1540
+
1541
+ const maybeSession = await getItemAsync(this.storage, this.storageKey)
1542
+
1543
+ this._debug('#getSession()', 'session from storage', maybeSession)
1544
+
1545
+ if (maybeSession !== null) {
1546
+ if (this._isValidSession(maybeSession)) {
1547
+ currentSession = maybeSession
1548
+ } else {
1549
+ this._debug('#getSession()', 'session from storage is not valid')
1550
+ await this._removeSession()
1551
+ }
1552
+ }
1553
+
1554
+ if (!currentSession) {
1555
+ return { data: { session: null }, error: null }
1556
+ }
1557
+
1558
+ // A session is considered expired before the access token _actually_
1559
+ // expires. When the autoRefreshToken option is off (or when the tab is
1560
+ // in the background), very eager users of getSession() -- like
1561
+ // realtime-js -- might send a valid JWT which will expire by the time it
1562
+ // reaches the server.
1563
+ const hasExpired = currentSession.expires_at
1564
+ ? currentSession.expires_at * 1000 - Date.now() < EXPIRY_MARGIN_MS
1565
+ : false
1566
+
1567
+ this._debug(
1568
+ '#__loadSession()',
1569
+ `session has${hasExpired ? '' : ' not'} expired`,
1570
+ 'expires_at',
1571
+ currentSession.expires_at
1572
+ )
1573
+
1574
+ if (!hasExpired) {
1575
+ if (this.userStorage) {
1576
+ const maybeUser: { user?: User | null } | null = (await getItemAsync(
1577
+ this.userStorage,
1578
+ this.storageKey + '-user'
1579
+ )) as any
1580
+
1581
+ if (maybeUser?.user) {
1582
+ currentSession.user = maybeUser.user
1583
+ } else {
1584
+ currentSession.user = userNotAvailableProxy()
1585
+ }
1586
+ }
1587
+
1588
+ if (this.storage.isServer && currentSession.user) {
1589
+ let suppressWarning = this.suppressGetSessionWarning
1590
+ const proxySession: Session = new Proxy(currentSession, {
1591
+ get: (target: any, prop: string, receiver: any) => {
1592
+ if (!suppressWarning && prop === 'user') {
1593
+ // only show warning when the user object is being accessed from the server
1594
+ console.warn(
1595
+ 'Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.'
1596
+ )
1597
+ suppressWarning = true // keeps this proxy instance from logging additional warnings
1598
+ this.suppressGetSessionWarning = true // keeps this client's future proxy instances from warning
1599
+ }
1600
+ return Reflect.get(target, prop, receiver)
1601
+ },
1602
+ })
1603
+ currentSession = proxySession
1604
+ }
1605
+
1606
+ return { data: { session: currentSession }, error: null }
1607
+ }
1608
+
1609
+ const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
1610
+ if (error) {
1611
+ return { data: { session: null }, error }
1612
+ }
1613
+
1614
+ return { data: { session }, error: null }
1615
+ } finally {
1616
+ this._debug('#__loadSession()', 'end')
1617
+ }
1618
+ }
1619
+
1620
+ /**
1621
+ * Gets the current user details if there is an existing session. This method
1622
+ * performs a network request to the Supabase Auth server, so the returned
1623
+ * value is authentic and can be used to base authorization rules on.
1624
+ *
1625
+ * @param jwt Takes in an optional access token JWT. If no JWT is provided, the JWT from the current session is used.
1626
+ */
1627
+ async getUser(jwt?: string): Promise<UserResponse> {
1628
+ if (jwt) {
1629
+ return await this._getUser(jwt)
1630
+ }
1631
+
1632
+ await this.initializePromise
1633
+
1634
+ const result = await this._acquireLock(-1, async () => {
1635
+ return await this._getUser()
1636
+ })
1637
+
1638
+ return result
1639
+ }
1640
+
1641
+ private async _getUser(jwt?: string): Promise<UserResponse> {
1642
+ try {
1643
+ if (jwt) {
1644
+ return await _request(this.fetch, 'GET', `${this.url}/user`, {
1645
+ headers: this.headers,
1646
+ jwt: jwt,
1647
+ xform: _userResponse,
1648
+ })
1649
+ }
1650
+
1651
+ return await this._useSession(async (result) => {
1652
+ const { data, error } = result
1653
+ if (error) {
1654
+ throw error
1655
+ }
1656
+
1657
+ // returns an error if there is no access_token or custom authorization header
1658
+ if (!data.session?.access_token && !this.hasCustomAuthorizationHeader) {
1659
+ return { data: { user: null }, error: new AuthSessionMissingError() }
1660
+ }
1661
+
1662
+ return await _request(this.fetch, 'GET', `${this.url}/user`, {
1663
+ headers: this.headers,
1664
+ jwt: data.session?.access_token ?? undefined,
1665
+ xform: _userResponse,
1666
+ })
1667
+ })
1668
+ } catch (error) {
1669
+ if (isAuthError(error)) {
1670
+ if (isAuthSessionMissingError(error)) {
1671
+ // JWT contains a `session_id` which does not correspond to an active
1672
+ // session in the database, indicating the user is signed out.
1673
+
1674
+ await this._removeSession()
1675
+ await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
1676
+ }
1677
+
1678
+ return { data: { user: null }, error }
1679
+ }
1680
+
1681
+ throw error
1682
+ }
1683
+ }
1684
+
1685
+ /**
1686
+ * Updates user data for a logged in user.
1687
+ */
1688
+ async updateUser(
1689
+ attributes: UserAttributes,
1690
+ options: {
1691
+ emailRedirectTo?: string | undefined
1692
+ } = {}
1693
+ ): Promise<UserResponse> {
1694
+ await this.initializePromise
1695
+
1696
+ return await this._acquireLock(-1, async () => {
1697
+ return await this._updateUser(attributes, options)
1698
+ })
1699
+ }
1700
+
1701
+ protected async _updateUser(
1702
+ attributes: UserAttributes,
1703
+ options: {
1704
+ emailRedirectTo?: string | undefined
1705
+ } = {}
1706
+ ): Promise<UserResponse> {
1707
+ try {
1708
+ return await this._useSession(async (result) => {
1709
+ const { data: sessionData, error: sessionError } = result
1710
+ if (sessionError) {
1711
+ throw sessionError
1712
+ }
1713
+ if (!sessionData.session) {
1714
+ throw new AuthSessionMissingError()
1715
+ }
1716
+ const session: Session = sessionData.session
1717
+ let codeChallenge: string | null = null
1718
+ let codeChallengeMethod: string | null = null
1719
+ if (this.flowType === 'pkce' && attributes.email != null) {
1720
+ ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
1721
+ this.storage,
1722
+ this.storageKey
1723
+ )
1724
+ }
1725
+
1726
+ const { data, error: userError } = await _request(this.fetch, 'PUT', `${this.url}/user`, {
1727
+ headers: this.headers,
1728
+ redirectTo: options?.emailRedirectTo,
1729
+ body: {
1730
+ ...attributes,
1731
+ code_challenge: codeChallenge,
1732
+ code_challenge_method: codeChallengeMethod,
1733
+ },
1734
+ jwt: session.access_token,
1735
+ xform: _userResponse,
1736
+ })
1737
+ if (userError) throw userError
1738
+ session.user = data.user as User
1739
+ await this._saveSession(session)
1740
+ await this._notifyAllSubscribers('USER_UPDATED', session)
1741
+ return { data: { user: session.user }, error: null }
1742
+ })
1743
+ } catch (error) {
1744
+ if (isAuthError(error)) {
1745
+ return { data: { user: null }, error }
1746
+ }
1747
+
1748
+ throw error
1749
+ }
1750
+ }
1751
+
1752
+ /**
1753
+ * 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.
1754
+ * If the refresh token or access token in the current session is invalid, an error will be thrown.
1755
+ * @param currentSession The current session that minimally contains an access token and refresh token.
1756
+ */
1757
+ async setSession(currentSession: {
1758
+ access_token: string
1759
+ refresh_token: string
1760
+ }): Promise<AuthResponse> {
1761
+ await this.initializePromise
1762
+
1763
+ return await this._acquireLock(-1, async () => {
1764
+ return await this._setSession(currentSession)
1765
+ })
1766
+ }
1767
+
1768
+ protected async _setSession(currentSession: {
1769
+ access_token: string
1770
+ refresh_token: string
1771
+ }): Promise<AuthResponse> {
1772
+ try {
1773
+ if (!currentSession.access_token || !currentSession.refresh_token) {
1774
+ throw new AuthSessionMissingError()
1775
+ }
1776
+
1777
+ const timeNow = Date.now() / 1000
1778
+ let expiresAt = timeNow
1779
+ let hasExpired = true
1780
+ let session: Session | null = null
1781
+ const { payload } = decodeJWT(currentSession.access_token)
1782
+ if (payload.exp) {
1783
+ expiresAt = payload.exp
1784
+ hasExpired = expiresAt <= timeNow
1785
+ }
1786
+
1787
+ if (hasExpired) {
1788
+ const { data: refreshedSession, error } = await this._callRefreshToken(
1789
+ currentSession.refresh_token
1790
+ )
1791
+ if (error) {
1792
+ return { data: { user: null, session: null }, error: error }
1793
+ }
1794
+
1795
+ if (!refreshedSession) {
1796
+ return { data: { user: null, session: null }, error: null }
1797
+ }
1798
+ session = refreshedSession
1799
+ } else {
1800
+ const { data, error } = await this._getUser(currentSession.access_token)
1801
+ if (error) {
1802
+ throw error
1803
+ }
1804
+ session = {
1805
+ access_token: currentSession.access_token,
1806
+ refresh_token: currentSession.refresh_token,
1807
+ user: data.user,
1808
+ token_type: 'bearer',
1809
+ expires_in: expiresAt - timeNow,
1810
+ expires_at: expiresAt,
1811
+ }
1812
+ await this._saveSession(session)
1813
+ await this._notifyAllSubscribers('SIGNED_IN', session)
1814
+ }
1815
+
1816
+ return { data: { user: session.user, session }, error: null }
1817
+ } catch (error) {
1818
+ if (isAuthError(error)) {
1819
+ return { data: { session: null, user: null }, error }
1820
+ }
1821
+
1822
+ throw error
1823
+ }
1824
+ }
1825
+
1826
+ /**
1827
+ * Returns a new session, regardless of expiry status.
1828
+ * Takes in an optional current session. If not passed in, then refreshSession() will attempt to retrieve it from getSession().
1829
+ * If the current session's refresh token is invalid, an error will be thrown.
1830
+ * @param currentSession The current session. If passed in, it must contain a refresh token.
1831
+ */
1832
+ async refreshSession(currentSession?: { refresh_token: string }): Promise<AuthResponse> {
1833
+ await this.initializePromise
1834
+
1835
+ return await this._acquireLock(-1, async () => {
1836
+ return await this._refreshSession(currentSession)
1837
+ })
1838
+ }
1839
+
1840
+ protected async _refreshSession(currentSession?: {
1841
+ refresh_token: string
1842
+ }): Promise<AuthResponse> {
1843
+ try {
1844
+ return await this._useSession(async (result) => {
1845
+ if (!currentSession) {
1846
+ const { data, error } = result
1847
+ if (error) {
1848
+ throw error
1849
+ }
1850
+
1851
+ currentSession = data.session ?? undefined
1852
+ }
1853
+
1854
+ if (!currentSession?.refresh_token) {
1855
+ throw new AuthSessionMissingError()
1856
+ }
1857
+
1858
+ const { data: session, error } = await this._callRefreshToken(currentSession.refresh_token)
1859
+ if (error) {
1860
+ return { data: { user: null, session: null }, error: error }
1861
+ }
1862
+
1863
+ if (!session) {
1864
+ return { data: { user: null, session: null }, error: null }
1865
+ }
1866
+
1867
+ return { data: { user: session.user, session }, error: null }
1868
+ })
1869
+ } catch (error) {
1870
+ if (isAuthError(error)) {
1871
+ return { data: { user: null, session: null }, error }
1872
+ }
1873
+
1874
+ throw error
1875
+ }
1876
+ }
1877
+
1878
+ /**
1879
+ * Gets the session data from a URL string
1880
+ */
1881
+ private async _getSessionFromURL(
1882
+ params: { [parameter: string]: string },
1883
+ callbackUrlType: string
1884
+ ): Promise<
1885
+ | {
1886
+ data: { session: Session; redirectType: string | null }
1887
+ error: null
1888
+ }
1889
+ | { data: { session: null; redirectType: null }; error: AuthError }
1890
+ > {
1891
+ try {
1892
+ if (!isBrowser()) throw new AuthImplicitGrantRedirectError('No browser detected.')
1893
+
1894
+ // If there's an error in the URL, it doesn't matter what flow it is, we just return the error.
1895
+ if (params.error || params.error_description || params.error_code) {
1896
+ // The error class returned implies that the redirect is from an implicit grant flow
1897
+ // but it could also be from a redirect error from a PKCE flow.
1898
+ throw new AuthImplicitGrantRedirectError(
1899
+ params.error_description || 'Error in URL with unspecified error_description',
1900
+ {
1901
+ error: params.error || 'unspecified_error',
1902
+ code: params.error_code || 'unspecified_code',
1903
+ }
1904
+ )
1905
+ }
1906
+
1907
+ // Checks for mismatches between the flowType initialised in the client and the URL parameters
1908
+ switch (callbackUrlType) {
1909
+ case 'implicit':
1910
+ if (this.flowType === 'pkce') {
1911
+ throw new AuthPKCEGrantCodeExchangeError('Not a valid PKCE flow url.')
1912
+ }
1913
+ break
1914
+ case 'pkce':
1915
+ if (this.flowType === 'implicit') {
1916
+ throw new AuthImplicitGrantRedirectError('Not a valid implicit grant flow url.')
1917
+ }
1918
+ break
1919
+ default:
1920
+ // there's no mismatch so we continue
1921
+ }
1922
+
1923
+ // Since this is a redirect for PKCE, we attempt to retrieve the code from the URL for the code exchange
1924
+ if (callbackUrlType === 'pkce') {
1925
+ this._debug('#_initialize()', 'begin', 'is PKCE flow', true)
1926
+ if (!params.code) throw new AuthPKCEGrantCodeExchangeError('No code detected.')
1927
+ const { data, error } = await this._exchangeCodeForSession(params.code)
1928
+ if (error) throw error
1929
+
1930
+ const url = new URL(window.location.href)
1931
+ url.searchParams.delete('code')
1932
+
1933
+ window.history.replaceState(window.history.state, '', url.toString())
1934
+
1935
+ return { data: { session: data.session, redirectType: null }, error: null }
1936
+ }
1937
+
1938
+ const {
1939
+ provider_token,
1940
+ provider_refresh_token,
1941
+ access_token,
1942
+ refresh_token,
1943
+ expires_in,
1944
+ expires_at,
1945
+ token_type,
1946
+ } = params
1947
+
1948
+ if (!access_token || !expires_in || !refresh_token || !token_type) {
1949
+ throw new AuthImplicitGrantRedirectError('No session defined in URL')
1950
+ }
1951
+
1952
+ const timeNow = Math.round(Date.now() / 1000)
1953
+ const expiresIn = parseInt(expires_in)
1954
+ let expiresAt = timeNow + expiresIn
1955
+
1956
+ if (expires_at) {
1957
+ expiresAt = parseInt(expires_at)
1958
+ }
1959
+
1960
+ const actuallyExpiresIn = expiresAt - timeNow
1961
+ if (actuallyExpiresIn * 1000 <= AUTO_REFRESH_TICK_DURATION_MS) {
1962
+ console.warn(
1963
+ `@supabase/gotrue-js: Session as retrieved from URL expires in ${actuallyExpiresIn}s, should have been closer to ${expiresIn}s`
1964
+ )
1965
+ }
1966
+
1967
+ const issuedAt = expiresAt - expiresIn
1968
+ if (timeNow - issuedAt >= 120) {
1969
+ console.warn(
1970
+ '@supabase/gotrue-js: Session as retrieved from URL was issued over 120s ago, URL could be stale',
1971
+ issuedAt,
1972
+ expiresAt,
1973
+ timeNow
1974
+ )
1975
+ } else if (timeNow - issuedAt < 0) {
1976
+ console.warn(
1977
+ '@supabase/gotrue-js: Session as retrieved from URL was issued in the future? Check the device clock for skew',
1978
+ issuedAt,
1979
+ expiresAt,
1980
+ timeNow
1981
+ )
1982
+ }
1983
+
1984
+ const { data, error } = await this._getUser(access_token)
1985
+ if (error) throw error
1986
+
1987
+ const session: Session = {
1988
+ provider_token,
1989
+ provider_refresh_token,
1990
+ access_token,
1991
+ expires_in: expiresIn,
1992
+ expires_at: expiresAt,
1993
+ refresh_token,
1994
+ token_type: token_type as 'bearer',
1995
+ user: data.user,
1996
+ }
1997
+
1998
+ // Remove tokens from URL
1999
+ window.location.hash = ''
2000
+ this._debug('#_getSessionFromURL()', 'clearing window.location.hash')
2001
+
2002
+ return { data: { session, redirectType: params.type }, error: null }
2003
+ } catch (error) {
2004
+ if (isAuthError(error)) {
2005
+ return { data: { session: null, redirectType: null }, error }
2006
+ }
2007
+
2008
+ throw error
2009
+ }
2010
+ }
2011
+
2012
+ /**
2013
+ * 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)
2014
+ */
2015
+ private _isImplicitGrantCallback(params: { [parameter: string]: string }): boolean {
2016
+ return Boolean(params.access_token || params.error_description)
2017
+ }
2018
+
2019
+ /**
2020
+ * Checks if the current URL and backing storage contain parameters given by a PKCE flow
2021
+ */
2022
+ private async _isPKCECallback(params: { [parameter: string]: string }): Promise<boolean> {
2023
+ const currentStorageContent = await getItemAsync(
2024
+ this.storage,
2025
+ `${this.storageKey}-code-verifier`
2026
+ )
2027
+
2028
+ return !!(params.code && currentStorageContent)
2029
+ }
2030
+
2031
+ /**
2032
+ * 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.
2033
+ *
2034
+ * 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)`.
2035
+ * 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.
2036
+ *
2037
+ * If using `others` scope, no `SIGNED_OUT` event is fired!
2038
+ */
2039
+ async signOut(options: SignOut = { scope: 'global' }): Promise<{ error: AuthError | null }> {
2040
+ await this.initializePromise
2041
+
2042
+ return await this._acquireLock(-1, async () => {
2043
+ return await this._signOut(options)
2044
+ })
2045
+ }
2046
+
2047
+ protected async _signOut(
2048
+ { scope }: SignOut = { scope: 'global' }
2049
+ ): Promise<{ error: AuthError | null }> {
2050
+ return await this._useSession(async (result) => {
2051
+ const { data, error: sessionError } = result
2052
+ if (sessionError) {
2053
+ return { error: sessionError }
2054
+ }
2055
+ const accessToken = data.session?.access_token
2056
+ if (accessToken) {
2057
+ const { error } = await this.admin.signOut(accessToken, scope)
2058
+ if (error) {
2059
+ // ignore 404s since user might not exist anymore
2060
+ // ignore 401s since an invalid or expired JWT should sign out the current session
2061
+ if (
2062
+ !(
2063
+ isAuthApiError(error) &&
2064
+ (error.status === 404 || error.status === 401 || error.status === 403)
2065
+ )
2066
+ ) {
2067
+ return { error }
2068
+ }
2069
+ }
2070
+ }
2071
+ if (scope !== 'others') {
2072
+ await this._removeSession()
2073
+ await removeItemAsync(this.storage, `${this.storageKey}-code-verifier`)
2074
+ }
2075
+ return { error: null }
2076
+ })
2077
+ }
2078
+
2079
+ /**
2080
+ * Receive a notification every time an auth event happens.
2081
+ * @param callback A callback function to be invoked when an auth event happens.
2082
+ */
2083
+ onAuthStateChange(
2084
+ callback: (event: AuthChangeEvent, session: Session | null) => void | Promise<void>
2085
+ ): {
2086
+ data: { subscription: Subscription }
2087
+ } {
2088
+ const id: string = uuid()
2089
+ const subscription: Subscription = {
2090
+ id,
2091
+ callback,
2092
+ unsubscribe: () => {
2093
+ this._debug('#unsubscribe()', 'state change callback with id removed', id)
2094
+
2095
+ this.stateChangeEmitters.delete(id)
2096
+ },
2097
+ }
2098
+
2099
+ this._debug('#onAuthStateChange()', 'registered callback with id', id)
2100
+
2101
+ this.stateChangeEmitters.set(id, subscription)
2102
+ ;(async () => {
2103
+ await this.initializePromise
2104
+
2105
+ await this._acquireLock(-1, async () => {
2106
+ this._emitInitialSession(id)
2107
+ })
2108
+ })()
2109
+
2110
+ return { data: { subscription } }
2111
+ }
2112
+
2113
+ private async _emitInitialSession(id: string): Promise<void> {
2114
+ return await this._useSession(async (result) => {
2115
+ try {
2116
+ const {
2117
+ data: { session },
2118
+ error,
2119
+ } = result
2120
+ if (error) throw error
2121
+
2122
+ await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', session)
2123
+ this._debug('INITIAL_SESSION', 'callback id', id, 'session', session)
2124
+ } catch (err) {
2125
+ await this.stateChangeEmitters.get(id)?.callback('INITIAL_SESSION', null)
2126
+ this._debug('INITIAL_SESSION', 'callback id', id, 'error', err)
2127
+ console.error(err)
2128
+ }
2129
+ })
2130
+ }
2131
+
2132
+ /**
2133
+ * Sends a password reset request to an email address. This method supports the PKCE flow.
2134
+ *
2135
+ * @param email The email address of the user.
2136
+ * @param options.redirectTo The URL to send the user to after they click the password reset link.
2137
+ * @param options.captchaToken Verification token received when the user completes the captcha on the site.
2138
+ */
2139
+ async resetPasswordForEmail(
2140
+ email: string,
2141
+ options: {
2142
+ redirectTo?: string
2143
+ captchaToken?: string
2144
+ } = {}
2145
+ ): Promise<
2146
+ | {
2147
+ data: {}
2148
+ error: null
2149
+ }
2150
+ | { data: null; error: AuthError }
2151
+ > {
2152
+ let codeChallenge: string | null = null
2153
+ let codeChallengeMethod: string | null = null
2154
+
2155
+ if (this.flowType === 'pkce') {
2156
+ ;[codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2157
+ this.storage,
2158
+ this.storageKey,
2159
+ true // isPasswordRecovery
2160
+ )
2161
+ }
2162
+ try {
2163
+ return await _request(this.fetch, 'POST', `${this.url}/recover`, {
2164
+ body: {
2165
+ email,
2166
+ code_challenge: codeChallenge,
2167
+ code_challenge_method: codeChallengeMethod,
2168
+ gotrue_meta_security: { captcha_token: options.captchaToken },
2169
+ },
2170
+ headers: this.headers,
2171
+ redirectTo: options.redirectTo,
2172
+ })
2173
+ } catch (error) {
2174
+ if (isAuthError(error)) {
2175
+ return { data: null, error }
2176
+ }
2177
+
2178
+ throw error
2179
+ }
2180
+ }
2181
+
2182
+ /**
2183
+ * Gets all the identities linked to a user.
2184
+ */
2185
+ async getUserIdentities(): Promise<
2186
+ | {
2187
+ data: {
2188
+ identities: UserIdentity[]
2189
+ }
2190
+ error: null
2191
+ }
2192
+ | { data: null; error: AuthError }
2193
+ > {
2194
+ try {
2195
+ const { data, error } = await this.getUser()
2196
+ if (error) throw error
2197
+ return { data: { identities: data.user.identities ?? [] }, error: null }
2198
+ } catch (error) {
2199
+ if (isAuthError(error)) {
2200
+ return { data: null, error }
2201
+ }
2202
+ throw error
2203
+ }
2204
+ }
2205
+
2206
+ /**
2207
+ * Links an oauth identity to an existing user.
2208
+ * This method supports the PKCE flow.
2209
+ */
2210
+ async linkIdentity(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse>
2211
+
2212
+ /**
2213
+ * Links an OIDC identity to an existing user.
2214
+ */
2215
+ async linkIdentity(credentials: SignInWithIdTokenCredentials): Promise<AuthTokenResponse>
2216
+
2217
+ async linkIdentity(credentials: any): Promise<any> {
2218
+ if ('token' in credentials) {
2219
+ return this.linkIdentityIdToken(credentials)
2220
+ }
2221
+
2222
+ return this.linkIdentityOAuth(credentials)
2223
+ }
2224
+
2225
+ private async linkIdentityOAuth(credentials: SignInWithOAuthCredentials): Promise<OAuthResponse> {
2226
+ try {
2227
+ const { data, error } = await this._useSession(async (result) => {
2228
+ const { data, error } = result
2229
+ if (error) throw error
2230
+ const url: string = await this._getUrlForProvider(
2231
+ `${this.url}/user/identities/authorize`,
2232
+ credentials.provider,
2233
+ {
2234
+ redirectTo: credentials.options?.redirectTo,
2235
+ scopes: credentials.options?.scopes,
2236
+ queryParams: credentials.options?.queryParams,
2237
+ skipBrowserRedirect: true,
2238
+ }
2239
+ )
2240
+ return await _request(this.fetch, 'GET', url, {
2241
+ headers: this.headers,
2242
+ jwt: data.session?.access_token ?? undefined,
2243
+ })
2244
+ })
2245
+ if (error) throw error
2246
+ if (isBrowser() && !credentials.options?.skipBrowserRedirect) {
2247
+ window.location.assign(data?.url)
2248
+ }
2249
+ return { data: { provider: credentials.provider, url: data?.url }, error: null }
2250
+ } catch (error) {
2251
+ if (isAuthError(error)) {
2252
+ return { data: { provider: credentials.provider, url: null }, error }
2253
+ }
2254
+ throw error
2255
+ }
2256
+ }
2257
+
2258
+ private async linkIdentityIdToken(
2259
+ credentials: SignInWithIdTokenCredentials
2260
+ ): Promise<AuthTokenResponse> {
2261
+ return await this._useSession(async (result) => {
2262
+ try {
2263
+ const {
2264
+ error: sessionError,
2265
+ data: { session },
2266
+ } = result
2267
+ if (sessionError) throw sessionError
2268
+
2269
+ const { options, provider, token, access_token, nonce } = credentials
2270
+
2271
+ const res = await _request(this.fetch, 'POST', `${this.url}/token?grant_type=id_token`, {
2272
+ headers: this.headers,
2273
+ jwt: session?.access_token ?? undefined,
2274
+ body: {
2275
+ provider,
2276
+ id_token: token,
2277
+ access_token,
2278
+ nonce,
2279
+ link_identity: true,
2280
+ gotrue_meta_security: { captcha_token: options?.captchaToken },
2281
+ },
2282
+ xform: _sessionResponse,
2283
+ })
2284
+
2285
+ const { data, error } = res
2286
+ if (error) {
2287
+ return { data: { user: null, session: null }, error }
2288
+ } else if (!data || !data.session || !data.user) {
2289
+ return {
2290
+ data: { user: null, session: null },
2291
+ error: new AuthInvalidTokenResponseError(),
2292
+ }
2293
+ }
2294
+ if (data.session) {
2295
+ await this._saveSession(data.session)
2296
+ await this._notifyAllSubscribers('USER_UPDATED', data.session)
2297
+ }
2298
+ return { data, error }
2299
+ } catch (error) {
2300
+ if (isAuthError(error)) {
2301
+ return { data: { user: null, session: null }, error }
2302
+ }
2303
+ throw error
2304
+ }
2305
+ })
2306
+ }
2307
+
2308
+ /**
2309
+ * 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.
2310
+ */
2311
+ async unlinkIdentity(identity: UserIdentity): Promise<
2312
+ | {
2313
+ data: {}
2314
+ error: null
2315
+ }
2316
+ | { data: null; error: AuthError }
2317
+ > {
2318
+ try {
2319
+ return await this._useSession(async (result) => {
2320
+ const { data, error } = result
2321
+ if (error) {
2322
+ throw error
2323
+ }
2324
+ return await _request(
2325
+ this.fetch,
2326
+ 'DELETE',
2327
+ `${this.url}/user/identities/${identity.identity_id}`,
2328
+ {
2329
+ headers: this.headers,
2330
+ jwt: data.session?.access_token ?? undefined,
2331
+ }
2332
+ )
2333
+ })
2334
+ } catch (error) {
2335
+ if (isAuthError(error)) {
2336
+ return { data: null, error }
2337
+ }
2338
+ throw error
2339
+ }
2340
+ }
2341
+
2342
+ /**
2343
+ * Generates a new JWT.
2344
+ * @param refreshToken A valid refresh token that was returned on login.
2345
+ */
2346
+ private async _refreshAccessToken(refreshToken: string): Promise<AuthResponse> {
2347
+ const debugName = `#_refreshAccessToken(${refreshToken.substring(0, 5)}...)`
2348
+ this._debug(debugName, 'begin')
2349
+
2350
+ try {
2351
+ const startedAt = Date.now()
2352
+
2353
+ // will attempt to refresh the token with exponential backoff
2354
+ return await retryable(
2355
+ async (attempt) => {
2356
+ if (attempt > 0) {
2357
+ await sleep(200 * Math.pow(2, attempt - 1)) // 200, 400, 800, ...
2358
+ }
2359
+
2360
+ this._debug(debugName, 'refreshing attempt', attempt)
2361
+
2362
+ return await _request(this.fetch, 'POST', `${this.url}/token?grant_type=refresh_token`, {
2363
+ body: { refresh_token: refreshToken },
2364
+ headers: this.headers,
2365
+ xform: _sessionResponse,
2366
+ })
2367
+ },
2368
+ (attempt, error) => {
2369
+ const nextBackOffInterval = 200 * Math.pow(2, attempt)
2370
+ return (
2371
+ error &&
2372
+ isAuthRetryableFetchError(error) &&
2373
+ // retryable only if the request can be sent before the backoff overflows the tick duration
2374
+ Date.now() + nextBackOffInterval - startedAt < AUTO_REFRESH_TICK_DURATION_MS
2375
+ )
2376
+ }
2377
+ )
2378
+ } catch (error) {
2379
+ this._debug(debugName, 'error', error)
2380
+
2381
+ if (isAuthError(error)) {
2382
+ return { data: { session: null, user: null }, error }
2383
+ }
2384
+ throw error
2385
+ } finally {
2386
+ this._debug(debugName, 'end')
2387
+ }
2388
+ }
2389
+
2390
+ private _isValidSession(maybeSession: unknown): maybeSession is Session {
2391
+ const isValidSession =
2392
+ typeof maybeSession === 'object' &&
2393
+ maybeSession !== null &&
2394
+ 'access_token' in maybeSession &&
2395
+ 'refresh_token' in maybeSession &&
2396
+ 'expires_at' in maybeSession
2397
+
2398
+ return isValidSession
2399
+ }
2400
+
2401
+ private async _handleProviderSignIn(
2402
+ provider: Provider,
2403
+ options: {
2404
+ redirectTo?: string
2405
+ scopes?: string
2406
+ queryParams?: { [key: string]: string }
2407
+ skipBrowserRedirect?: boolean
2408
+ }
2409
+ ) {
2410
+ const url: string = await this._getUrlForProvider(`${this.url}/authorize`, provider, {
2411
+ redirectTo: options.redirectTo,
2412
+ scopes: options.scopes,
2413
+ queryParams: options.queryParams,
2414
+ })
2415
+
2416
+ this._debug('#_handleProviderSignIn()', 'provider', provider, 'options', options, 'url', url)
2417
+
2418
+ // try to open on the browser
2419
+ if (isBrowser() && !options.skipBrowserRedirect) {
2420
+ window.location.assign(url)
2421
+ }
2422
+
2423
+ return { data: { provider, url }, error: null }
2424
+ }
2425
+
2426
+ /**
2427
+ * Recovers the session from LocalStorage and refreshes the token
2428
+ * Note: this method is async to accommodate for AsyncStorage e.g. in React native.
2429
+ */
2430
+ private async _recoverAndRefresh() {
2431
+ const debugName = '#_recoverAndRefresh()'
2432
+ this._debug(debugName, 'begin')
2433
+
2434
+ try {
2435
+ const currentSession = (await getItemAsync(this.storage, this.storageKey)) as Session | null
2436
+
2437
+ if (currentSession && this.userStorage) {
2438
+ let maybeUser: { user: User | null } | null = (await getItemAsync(
2439
+ this.userStorage,
2440
+ this.storageKey + '-user'
2441
+ )) as any
2442
+
2443
+ if (!this.storage.isServer && Object.is(this.storage, this.userStorage) && !maybeUser) {
2444
+ // storage and userStorage are the same storage medium, for example
2445
+ // window.localStorage if userStorage does not have the user from
2446
+ // storage stored, store it first thereby migrating the user object
2447
+ // from storage -> userStorage
2448
+
2449
+ maybeUser = { user: currentSession.user }
2450
+ await setItemAsync(this.userStorage, this.storageKey + '-user', maybeUser)
2451
+ }
2452
+
2453
+ currentSession.user = maybeUser?.user ?? userNotAvailableProxy()
2454
+ } else if (currentSession && !currentSession.user) {
2455
+ // user storage is not set, let's check if it was previously enabled so
2456
+ // we bring back the storage as it should be
2457
+
2458
+ if (!currentSession.user) {
2459
+ // test if userStorage was previously enabled and the storage medium was the same, to move the user back under the same key
2460
+ const separateUser: { user: User | null } | null = (await getItemAsync(
2461
+ this.storage,
2462
+ this.storageKey + '-user'
2463
+ )) as any
2464
+
2465
+ if (separateUser && separateUser?.user) {
2466
+ currentSession.user = separateUser.user
2467
+
2468
+ await removeItemAsync(this.storage, this.storageKey + '-user')
2469
+ await setItemAsync(this.storage, this.storageKey, currentSession)
2470
+ } else {
2471
+ currentSession.user = userNotAvailableProxy()
2472
+ }
2473
+ }
2474
+ }
2475
+
2476
+ this._debug(debugName, 'session from storage', currentSession)
2477
+
2478
+ if (!this._isValidSession(currentSession)) {
2479
+ this._debug(debugName, 'session is not valid')
2480
+ if (currentSession !== null) {
2481
+ await this._removeSession()
2482
+ }
2483
+
2484
+ return
2485
+ }
2486
+
2487
+ const expiresWithMargin =
2488
+ (currentSession.expires_at ?? Infinity) * 1000 - Date.now() < EXPIRY_MARGIN_MS
2489
+
2490
+ this._debug(
2491
+ debugName,
2492
+ `session has${expiresWithMargin ? '' : ' not'} expired with margin of ${EXPIRY_MARGIN_MS}s`
2493
+ )
2494
+
2495
+ if (expiresWithMargin) {
2496
+ if (this.autoRefreshToken && currentSession.refresh_token) {
2497
+ const { error } = await this._callRefreshToken(currentSession.refresh_token)
2498
+
2499
+ if (error) {
2500
+ console.error(error)
2501
+
2502
+ if (!isAuthRetryableFetchError(error)) {
2503
+ this._debug(
2504
+ debugName,
2505
+ 'refresh failed with a non-retryable error, removing the session',
2506
+ error
2507
+ )
2508
+ await this._removeSession()
2509
+ }
2510
+ }
2511
+ }
2512
+ } else if (
2513
+ currentSession.user &&
2514
+ (currentSession.user as any).__isUserNotAvailableProxy === true
2515
+ ) {
2516
+ // If we have a proxy user, try to get the real user data
2517
+ try {
2518
+ const { data, error: userError } = await this._getUser(currentSession.access_token)
2519
+
2520
+ if (!userError && data?.user) {
2521
+ currentSession.user = data.user
2522
+ await this._saveSession(currentSession)
2523
+ await this._notifyAllSubscribers('SIGNED_IN', currentSession)
2524
+ } else {
2525
+ this._debug(debugName, 'could not get user data, skipping SIGNED_IN notification')
2526
+ }
2527
+ } catch (getUserError) {
2528
+ console.error('Error getting user data:', getUserError)
2529
+ this._debug(
2530
+ debugName,
2531
+ 'error getting user data, skipping SIGNED_IN notification',
2532
+ getUserError
2533
+ )
2534
+ }
2535
+ } else {
2536
+ // no need to persist currentSession again, as we just loaded it from
2537
+ // local storage; persisting it again may overwrite a value saved by
2538
+ // another client with access to the same local storage
2539
+ await this._notifyAllSubscribers('SIGNED_IN', currentSession)
2540
+ }
2541
+ } catch (err) {
2542
+ this._debug(debugName, 'error', err)
2543
+
2544
+ console.error(err)
2545
+ return
2546
+ } finally {
2547
+ this._debug(debugName, 'end')
2548
+ }
2549
+ }
2550
+
2551
+ private async _callRefreshToken(refreshToken: string): Promise<CallRefreshTokenResult> {
2552
+ if (!refreshToken) {
2553
+ throw new AuthSessionMissingError()
2554
+ }
2555
+
2556
+ // refreshing is already in progress
2557
+ if (this.refreshingDeferred) {
2558
+ return this.refreshingDeferred.promise
2559
+ }
2560
+
2561
+ const debugName = `#_callRefreshToken(${refreshToken.substring(0, 5)}...)`
2562
+
2563
+ this._debug(debugName, 'begin')
2564
+
2565
+ try {
2566
+ this.refreshingDeferred = new Deferred<CallRefreshTokenResult>()
2567
+
2568
+ const { data, error } = await this._refreshAccessToken(refreshToken)
2569
+ if (error) throw error
2570
+ if (!data.session) throw new AuthSessionMissingError()
2571
+
2572
+ await this._saveSession(data.session)
2573
+ await this._notifyAllSubscribers('TOKEN_REFRESHED', data.session)
2574
+
2575
+ const result = { data: data.session, error: null }
2576
+
2577
+ this.refreshingDeferred.resolve(result)
2578
+
2579
+ return result
2580
+ } catch (error) {
2581
+ this._debug(debugName, 'error', error)
2582
+
2583
+ if (isAuthError(error)) {
2584
+ const result = { data: null, error }
2585
+
2586
+ if (!isAuthRetryableFetchError(error)) {
2587
+ await this._removeSession()
2588
+ }
2589
+
2590
+ this.refreshingDeferred?.resolve(result)
2591
+
2592
+ return result
2593
+ }
2594
+
2595
+ this.refreshingDeferred?.reject(error)
2596
+ throw error
2597
+ } finally {
2598
+ this.refreshingDeferred = null
2599
+ this._debug(debugName, 'end')
2600
+ }
2601
+ }
2602
+
2603
+ private async _notifyAllSubscribers(
2604
+ event: AuthChangeEvent,
2605
+ session: Session | null,
2606
+ broadcast = true
2607
+ ) {
2608
+ const debugName = `#_notifyAllSubscribers(${event})`
2609
+ this._debug(debugName, 'begin', session, `broadcast = ${broadcast}`)
2610
+
2611
+ try {
2612
+ if (this.broadcastChannel && broadcast) {
2613
+ this.broadcastChannel.postMessage({ event, session })
2614
+ }
2615
+
2616
+ const errors: any[] = []
2617
+ const promises = Array.from(this.stateChangeEmitters.values()).map(async (x) => {
2618
+ try {
2619
+ await x.callback(event, session)
2620
+ } catch (e: any) {
2621
+ errors.push(e)
2622
+ }
2623
+ })
2624
+
2625
+ await Promise.all(promises)
2626
+
2627
+ if (errors.length > 0) {
2628
+ for (let i = 0; i < errors.length; i += 1) {
2629
+ console.error(errors[i])
2630
+ }
2631
+
2632
+ throw errors[0]
2633
+ }
2634
+ } finally {
2635
+ this._debug(debugName, 'end')
2636
+ }
2637
+ }
2638
+
2639
+ /**
2640
+ * set currentSession and currentUser
2641
+ * process to _startAutoRefreshToken if possible
2642
+ */
2643
+ private async _saveSession(session: Session) {
2644
+ this._debug('#_saveSession()', session)
2645
+ // _saveSession is always called whenever a new session has been acquired
2646
+ // so we can safely suppress the warning returned by future getSession calls
2647
+ this.suppressGetSessionWarning = true
2648
+
2649
+ // Create a shallow copy to work with, to avoid mutating the original session object if it's used elsewhere
2650
+ const sessionToProcess = { ...session }
2651
+
2652
+ const userIsProxy =
2653
+ sessionToProcess.user && (sessionToProcess.user as any).__isUserNotAvailableProxy === true
2654
+ if (this.userStorage) {
2655
+ if (!userIsProxy && sessionToProcess.user) {
2656
+ // If it's a real user object, save it to userStorage.
2657
+ await setItemAsync(this.userStorage, this.storageKey + '-user', {
2658
+ user: sessionToProcess.user,
2659
+ })
2660
+ } else if (userIsProxy) {
2661
+ // If it's the proxy, it means user was not found in userStorage.
2662
+ // We should ensure no stale user data for this key exists in userStorage if we were to save null,
2663
+ // or simply not save the proxy. For now, we don't save the proxy here.
2664
+ // If there's a need to clear userStorage if user becomes proxy, that logic would go here.
2665
+ }
2666
+
2667
+ // Prepare the main session data for primary storage: remove the user property before cloning
2668
+ // This is important because the original session.user might be the proxy
2669
+ const mainSessionData: Omit<Session, 'user'> & { user?: User } = { ...sessionToProcess }
2670
+ delete mainSessionData.user // Remove user (real or proxy) before cloning for main storage
2671
+
2672
+ const clonedMainSessionData = deepClone(mainSessionData)
2673
+ await setItemAsync(this.storage, this.storageKey, clonedMainSessionData)
2674
+ } else {
2675
+ // No userStorage is configured.
2676
+ // In this case, session.user should ideally not be a proxy.
2677
+ // If it were, structuredClone would fail. This implies an issue elsewhere if user is a proxy here
2678
+ const clonedSession = deepClone(sessionToProcess) // sessionToProcess still has its original user property
2679
+ await setItemAsync(this.storage, this.storageKey, clonedSession)
2680
+ }
2681
+ }
2682
+
2683
+ private async _removeSession() {
2684
+ this._debug('#_removeSession()')
2685
+
2686
+ await removeItemAsync(this.storage, this.storageKey)
2687
+ await removeItemAsync(this.storage, this.storageKey + '-code-verifier')
2688
+ await removeItemAsync(this.storage, this.storageKey + '-user')
2689
+
2690
+ if (this.userStorage) {
2691
+ await removeItemAsync(this.userStorage, this.storageKey + '-user')
2692
+ }
2693
+
2694
+ await this._notifyAllSubscribers('SIGNED_OUT', null)
2695
+ }
2696
+
2697
+ /**
2698
+ * Removes any registered visibilitychange callback.
2699
+ *
2700
+ * {@see #startAutoRefresh}
2701
+ * {@see #stopAutoRefresh}
2702
+ */
2703
+ private _removeVisibilityChangedCallback() {
2704
+ this._debug('#_removeVisibilityChangedCallback()')
2705
+
2706
+ const callback = this.visibilityChangedCallback
2707
+ this.visibilityChangedCallback = null
2708
+
2709
+ try {
2710
+ if (callback && isBrowser() && window?.removeEventListener) {
2711
+ window.removeEventListener('visibilitychange', callback)
2712
+ }
2713
+ } catch (e) {
2714
+ console.error('removing visibilitychange callback failed', e)
2715
+ }
2716
+ }
2717
+
2718
+ /**
2719
+ * This is the private implementation of {@link #startAutoRefresh}. Use this
2720
+ * within the library.
2721
+ */
2722
+ private async _startAutoRefresh() {
2723
+ await this._stopAutoRefresh()
2724
+
2725
+ this._debug('#_startAutoRefresh()')
2726
+
2727
+ const ticker = setInterval(() => this._autoRefreshTokenTick(), AUTO_REFRESH_TICK_DURATION_MS)
2728
+ this.autoRefreshTicker = ticker
2729
+
2730
+ if (ticker && typeof ticker === 'object' && typeof ticker.unref === 'function') {
2731
+ // ticker is a NodeJS Timeout object that has an `unref` method
2732
+ // https://nodejs.org/api/timers.html#timeoutunref
2733
+ // When auto refresh is used in NodeJS (like for testing) the
2734
+ // `setInterval` is preventing the process from being marked as
2735
+ // finished and tests run endlessly. This can be prevented by calling
2736
+ // `unref()` on the returned object.
2737
+ ticker.unref()
2738
+ // @ts-expect-error TS has no context of Deno
2739
+ } else if (typeof Deno !== 'undefined' && typeof Deno.unrefTimer === 'function') {
2740
+ // similar like for NodeJS, but with the Deno API
2741
+ // https://deno.land/api@latest?unstable&s=Deno.unrefTimer
2742
+ // @ts-expect-error TS has no context of Deno
2743
+ Deno.unrefTimer(ticker)
2744
+ }
2745
+
2746
+ // run the tick immediately, but in the next pass of the event loop so that
2747
+ // #_initialize can be allowed to complete without recursively waiting on
2748
+ // itself
2749
+ setTimeout(async () => {
2750
+ await this.initializePromise
2751
+ await this._autoRefreshTokenTick()
2752
+ }, 0)
2753
+ }
2754
+
2755
+ /**
2756
+ * This is the private implementation of {@link #stopAutoRefresh}. Use this
2757
+ * within the library.
2758
+ */
2759
+ private async _stopAutoRefresh() {
2760
+ this._debug('#_stopAutoRefresh()')
2761
+
2762
+ const ticker = this.autoRefreshTicker
2763
+ this.autoRefreshTicker = null
2764
+
2765
+ if (ticker) {
2766
+ clearInterval(ticker)
2767
+ }
2768
+ }
2769
+
2770
+ /**
2771
+ * Starts an auto-refresh process in the background. The session is checked
2772
+ * every few seconds. Close to the time of expiration a process is started to
2773
+ * refresh the session. If refreshing fails it will be retried for as long as
2774
+ * necessary.
2775
+ *
2776
+ * If you set the {@link GoTrueClientOptions#autoRefreshToken} you don't need
2777
+ * to call this function, it will be called for you.
2778
+ *
2779
+ * On browsers the refresh process works only when the tab/window is in the
2780
+ * foreground to conserve resources as well as prevent race conditions and
2781
+ * flooding auth with requests. If you call this method any managed
2782
+ * visibility change callback will be removed and you must manage visibility
2783
+ * changes on your own.
2784
+ *
2785
+ * On non-browser platforms the refresh process works *continuously* in the
2786
+ * background, which may not be desirable. You should hook into your
2787
+ * platform's foreground indication mechanism and call these methods
2788
+ * appropriately to conserve resources.
2789
+ *
2790
+ * {@see #stopAutoRefresh}
2791
+ */
2792
+ async startAutoRefresh() {
2793
+ this._removeVisibilityChangedCallback()
2794
+ await this._startAutoRefresh()
2795
+ }
2796
+
2797
+ /**
2798
+ * Stops an active auto refresh process running in the background (if any).
2799
+ *
2800
+ * If you call this method any managed visibility change callback will be
2801
+ * removed and you must manage visibility changes on your own.
2802
+ *
2803
+ * See {@link #startAutoRefresh} for more details.
2804
+ */
2805
+ async stopAutoRefresh() {
2806
+ this._removeVisibilityChangedCallback()
2807
+ await this._stopAutoRefresh()
2808
+ }
2809
+
2810
+ /**
2811
+ * Runs the auto refresh token tick.
2812
+ */
2813
+ private async _autoRefreshTokenTick() {
2814
+ this._debug('#_autoRefreshTokenTick()', 'begin')
2815
+
2816
+ try {
2817
+ await this._acquireLock(0, async () => {
2818
+ try {
2819
+ const now = Date.now()
2820
+
2821
+ try {
2822
+ return await this._useSession(async (result) => {
2823
+ const {
2824
+ data: { session },
2825
+ } = result
2826
+
2827
+ if (!session || !session.refresh_token || !session.expires_at) {
2828
+ this._debug('#_autoRefreshTokenTick()', 'no session')
2829
+ return
2830
+ }
2831
+
2832
+ // session will expire in this many ticks (or has already expired if <= 0)
2833
+ const expiresInTicks = Math.floor(
2834
+ (session.expires_at * 1000 - now) / AUTO_REFRESH_TICK_DURATION_MS
2835
+ )
2836
+
2837
+ this._debug(
2838
+ '#_autoRefreshTokenTick()',
2839
+ `access token expires in ${expiresInTicks} ticks, a tick lasts ${AUTO_REFRESH_TICK_DURATION_MS}ms, refresh threshold is ${AUTO_REFRESH_TICK_THRESHOLD} ticks`
2840
+ )
2841
+
2842
+ if (expiresInTicks <= AUTO_REFRESH_TICK_THRESHOLD) {
2843
+ await this._callRefreshToken(session.refresh_token)
2844
+ }
2845
+ })
2846
+ } catch (e: any) {
2847
+ console.error(
2848
+ 'Auto refresh tick failed with error. This is likely a transient error.',
2849
+ e
2850
+ )
2851
+ }
2852
+ } finally {
2853
+ this._debug('#_autoRefreshTokenTick()', 'end')
2854
+ }
2855
+ })
2856
+ } catch (e: any) {
2857
+ if (e.isAcquireTimeout || e instanceof LockAcquireTimeoutError) {
2858
+ this._debug('auto refresh token tick lock not available')
2859
+ } else {
2860
+ throw e
2861
+ }
2862
+ }
2863
+ }
2864
+
2865
+ /**
2866
+ * Registers callbacks on the browser / platform, which in-turn run
2867
+ * algorithms when the browser window/tab are in foreground. On non-browser
2868
+ * platforms it assumes always foreground.
2869
+ */
2870
+ private async _handleVisibilityChange() {
2871
+ this._debug('#_handleVisibilityChange()')
2872
+
2873
+ if (!isBrowser() || !window?.addEventListener) {
2874
+ if (this.autoRefreshToken) {
2875
+ // in non-browser environments the refresh token ticker runs always
2876
+ this.startAutoRefresh()
2877
+ }
2878
+
2879
+ return false
2880
+ }
2881
+
2882
+ try {
2883
+ this.visibilityChangedCallback = async () => await this._onVisibilityChanged(false)
2884
+
2885
+ window?.addEventListener('visibilitychange', this.visibilityChangedCallback)
2886
+
2887
+ // now immediately call the visbility changed callback to setup with the
2888
+ // current visbility state
2889
+ await this._onVisibilityChanged(true) // initial call
2890
+ } catch (error) {
2891
+ console.error('_handleVisibilityChange', error)
2892
+ }
2893
+ }
2894
+
2895
+ /**
2896
+ * Callback registered with `window.addEventListener('visibilitychange')`.
2897
+ */
2898
+ private async _onVisibilityChanged(calledFromInitialize: boolean) {
2899
+ const methodName = `#_onVisibilityChanged(${calledFromInitialize})`
2900
+ this._debug(methodName, 'visibilityState', document.visibilityState)
2901
+
2902
+ if (document.visibilityState === 'visible') {
2903
+ if (this.autoRefreshToken) {
2904
+ // in browser environments the refresh token ticker runs only on focused tabs
2905
+ // which prevents race conditions
2906
+ this._startAutoRefresh()
2907
+ }
2908
+
2909
+ if (!calledFromInitialize) {
2910
+ // called when the visibility has changed, i.e. the browser
2911
+ // transitioned from hidden -> visible so we need to see if the session
2912
+ // should be recovered immediately... but to do that we need to acquire
2913
+ // the lock first asynchronously
2914
+ await this.initializePromise
2915
+
2916
+ await this._acquireLock(-1, async () => {
2917
+ if (document.visibilityState !== 'visible') {
2918
+ this._debug(
2919
+ methodName,
2920
+ 'acquired the lock to recover the session, but the browser visibilityState is no longer visible, aborting'
2921
+ )
2922
+
2923
+ // visibility has changed while waiting for the lock, abort
2924
+ return
2925
+ }
2926
+
2927
+ // recover the session
2928
+ await this._recoverAndRefresh()
2929
+ })
2930
+ }
2931
+ } else if (document.visibilityState === 'hidden') {
2932
+ if (this.autoRefreshToken) {
2933
+ this._stopAutoRefresh()
2934
+ }
2935
+ }
2936
+ }
2937
+
2938
+ /**
2939
+ * Generates the relevant login URL for a third-party provider.
2940
+ * @param options.redirectTo A URL or mobile address to send the user to after they are confirmed.
2941
+ * @param options.scopes A space-separated list of scopes granted to the OAuth application.
2942
+ * @param options.queryParams An object of key-value pairs containing query parameters granted to the OAuth application.
2943
+ */
2944
+ private async _getUrlForProvider(
2945
+ url: string,
2946
+ provider: Provider,
2947
+ options: {
2948
+ redirectTo?: string
2949
+ scopes?: string
2950
+ queryParams?: { [key: string]: string }
2951
+ skipBrowserRedirect?: boolean
2952
+ }
2953
+ ) {
2954
+ const urlParams: string[] = [`provider=${encodeURIComponent(provider)}`]
2955
+ if (options?.redirectTo) {
2956
+ urlParams.push(`redirect_to=${encodeURIComponent(options.redirectTo)}`)
2957
+ }
2958
+ if (options?.scopes) {
2959
+ urlParams.push(`scopes=${encodeURIComponent(options.scopes)}`)
2960
+ }
2961
+ if (this.flowType === 'pkce') {
2962
+ const [codeChallenge, codeChallengeMethod] = await getCodeChallengeAndMethod(
2963
+ this.storage,
2964
+ this.storageKey
2965
+ )
2966
+
2967
+ const flowParams = new URLSearchParams({
2968
+ code_challenge: `${encodeURIComponent(codeChallenge)}`,
2969
+ code_challenge_method: `${encodeURIComponent(codeChallengeMethod)}`,
2970
+ })
2971
+ urlParams.push(flowParams.toString())
2972
+ }
2973
+ if (options?.queryParams) {
2974
+ const query = new URLSearchParams(options.queryParams)
2975
+ urlParams.push(query.toString())
2976
+ }
2977
+ if (options?.skipBrowserRedirect) {
2978
+ urlParams.push(`skip_http_redirect=${options.skipBrowserRedirect}`)
2979
+ }
2980
+
2981
+ return `${url}?${urlParams.join('&')}`
2982
+ }
2983
+
2984
+ private async _unenroll(params: MFAUnenrollParams): Promise<AuthMFAUnenrollResponse> {
2985
+ try {
2986
+ return await this._useSession(async (result) => {
2987
+ const { data: sessionData, error: sessionError } = result
2988
+ if (sessionError) {
2989
+ return { data: null, error: sessionError }
2990
+ }
2991
+
2992
+ return await _request(this.fetch, 'DELETE', `${this.url}/factors/${params.factorId}`, {
2993
+ headers: this.headers,
2994
+ jwt: sessionData?.session?.access_token,
2995
+ })
2996
+ })
2997
+ } catch (error) {
2998
+ if (isAuthError(error)) {
2999
+ return { data: null, error }
3000
+ }
3001
+ throw error
3002
+ }
3003
+ }
3004
+
3005
+ /**
3006
+ * {@see GoTrueMFAApi#enroll}
3007
+ */
3008
+ private async _enroll(params: MFAEnrollTOTPParams): Promise<AuthMFAEnrollTOTPResponse>
3009
+ private async _enroll(params: MFAEnrollPhoneParams): Promise<AuthMFAEnrollPhoneResponse>
3010
+ private async _enroll(params: MFAEnrollWebauthnParams): Promise<AuthMFAEnrollWebauthnResponse>
3011
+ private async _enroll(params: MFAEnrollParams): Promise<AuthMFAEnrollResponse> {
3012
+ try {
3013
+ return await this._useSession(async (result) => {
3014
+ const { data: sessionData, error: sessionError } = result
3015
+ if (sessionError) {
3016
+ return { data: null, error: sessionError }
3017
+ }
3018
+
3019
+ const body = {
3020
+ friendly_name: params.friendlyName,
3021
+ factor_type: params.factorType,
3022
+ ...(params.factorType === 'phone'
3023
+ ? { phone: params.phone }
3024
+ : params.factorType === 'totp'
3025
+ ? { issuer: params.issuer }
3026
+ : {}),
3027
+ }
3028
+
3029
+ const { data, error } = (await _request(this.fetch, 'POST', `${this.url}/factors`, {
3030
+ body,
3031
+ headers: this.headers,
3032
+ jwt: sessionData?.session?.access_token,
3033
+ })) as AuthMFAEnrollResponse
3034
+ if (error) {
3035
+ return { data: null, error }
3036
+ }
3037
+
3038
+ if (params.factorType === 'totp' && data.type === 'totp' && data?.totp?.qr_code) {
3039
+ data.totp.qr_code = `data:image/svg+xml;utf-8,${data.totp.qr_code}`
3040
+ }
3041
+
3042
+ return { data, error: null }
3043
+ })
3044
+ } catch (error) {
3045
+ if (isAuthError(error)) {
3046
+ return { data: null, error }
3047
+ }
3048
+ throw error
3049
+ }
3050
+ }
3051
+
3052
+ /**
3053
+ * {@see GoTrueMFAApi#verify}
3054
+ */
3055
+ private async _verify(params: MFAVerifyTOTPParams): Promise<AuthMFAVerifyResponse>
3056
+ private async _verify(params: MFAVerifyPhoneParams): Promise<AuthMFAVerifyResponse>
3057
+ private async _verify<T extends 'create' | 'request'>(
3058
+ params: MFAVerifyWebauthnParams<T>
3059
+ ): Promise<AuthMFAVerifyResponse>
3060
+ private async _verify(params: MFAVerifyParams): Promise<AuthMFAVerifyResponse> {
3061
+ return this._acquireLock(-1, async () => {
3062
+ try {
3063
+ return await this._useSession(async (result) => {
3064
+ const { data: sessionData, error: sessionError } = result
3065
+ if (sessionError) {
3066
+ return { data: null, error: sessionError }
3067
+ }
3068
+
3069
+ const body: StrictOmit<
3070
+ | Exclude<MFAVerifyParams, MFAVerifyWebauthnParams>
3071
+ /** Exclude out the webauthn params from here because we're going to need to serialize them in the response */
3072
+ | Prettify<
3073
+ StrictOmit<MFAVerifyWebauthnParams, 'webauthn'> & {
3074
+ webauthn: Prettify<
3075
+ StrictOmit<MFAVerifyWebauthnParamFields['webauthn'], 'credential_response'> & {
3076
+ credential_response: PublicKeyCredentialJSON
3077
+ }
3078
+ >
3079
+ }
3080
+ >,
3081
+ /* Exclude challengeId because the backend expects snake_case, and exclude factorId since it's passed in the path params */
3082
+ 'challengeId' | 'factorId'
3083
+ > & {
3084
+ challenge_id: string
3085
+ } = {
3086
+ challenge_id: params.challengeId,
3087
+ ...('webauthn' in params
3088
+ ? {
3089
+ webauthn: {
3090
+ ...params.webauthn,
3091
+ credential_response:
3092
+ params.webauthn.type === 'create'
3093
+ ? serializeCredentialCreationResponse(
3094
+ params.webauthn.credential_response as RegistrationCredential
3095
+ )
3096
+ : serializeCredentialRequestResponse(
3097
+ params.webauthn.credential_response as AuthenticationCredential
3098
+ ),
3099
+ },
3100
+ }
3101
+ : { code: params.code }),
3102
+ }
3103
+
3104
+ const { data, error } = await _request(
3105
+ this.fetch,
3106
+ 'POST',
3107
+ `${this.url}/factors/${params.factorId}/verify`,
3108
+ {
3109
+ body,
3110
+ headers: this.headers,
3111
+ jwt: sessionData?.session?.access_token,
3112
+ }
3113
+ )
3114
+ if (error) {
3115
+ return { data: null, error }
3116
+ }
3117
+
3118
+ await this._saveSession({
3119
+ expires_at: Math.round(Date.now() / 1000) + data.expires_in,
3120
+ ...data,
3121
+ })
3122
+ await this._notifyAllSubscribers('MFA_CHALLENGE_VERIFIED', data)
3123
+
3124
+ return { data, error }
3125
+ })
3126
+ } catch (error) {
3127
+ if (isAuthError(error)) {
3128
+ return { data: null, error }
3129
+ }
3130
+ throw error
3131
+ }
3132
+ })
3133
+ }
3134
+
3135
+ /**
3136
+ * {@see GoTrueMFAApi#challenge}
3137
+ */
3138
+ private async _challenge(
3139
+ params: MFAChallengeTOTPParams
3140
+ ): Promise<Prettify<AuthMFAChallengeTOTPResponse>>
3141
+ private async _challenge(
3142
+ params: MFAChallengePhoneParams
3143
+ ): Promise<Prettify<AuthMFAChallengePhoneResponse>>
3144
+ private async _challenge(
3145
+ params: MFAChallengeWebauthnParams
3146
+ ): Promise<Prettify<AuthMFAChallengeWebauthnResponse>>
3147
+ private async _challenge(params: MFAChallengeParams): Promise<AuthMFAChallengeResponse> {
3148
+ return this._acquireLock(-1, async () => {
3149
+ try {
3150
+ return await this._useSession(async (result) => {
3151
+ const { data: sessionData, error: sessionError } = result
3152
+ if (sessionError) {
3153
+ return { data: null, error: sessionError }
3154
+ }
3155
+
3156
+ const response = (await _request(
3157
+ this.fetch,
3158
+ 'POST',
3159
+ `${this.url}/factors/${params.factorId}/challenge`,
3160
+ {
3161
+ body: params,
3162
+ headers: this.headers,
3163
+ jwt: sessionData?.session?.access_token,
3164
+ }
3165
+ )) as
3166
+ | Exclude<AuthMFAChallengeResponse, AuthMFAChallengeWebauthnResponse>
3167
+ /** The server will send `serialized` data, so we assert the serialized response */
3168
+ | AuthMFAChallengeWebauthnServerResponse
3169
+
3170
+ if (response.error) {
3171
+ return response
3172
+ }
3173
+
3174
+ const { data } = response
3175
+
3176
+ if (data.type !== 'webauthn') {
3177
+ return { data, error: null }
3178
+ }
3179
+
3180
+ switch (data.webauthn.type) {
3181
+ case 'create':
3182
+ return {
3183
+ data: {
3184
+ ...data,
3185
+ webauthn: {
3186
+ ...data.webauthn,
3187
+ credential_options: {
3188
+ ...data.webauthn.credential_options,
3189
+ publicKey: deserializeCredentialCreationOptions(
3190
+ data.webauthn.credential_options.publicKey
3191
+ ),
3192
+ },
3193
+ },
3194
+ },
3195
+ error: null,
3196
+ }
3197
+ case 'request':
3198
+ return {
3199
+ data: {
3200
+ ...data,
3201
+ webauthn: {
3202
+ ...data.webauthn,
3203
+ credential_options: {
3204
+ ...data.webauthn.credential_options,
3205
+ publicKey: deserializeCredentialRequestOptions(
3206
+ data.webauthn.credential_options.publicKey
3207
+ ),
3208
+ },
3209
+ },
3210
+ },
3211
+ error: null,
3212
+ }
3213
+ }
3214
+ })
3215
+ } catch (error) {
3216
+ if (isAuthError(error)) {
3217
+ return { data: null, error }
3218
+ }
3219
+ throw error
3220
+ }
3221
+ })
3222
+ }
3223
+
3224
+ /**
3225
+ * {@see GoTrueMFAApi#challengeAndVerify}
3226
+ */
3227
+ private async _challengeAndVerify(
3228
+ params: MFAChallengeAndVerifyParams
3229
+ ): Promise<AuthMFAVerifyResponse> {
3230
+ // both _challenge and _verify independently acquire the lock, so no need
3231
+ // to acquire it here
3232
+
3233
+ const { data: challengeData, error: challengeError } = await this._challenge({
3234
+ factorId: params.factorId,
3235
+ })
3236
+ if (challengeError) {
3237
+ return { data: null, error: challengeError }
3238
+ }
3239
+
3240
+ return await this._verify({
3241
+ factorId: params.factorId,
3242
+ challengeId: challengeData.id,
3243
+ code: params.code,
3244
+ })
3245
+ }
3246
+
3247
+ /**
3248
+ * {@see GoTrueMFAApi#listFactors}
3249
+ */
3250
+ private async _listFactors(): Promise<AuthMFAListFactorsResponse> {
3251
+ // use #getUser instead of #_getUser as the former acquires a lock
3252
+ const {
3253
+ data: { user },
3254
+ error: userError,
3255
+ } = await this.getUser()
3256
+ if (userError) {
3257
+ return { data: null, error: userError }
3258
+ }
3259
+
3260
+ const data: AuthMFAListFactorsResponse['data'] = {
3261
+ all: [],
3262
+ phone: [],
3263
+ totp: [],
3264
+ webauthn: [],
3265
+ }
3266
+
3267
+ // loop over the factors ONCE
3268
+ for (const factor of user?.factors ?? []) {
3269
+ data.all.push(factor)
3270
+ if (factor.status === 'verified') {
3271
+ ;(data[factor.factor_type] as (typeof factor)[]).push(factor)
3272
+ }
3273
+ }
3274
+
3275
+ return {
3276
+ data,
3277
+ error: null,
3278
+ }
3279
+ }
3280
+
3281
+ /**
3282
+ * {@see GoTrueMFAApi#getAuthenticatorAssuranceLevel}
3283
+ */
3284
+ private async _getAuthenticatorAssuranceLevel(): Promise<AuthMFAGetAuthenticatorAssuranceLevelResponse> {
3285
+ return this._acquireLock(-1, async () => {
3286
+ return await this._useSession(async (result) => {
3287
+ const {
3288
+ data: { session },
3289
+ error: sessionError,
3290
+ } = result
3291
+ if (sessionError) {
3292
+ return { data: null, error: sessionError }
3293
+ }
3294
+ if (!session) {
3295
+ return {
3296
+ data: { currentLevel: null, nextLevel: null, currentAuthenticationMethods: [] },
3297
+ error: null,
3298
+ }
3299
+ }
3300
+
3301
+ const { payload } = decodeJWT(session.access_token)
3302
+
3303
+ let currentLevel: AuthenticatorAssuranceLevels | null = null
3304
+
3305
+ if (payload.aal) {
3306
+ currentLevel = payload.aal
3307
+ }
3308
+
3309
+ let nextLevel: AuthenticatorAssuranceLevels | null = currentLevel
3310
+
3311
+ const verifiedFactors =
3312
+ session.user.factors?.filter((factor: Factor) => factor.status === 'verified') ?? []
3313
+
3314
+ if (verifiedFactors.length > 0) {
3315
+ nextLevel = 'aal2'
3316
+ }
3317
+
3318
+ const currentAuthenticationMethods = payload.amr || []
3319
+
3320
+ return { data: { currentLevel, nextLevel, currentAuthenticationMethods }, error: null }
3321
+ })
3322
+ })
3323
+ }
3324
+
3325
+ private async fetchJwk(kid: string, jwks: { keys: JWK[] } = { keys: [] }): Promise<JWK | null> {
3326
+ // try fetching from the supplied jwks
3327
+ let jwk = jwks.keys.find((key) => key.kid === kid)
3328
+ if (jwk) {
3329
+ return jwk
3330
+ }
3331
+
3332
+ const now = Date.now()
3333
+
3334
+ // try fetching from cache
3335
+ jwk = this.jwks.keys.find((key) => key.kid === kid)
3336
+
3337
+ // jwk exists and jwks isn't stale
3338
+ if (jwk && this.jwks_cached_at + JWKS_TTL > now) {
3339
+ return jwk
3340
+ }
3341
+ // jwk isn't cached in memory so we need to fetch it from the well-known endpoint
3342
+ const { data, error } = await _request(this.fetch, 'GET', `${this.url}/.well-known/jwks.json`, {
3343
+ headers: this.headers,
3344
+ })
3345
+ if (error) {
3346
+ throw error
3347
+ }
3348
+ if (!data.keys || data.keys.length === 0) {
3349
+ return null
3350
+ }
3351
+
3352
+ this.jwks = data
3353
+ this.jwks_cached_at = now
3354
+
3355
+ // Find the signing key
3356
+ jwk = data.keys.find((key: any) => key.kid === kid)
3357
+ if (!jwk) {
3358
+ return null
3359
+ }
3360
+ return jwk
3361
+ }
3362
+
3363
+ /**
3364
+ * Extracts the JWT claims present in the access token by first verifying the
3365
+ * JWT against the server's JSON Web Key Set endpoint
3366
+ * `/.well-known/jwks.json` which is often cached, resulting in significantly
3367
+ * faster responses. Prefer this method over {@link #getUser} which always
3368
+ * sends a request to the Auth server for each JWT.
3369
+ *
3370
+ * If the project is not using an asymmetric JWT signing key (like ECC or
3371
+ * RSA) it always sends a request to the Auth server (similar to {@link
3372
+ * #getUser}) to verify the JWT.
3373
+ *
3374
+ * @param jwt An optional specific JWT you wish to verify, not the one you
3375
+ * can obtain from {@link #getSession}.
3376
+ * @param options Various additional options that allow you to customize the
3377
+ * behavior of this method.
3378
+ */
3379
+ async getClaims(
3380
+ jwt?: string,
3381
+ options: {
3382
+ /**
3383
+ * @deprecated Please use options.jwks instead.
3384
+ */
3385
+ keys?: JWK[]
3386
+
3387
+ /** If set to `true` the `exp` claim will not be validated against the current time. */
3388
+ allowExpired?: boolean
3389
+
3390
+ /** If set, this JSON Web Key Set is going to have precedence over the cached value available on the server. */
3391
+ jwks?: { keys: JWK[] }
3392
+ } = {}
3393
+ ): Promise<
3394
+ | {
3395
+ data: { claims: JwtPayload; header: JwtHeader; signature: Uint8Array }
3396
+ error: null
3397
+ }
3398
+ | { data: null; error: AuthError }
3399
+ | { data: null; error: null }
3400
+ > {
3401
+ try {
3402
+ let token = jwt
3403
+ if (!token) {
3404
+ const { data, error } = await this.getSession()
3405
+ if (error || !data.session) {
3406
+ return { data: null, error }
3407
+ }
3408
+ token = data.session.access_token
3409
+ }
3410
+
3411
+ const {
3412
+ header,
3413
+ payload,
3414
+ signature,
3415
+ raw: { header: rawHeader, payload: rawPayload },
3416
+ } = decodeJWT(token)
3417
+
3418
+ if (!options?.allowExpired) {
3419
+ // Reject expired JWTs should only happen if jwt argument was passed
3420
+ validateExp(payload.exp)
3421
+ }
3422
+
3423
+ const signingKey =
3424
+ !header.alg ||
3425
+ header.alg.startsWith('HS') ||
3426
+ !header.kid ||
3427
+ !('crypto' in globalThis && 'subtle' in globalThis.crypto)
3428
+ ? null
3429
+ : await this.fetchJwk(header.kid, options?.keys ? { keys: options.keys } : options?.jwks)
3430
+
3431
+ // If symmetric algorithm or WebCrypto API is unavailable, fallback to getUser()
3432
+ if (!signingKey) {
3433
+ const { error } = await this.getUser(token)
3434
+ if (error) {
3435
+ throw error
3436
+ }
3437
+ // getUser succeeds so the claims in the JWT can be trusted
3438
+ return {
3439
+ data: {
3440
+ claims: payload,
3441
+ header,
3442
+ signature,
3443
+ },
3444
+ error: null,
3445
+ }
3446
+ }
3447
+
3448
+ const algorithm = getAlgorithm(header.alg)
3449
+
3450
+ // Convert JWK to CryptoKey
3451
+ const publicKey = await crypto.subtle.importKey('jwk', signingKey, algorithm, true, [
3452
+ 'verify',
3453
+ ])
3454
+
3455
+ // Verify the signature
3456
+ const isValid = await crypto.subtle.verify(
3457
+ algorithm,
3458
+ publicKey,
3459
+ signature,
3460
+ stringToUint8Array(`${rawHeader}.${rawPayload}`)
3461
+ )
3462
+
3463
+ if (!isValid) {
3464
+ throw new AuthInvalidJwtError('Invalid JWT signature')
3465
+ }
3466
+
3467
+ // If verification succeeds, decode and return claims
3468
+ return {
3469
+ data: {
3470
+ claims: payload,
3471
+ header,
3472
+ signature,
3473
+ },
3474
+ error: null,
3475
+ }
3476
+ } catch (error) {
3477
+ if (isAuthError(error)) {
3478
+ return { data: null, error }
3479
+ }
3480
+ throw error
3481
+ }
3482
+ }
3483
+ }