@depup/supabase__auth-js 2.99.2-depup.0

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