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

supabase / auth-js / 17292897055

28 Aug 2025 10:18AM UTC coverage: 80.811%. Remained the same
17292897055

Pull #1105

github

web-flow
Merge a6a43573f into be9a27cc7
Pull Request #1105: chore: secure-proof workflows

1080 of 1441 branches covered (74.95%)

Branch coverage included in aggregate %.

1451 of 1691 relevant lines covered (85.81%)

173.18 hits per line

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

93.06
/src/lib/helpers.ts
1
import { API_VERSION_HEADER_NAME, BASE64URL_REGEX } from './constants'
14✔
2
import { AuthInvalidJwtError } from './errors'
14✔
3
import { base64UrlToUint8Array, stringFromBase64URL } from './base64url'
14✔
4
import { JwtHeader, JwtPayload, SupportedStorage, User } from './types'
5

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

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

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

21
const localStorageWriteTests = {
14✔
22
  tested: false,
23
  writable: false,
24
}
25

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

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

43
  if (localStorageWriteTests.tested) {
4✔
44
    return localStorageWriteTests.writable
2✔
45
  }
46

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

49
  try {
2✔
50
    globalThis.localStorage.setItem(randomKey, randomKey)
2✔
51
    globalThis.localStorage.removeItem(randomKey)
2✔
52

53
    localStorageWriteTests.tested = true
2✔
54
    localStorageWriteTests.writable = true
2✔
55
  } catch (e) {
56
    // localStorage can't be written to
57
    // 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
58

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

63
  return localStorageWriteTests.writable
2✔
64
}
65

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

72
  const url = new URL(href)
76✔
73

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

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

90
  return result
76✔
91
}
92

93
type Fetch = typeof fetch
94

95
export const resolveFetch = (customFetch?: Fetch): Fetch => {
14✔
96
  let _fetch: Fetch
97
  if (customFetch) {
406✔
98
    _fetch = customFetch
38✔
99
  } else if (typeof fetch === 'undefined') {
368✔
100
    _fetch = (...args) =>
54✔
101
      import('@supabase/node-fetch' as any).then(({ default: fetch }) => fetch(...args))
4✔
102
  } else {
103
    _fetch = fetch
314✔
104
  }
105
  return (...args) => _fetch(...args)
464✔
106
}
107

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

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

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

131
  if (!value) {
581✔
132
    return null
336✔
133
  }
134

135
  try {
245✔
136
    return JSON.parse(value)
245✔
137
  } catch {
138
    return value
10✔
139
  }
140
}
141

142
export const removeItemAsync = async (storage: SupportedStorage, key: string): Promise<void> => {
14✔
143
  await storage.removeItem(key)
1,030✔
144
}
145

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

154
  public readonly promise!: PromiseLike<T>
155

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

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

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

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

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

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

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

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

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

244
  return promise
28✔
245
}
246

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

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

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

274
  return Array.from(bytes)
11✔
275
    .map((c) => String.fromCharCode(c))
352✔
276
    .join('')
277
}
278

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

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

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

311
/** Parses the API version which is 2YYY-MM-DD. */
312
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
14✔
313

314
export function parseResponseAPIVersion(response: Response) {
14✔
315
  const apiVersion = response.headers.get(API_VERSION_HEADER_NAME)
142✔
316

317
  if (!apiVersion) {
142✔
318
    return null
16✔
319
  }
320

321
  if (!apiVersion.match(API_VERSION_REGEX)) {
126✔
322
    return null
6✔
323
  }
324

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

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

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

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

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

371
export function userNotAvailableProxy(): User {
14✔
372
  const proxyTarget = {} as User
16✔
373

374
  return new Proxy(proxyTarget, {
16✔
375
    get: (target: any, prop: string) => {
376
      if (prop === '__isUserNotAvailableProxy') {
8✔
377
        return true
6✔
378
      }
379
      // Preventative check for common problematic symbols during cloning/inspection
380
      // These symbols might be accessed by structuredClone or other internal mechanisms.
381
      if (typeof prop === 'symbol') {
2!
382
        const sProp = (prop as symbol).toString()
×
383
        if (
×
384
          sProp === 'Symbol(Symbol.toPrimitive)' ||
×
385
          sProp === 'Symbol(Symbol.toStringTag)' ||
386
          sProp === 'Symbol(util.inspect.custom)'
387
        ) {
388
          // Node.js util.inspect
389
          return undefined
×
390
        }
391
      }
392
      throw new Error(
2✔
393
        `@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.`
394
      )
395
    },
396
    set: (_target: any, prop: string) => {
397
      throw new Error(
2✔
398
        `@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.`
399
      )
400
    },
401
    deleteProperty: (_target: any, prop: string) => {
402
      throw new Error(
×
403
        `@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.`
404
      )
405
    },
406
  })
407
}
408

409
/**
410
 * Deep clones a JSON-serializable object using JSON.parse(JSON.stringify(obj)).
411
 * Note: Only works for JSON-safe data.
412
 */
413
export function deepClone<T>(obj: T): T {
14✔
414
  return JSON.parse(JSON.stringify(obj))
177✔
415
}
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

© 2025 Coveralls, Inc