• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / auth-js / 21284056506

23 Jan 2026 11:08AM UTC coverage: 69.764% (-0.9%) from 70.669%
21284056506

Pull #1131

github

web-flow
Merge 3516059e3 into 25c6b4271
Pull Request #1131: chore(deps-dev): bump jws from 3.2.2 to 3.2.3

1070 of 1681 branches covered (63.65%)

Branch coverage included in aggregate %.

1475 of 1967 relevant lines covered (74.99%)

69.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.57
/src/lib/helpers.ts
1
import { API_VERSION_HEADER_NAME, BASE64URL_REGEX } from './constants'
7✔
2
import { AuthInvalidJwtError } from './errors'
7✔
3
import { base64UrlToUint8Array, stringFromBase64URL } from './base64url'
7✔
4
import { JwtHeader, JwtPayload, SupportedStorage, User } from './types'
5
import { Uint8Array_ } from './webauthn.dom'
6

7
export function expiresAt(expiresIn: number) {
7✔
8
  const timeNow = Math.round(Date.now() / 1000)
×
9
  return timeNow + expiresIn
×
10
}
11

12
export function uuid() {
7✔
13
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
5✔
14
    const r = (Math.random() * 16) | 0,
155✔
15
      v = c == 'x' ? r : (r & 0x3) | 0x8
155✔
16
    return v.toString(16)
155✔
17
  })
18
}
19

20
export const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined'
442✔
21

22
const localStorageWriteTests = {
7✔
23
  tested: false,
24
  writable: false,
25
}
26

27
/**
28
 * Checks whether localStorage is supported on this browser.
29
 */
30
export const supportsLocalStorage = () => {
7✔
31
  if (!isBrowser()) {
25✔
32
    return false
13✔
33
  }
34

35
  try {
12✔
36
    if (typeof globalThis.localStorage !== 'object') {
12✔
37
      return false
10✔
38
    }
39
  } catch (e) {
40
    // DOM exception when accessing `localStorage`
41
    return false
×
42
  }
43

44
  if (localStorageWriteTests.tested) {
2✔
45
    return localStorageWriteTests.writable
1✔
46
  }
47

48
  const randomKey = `lswt-${Math.random()}${Math.random()}`
1✔
49

50
  try {
1✔
51
    globalThis.localStorage.setItem(randomKey, randomKey)
1✔
52
    globalThis.localStorage.removeItem(randomKey)
1✔
53

54
    localStorageWriteTests.tested = true
1✔
55
    localStorageWriteTests.writable = true
1✔
56
  } catch (e) {
57
    // localStorage can't be written to
58
    // https://www.chromium.org/for-testers/bug-reporting-guidelines/uncaught-securityerror-failed-to-read-the-localstorage-property-from-window-access-is-denied-for-this-document
59

60
    localStorageWriteTests.tested = true
×
61
    localStorageWriteTests.writable = false
×
62
  }
63

64
  return localStorageWriteTests.writable
1✔
65
}
66

67
/**
68
 * Extracts parameters encoded in the URL both in the query and fragment.
69
 */
70
export function parseParametersFromURL(href: string) {
7✔
71
  const result: { [parameter: string]: string } = {}
38✔
72

73
  const url = new URL(href)
38✔
74

75
  if (url.hash && url.hash[0] === '#') {
38✔
76
    try {
4✔
77
      const hashSearchParams = new URLSearchParams(url.hash.substring(1))
4✔
78
      hashSearchParams.forEach((value, key) => {
4✔
79
        result[key] = value
11✔
80
      })
81
    } catch (e: any) {
82
      // hash is not a query string
83
    }
84
  }
85

86
  // search parameters take precedence over hash parameters
87
  url.searchParams.forEach((value, key) => {
38✔
88
    result[key] = value
22✔
89
  })
90

91
  return result
38✔
92
}
93

94
type Fetch = typeof fetch
95

96
export const resolveFetch = (customFetch?: Fetch): Fetch => {
7✔
97
  let _fetch: Fetch
98
  if (customFetch) {
203✔
99
    _fetch = customFetch
19✔
100
  } else if (typeof fetch === 'undefined') {
184✔
101
    _fetch = (...args) =>
27✔
102
      import('@supabase/node-fetch' as any).then(({ default: fetch }) => fetch(...args))
2✔
103
  } else {
104
    _fetch = fetch
157✔
105
  }
106
  return (...args) => _fetch(...args)
232✔
107
}
108

109
export const looksLikeFetchResponse = (maybeResponse: unknown): maybeResponse is Response => {
7✔
110
  return (
68✔
111
    typeof maybeResponse === 'object' &&
408✔
112
    maybeResponse !== null &&
113
    'status' in maybeResponse &&
114
    'ok' in maybeResponse &&
115
    'json' in maybeResponse &&
116
    typeof (maybeResponse as any).json === 'function'
117
  )
118
}
119

120
// Storage helpers
121
export const setItemAsync = async (
7✔
122
  storage: SupportedStorage,
123
  key: string,
124
  data: any
125
): Promise<void> => {
126
  await storage.setItem(key, JSON.stringify(data))
101✔
127
}
128

129
export const getItemAsync = async (storage: SupportedStorage, key: string): Promise<unknown> => {
7✔
130
  const value = await storage.getItem(key)
292✔
131

132
  if (!value) {
290✔
133
    return null
168✔
134
  }
135

136
  try {
122✔
137
    return JSON.parse(value)
122✔
138
  } catch {
139
    return value
5✔
140
  }
141
}
142

143
export const removeItemAsync = async (storage: SupportedStorage, key: string): Promise<void> => {
7✔
144
  await storage.removeItem(key)
515✔
145
}
146

147
/**
148
 * A deferred represents some asynchronous work that is not yet finished, which
149
 * may or may not culminate in a value.
150
 * Taken from: https://github.com/mike-north/types/blob/master/src/async.ts
151
 */
152
export class Deferred<T = any> {
7✔
153
  public static promiseConstructor: PromiseConstructor = Promise
7✔
154

155
  public readonly promise!: PromiseLike<T>
156

157
  public readonly resolve!: (value?: T | PromiseLike<T>) => void
158

159
  public readonly reject!: (reason?: any) => any
160

161
  public constructor() {
162
    // eslint-disable-next-line @typescript-eslint/no-extra-semi
163
    ;(this as any).promise = new Deferred.promiseConstructor((res, rej) => {
16✔
164
      // eslint-disable-next-line @typescript-eslint/no-extra-semi
165
      ;(this as any).resolve = res
16✔
166
      // eslint-disable-next-line @typescript-eslint/no-extra-semi
167
      ;(this as any).reject = rej
16✔
168
    })
169
  }
170
}
171

172
export function decodeJWT(token: string): {
7✔
173
  header: JwtHeader
174
  payload: JwtPayload
175
  signature: Uint8Array_
176
  raw: {
177
    header: string
178
    payload: string
179
  }
180
} {
181
  const parts = token.split('.')
14✔
182

183
  if (parts.length !== 3) {
14✔
184
    throw new AuthInvalidJwtError('Invalid JWT structure')
3✔
185
  }
186

187
  // Regex checks for base64url format
188
  for (let i = 0; i < parts.length; i++) {
11✔
189
    if (!BASE64URL_REGEX.test(parts[i] as string)) {
32✔
190
      throw new AuthInvalidJwtError('JWT not in base64url format')
2✔
191
    }
192
  }
193
  const data = {
9✔
194
    // using base64url lib
195
    header: JSON.parse(stringFromBase64URL(parts[0])),
196
    payload: JSON.parse(stringFromBase64URL(parts[1])),
197
    signature: base64UrlToUint8Array(parts[2]),
198
    raw: {
199
      header: parts[0],
200
      payload: parts[1],
201
    },
202
  }
203
  return data
9✔
204
}
205

206
/**
207
 * Creates a promise that resolves to null after some time.
208
 */
209
export async function sleep(time: number): Promise<null> {
7✔
210
  return await new Promise((accept) => {
1✔
211
    setTimeout(() => accept(null), time)
1✔
212
  })
213
}
214

215
/**
216
 * Converts the provided async function into a retryable function. Each result
217
 * or thrown error is sent to the isRetryable function which should return true
218
 * if the function should run again.
219
 */
220
export function retryable<T>(
7✔
221
  fn: (attempt: number) => Promise<T>,
222
  isRetryable: (attempt: number, error: any | null, result?: T) => boolean
223
): Promise<T> {
224
  const promise = new Promise<T>((accept, reject) => {
14✔
225
    // eslint-disable-next-line @typescript-eslint/no-extra-semi
226
    ;(async () => {
14✔
227
      for (let attempt = 0; attempt < Infinity; attempt++) {
14✔
228
        try {
14✔
229
          const result = await fn(attempt)
14✔
230

231
          if (!isRetryable(attempt, null, result)) {
9✔
232
            accept(result)
9✔
233
            return
9✔
234
          }
235
        } catch (e: any) {
236
          if (!isRetryable(attempt, e)) {
5✔
237
            reject(e)
5✔
238
            return
5✔
239
          }
240
        }
241
      }
242
    })()
243
  })
244

245
  return promise
14✔
246
}
247

248
function dec2hex(dec: number) {
249
  return ('0' + dec.toString(16)).substr(-2)
×
250
}
251

252
// Functions below taken from: https://stackoverflow.com/questions/63309409/creating-a-code-verifier-and-challenge-for-pkce-auth-on-spotify-api-in-reactjs
253
export function generatePKCEVerifier() {
7✔
254
  const verifierLength = 56
12✔
255
  const array = new Uint32Array(verifierLength)
12✔
256
  if (typeof crypto === 'undefined') {
12✔
257
    const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
12✔
258
    const charSetLen = charSet.length
12✔
259
    let verifier = ''
12✔
260
    for (let i = 0; i < verifierLength; i++) {
12✔
261
      verifier += charSet.charAt(Math.floor(Math.random() * charSetLen))
672✔
262
    }
263
    return verifier
12✔
264
  }
265
  crypto.getRandomValues(array)
×
266
  return Array.from(array, dec2hex).join('')
×
267
}
268

269
async function sha256(randomString: string) {
270
  const encoder = new TextEncoder()
×
271
  const encodedData = encoder.encode(randomString)
×
272
  const hash = await crypto.subtle.digest('SHA-256', encodedData)
×
273
  const bytes = new Uint8Array(hash)
×
274

275
  return Array.from(bytes)
×
276
    .map((c) => String.fromCharCode(c))
×
277
    .join('')
278
}
279

280
export async function generatePKCEChallenge(verifier: string) {
7✔
281
  const hasCryptoSupport =
282
    typeof crypto !== 'undefined' &&
12!
283
    typeof crypto.subtle !== 'undefined' &&
284
    typeof TextEncoder !== 'undefined'
285

286
  if (!hasCryptoSupport) {
12✔
287
    console.warn(
12✔
288
      'WebCrypto API is not supported. Code challenge method will default to use plain instead of sha256.'
289
    )
290
    return verifier
12✔
291
  }
292
  const hashed = await sha256(verifier)
×
293
  return btoa(hashed).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
×
294
}
295

296
export async function getCodeChallengeAndMethod(
7✔
297
  storage: SupportedStorage,
298
  storageKey: string,
299
  isPasswordRecovery = false
10✔
300
) {
301
  const codeVerifier = generatePKCEVerifier()
12✔
302
  let storedCodeVerifier = codeVerifier
12✔
303
  if (isPasswordRecovery) {
12✔
304
    storedCodeVerifier += '/PASSWORD_RECOVERY'
1✔
305
  }
306
  await setItemAsync(storage, `${storageKey}-code-verifier`, storedCodeVerifier)
12✔
307
  const codeChallenge = await generatePKCEChallenge(codeVerifier)
12✔
308
  const codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'
12!
309
  return [codeChallenge, codeChallengeMethod]
12✔
310
}
311

312
/** Parses the API version which is 2YYY-MM-DD. */
313
const API_VERSION_REGEX = /^2[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-9]|3[0-1])$/i
7✔
314

315
export function parseResponseAPIVersion(response: Response) {
7✔
316
  const apiVersion = response.headers.get(API_VERSION_HEADER_NAME)
71✔
317

318
  if (!apiVersion) {
71✔
319
    return null
8✔
320
  }
321

322
  if (!apiVersion.match(API_VERSION_REGEX)) {
63✔
323
    return null
3✔
324
  }
325

326
  try {
60✔
327
    const date = new Date(`${apiVersion}T00:00:00.0Z`)
60✔
328
    return date
60✔
329
  } catch (e: any) {
330
    return null
×
331
  }
332
}
333

334
export function validateExp(exp: number) {
7✔
335
  if (!exp) {
6✔
336
    throw new Error('Missing exp claim')
1✔
337
  }
338
  const timeNow = Math.floor(Date.now() / 1000)
5✔
339
  if (exp <= timeNow) {
5✔
340
    throw new Error('JWT has expired')
1✔
341
  }
342
}
343

344
export function getAlgorithm(
7✔
345
  alg: 'HS256' | 'RS256' | 'ES256'
346
): RsaHashedImportParams | EcKeyImportParams {
347
  switch (alg) {
3✔
348
    case 'RS256':
349
      return {
1✔
350
        name: 'RSASSA-PKCS1-v1_5',
351
        hash: { name: 'SHA-256' },
352
      }
353
    case 'ES256':
354
      return {
1✔
355
        name: 'ECDSA',
356
        namedCurve: 'P-256',
357
        hash: { name: 'SHA-256' },
358
      }
359
    default:
360
      throw new Error('Invalid alg claim')
1✔
361
  }
362
}
363

364
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
7✔
365

366
export function validateUUID(str: string) {
7✔
367
  if (!UUID_REGEX.test(str)) {
21✔
368
    throw new Error('@supabase/auth-js: Expected parameter to be UUID but is not')
3✔
369
  }
370
}
371

372
export function userNotAvailableProxy(): User {
7✔
373
  const proxyTarget = {} as User
8✔
374

375
  return new Proxy(proxyTarget, {
8✔
376
    get: (target: any, prop: string) => {
377
      if (prop === '__isUserNotAvailableProxy') {
4✔
378
        return true
3✔
379
      }
380
      // Preventative check for common problematic symbols during cloning/inspection
381
      // These symbols might be accessed by structuredClone or other internal mechanisms.
382
      if (typeof prop === 'symbol') {
1!
383
        const sProp = (prop as symbol).toString()
×
384
        if (
×
385
          sProp === 'Symbol(Symbol.toPrimitive)' ||
×
386
          sProp === 'Symbol(Symbol.toStringTag)' ||
387
          sProp === 'Symbol(util.inspect.custom)'
388
        ) {
389
          // Node.js util.inspect
390
          return undefined
×
391
        }
392
      }
393
      throw new Error(
1✔
394
        `@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Accessing the "${prop}" property of the session object is not supported. Please use getUser() instead.`
395
      )
396
    },
397
    set: (_target: any, prop: string) => {
398
      throw new Error(
1✔
399
        `@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Setting the "${prop}" property of the session object is not supported. Please use getUser() to fetch a user object you can manipulate.`
400
      )
401
    },
402
    deleteProperty: (_target: any, prop: string) => {
403
      throw new Error(
×
404
        `@supabase/auth-js: client was created with userStorage option and there was no user stored in the user storage. Deleting the "${prop}" property of the session object is not supported. Please use getUser() to fetch a user object you can manipulate.`
405
      )
406
    },
407
  })
408
}
409

410
/**
411
 * Deep clones a JSON-serializable object using JSON.parse(JSON.stringify(obj)).
412
 * Note: Only works for JSON-safe data.
413
 */
414
export function deepClone<T>(obj: T): T {
7✔
415
  return JSON.parse(JSON.stringify(obj))
88✔
416
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc