wip:milestone 0 fixes
Some checks failed
CI/CD Pipeline / unit-tests (push) Failing after 1m16s
CI/CD Pipeline / integration-tests (push) Failing after 2m32s
CI/CD Pipeline / lint (push) Successful in 5m22s
CI/CD Pipeline / e2e-tests (push) Has been skipped
CI/CD Pipeline / build (push) Has been skipped

This commit is contained in:
2026-03-15 12:35:42 +02:00
parent 6708cf28a7
commit cffdf8af86
61266 changed files with 4511646 additions and 1938 deletions

View File

@@ -0,0 +1,5 @@
import GoTrueAdminApi from './GoTrueAdminApi'
const AuthAdminApi = GoTrueAdminApi
export default AuthAdminApi

View File

@@ -0,0 +1,5 @@
import GoTrueClient from './GoTrueClient'
const AuthClient = GoTrueClient
export default AuthClient

View File

@@ -0,0 +1,723 @@
import {
Fetch,
_generateLinkResponse,
_noResolveJsonResponse,
_request,
_userResponse,
} from './lib/fetch'
import { resolveFetch, validateUUID } from './lib/helpers'
import {
AdminUserAttributes,
GenerateLinkParams,
GenerateLinkResponse,
Pagination,
User,
UserResponse,
GoTrueAdminMFAApi,
AuthMFAAdminDeleteFactorParams,
AuthMFAAdminDeleteFactorResponse,
AuthMFAAdminListFactorsParams,
AuthMFAAdminListFactorsResponse,
PageParams,
SIGN_OUT_SCOPES,
SignOutScope,
GoTrueAdminOAuthApi,
CreateOAuthClientParams,
UpdateOAuthClientParams,
OAuthClientResponse,
OAuthClientListResponse,
GoTrueAdminCustomProvidersApi,
CreateCustomProviderParams,
UpdateCustomProviderParams,
ListCustomProvidersParams,
CustomProviderResponse,
CustomProviderListResponse,
} from './lib/types'
import { AuthError, isAuthError } from './lib/errors'
export default class GoTrueAdminApi {
/** Contains all MFA administration methods. */
mfa: GoTrueAdminMFAApi
/**
* Contains all OAuth client administration methods.
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
*/
oauth: GoTrueAdminOAuthApi
/** Contains all custom OIDC/OAuth provider administration methods. */
customProviders: GoTrueAdminCustomProvidersApi
protected url: string
protected headers: {
[key: string]: string
}
protected fetch: Fetch
/**
* Creates an admin API client that can be used to manage users and OAuth clients.
*
* @example
* ```ts
* import { GoTrueAdminApi } from '@supabase/auth-js'
*
* const admin = new GoTrueAdminApi({
* url: 'https://xyzcompany.supabase.co/auth/v1',
* headers: { Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY}` },
* })
* ```
*/
constructor({
url = '',
headers = {},
fetch,
}: {
url: string
headers?: {
[key: string]: string
}
fetch?: Fetch
}) {
this.url = url
this.headers = headers
this.fetch = resolveFetch(fetch)
this.mfa = {
listFactors: this._listFactors.bind(this),
deleteFactor: this._deleteFactor.bind(this),
}
this.oauth = {
listClients: this._listOAuthClients.bind(this),
createClient: this._createOAuthClient.bind(this),
getClient: this._getOAuthClient.bind(this),
updateClient: this._updateOAuthClient.bind(this),
deleteClient: this._deleteOAuthClient.bind(this),
regenerateClientSecret: this._regenerateOAuthClientSecret.bind(this),
}
this.customProviders = {
listProviders: this._listCustomProviders.bind(this),
createProvider: this._createCustomProvider.bind(this),
getProvider: this._getCustomProvider.bind(this),
updateProvider: this._updateCustomProvider.bind(this),
deleteProvider: this._deleteCustomProvider.bind(this),
}
}
/**
* Removes a logged-in session.
* @param jwt A valid, logged-in JWT.
* @param scope The logout sope.
*/
async signOut(
jwt: string,
scope: SignOutScope = SIGN_OUT_SCOPES[0]
): Promise<{ data: null; error: AuthError | null }> {
if (SIGN_OUT_SCOPES.indexOf(scope) < 0) {
throw new Error(
`@supabase/auth-js: Parameter scope must be one of ${SIGN_OUT_SCOPES.join(', ')}`
)
}
try {
await _request(this.fetch, 'POST', `${this.url}/logout?scope=${scope}`, {
headers: this.headers,
jwt,
noResolveJson: true,
})
return { data: null, error: null }
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Sends an invite link to an email address.
* @param email The email address of the user.
* @param options Additional options to be included when inviting.
*/
async inviteUserByEmail(
email: string,
options: {
/** A custom data object to store additional metadata about the user. This maps to the `auth.users.user_metadata` column. */
data?: object
/** The URL which will be appended to the email link sent to the user's email address. Once clicked the user will end up on this URL. */
redirectTo?: string
} = {}
): Promise<UserResponse> {
try {
return await _request(this.fetch, 'POST', `${this.url}/invite`, {
body: { email, data: options.data },
headers: this.headers,
redirectTo: options.redirectTo,
xform: _userResponse,
})
} catch (error) {
if (isAuthError(error)) {
return { data: { user: null }, error }
}
throw error
}
}
/**
* Generates email links and OTPs to be sent via a custom email provider.
* @param email The user's email.
* @param options.password User password. For signup only.
* @param options.data Optional user metadata. For signup only.
* @param options.redirectTo The redirect url which should be appended to the generated link
*/
async generateLink(params: GenerateLinkParams): Promise<GenerateLinkResponse> {
try {
const { options, ...rest } = params
const body: any = { ...rest, ...options }
if ('newEmail' in rest) {
// replace newEmail with new_email in request body
body.new_email = rest?.newEmail
delete body['newEmail']
}
return await _request(this.fetch, 'POST', `${this.url}/admin/generate_link`, {
body: body,
headers: this.headers,
xform: _generateLinkResponse,
redirectTo: options?.redirectTo,
})
} catch (error) {
if (isAuthError(error)) {
return {
data: {
properties: null,
user: null,
},
error,
}
}
throw error
}
}
// User Admin API
/**
* Creates a new user.
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
async createUser(attributes: AdminUserAttributes): Promise<UserResponse> {
try {
return await _request(this.fetch, 'POST', `${this.url}/admin/users`, {
body: attributes,
headers: this.headers,
xform: _userResponse,
})
} catch (error) {
if (isAuthError(error)) {
return { data: { user: null }, error }
}
throw error
}
}
/**
* Get a list of users.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
* @param params An object which supports `page` and `perPage` as numbers, to alter the paginated results.
*/
async listUsers(
params?: PageParams
): Promise<
| { data: { users: User[]; aud: string } & Pagination; error: null }
| { data: { users: [] }; error: AuthError }
> {
try {
const pagination: Pagination = { nextPage: null, lastPage: 0, total: 0 }
const response = await _request(this.fetch, 'GET', `${this.url}/admin/users`, {
headers: this.headers,
noResolveJson: true,
query: {
page: params?.page?.toString() ?? '',
per_page: params?.perPage?.toString() ?? '',
},
xform: _noResolveJsonResponse,
})
if (response.error) throw response.error
const users = await response.json()
const total = response.headers.get('x-total-count') ?? 0
const links = response.headers.get('link')?.split(',') ?? []
if (links.length > 0) {
links.forEach((link: string) => {
const page = parseInt(link.split(';')[0].split('=')[1].substring(0, 1))
const rel = JSON.parse(link.split(';')[1].split('=')[1])
pagination[`${rel}Page`] = page
})
pagination.total = parseInt(total)
}
return { data: { ...users, ...pagination }, error: null }
} catch (error) {
if (isAuthError(error)) {
return { data: { users: [] }, error }
}
throw error
}
}
/**
* Get user by id.
*
* @param uid The user's unique identifier
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
async getUserById(uid: string): Promise<UserResponse> {
validateUUID(uid)
try {
return await _request(this.fetch, 'GET', `${this.url}/admin/users/${uid}`, {
headers: this.headers,
xform: _userResponse,
})
} catch (error) {
if (isAuthError(error)) {
return { data: { user: null }, error }
}
throw error
}
}
/**
* Updates the user data. Changes are applied directly without confirmation flows.
*
* @param uid The user's unique identifier
* @param attributes The data you want to update.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*
* @remarks
* **Important:** This is a server-side operation and does **not** trigger client-side
* `onAuthStateChange` listeners. The admin API has no connection to client state.
*
* To sync changes to the client after calling this method:
* 1. On the client, call `supabase.auth.refreshSession()` to fetch the updated user data
* 2. This will trigger the `TOKEN_REFRESHED` event and notify all listeners
*
* @example
* ```typescript
* // Server-side (Edge Function)
* const { data, error } = await supabase.auth.admin.updateUserById(
* userId,
* { user_metadata: { preferences: { theme: 'dark' } } }
* )
*
* // Client-side (to sync the changes)
* const { data, error } = await supabase.auth.refreshSession()
* // onAuthStateChange listeners will now be notified with updated user
* ```
*
* @see {@link GoTrueClient.refreshSession} for syncing admin changes to the client
* @see {@link GoTrueClient.updateUser} for client-side user updates (triggers listeners automatically)
*/
async updateUserById(uid: string, attributes: AdminUserAttributes): Promise<UserResponse> {
validateUUID(uid)
try {
return await _request(this.fetch, 'PUT', `${this.url}/admin/users/${uid}`, {
body: attributes,
headers: this.headers,
xform: _userResponse,
})
} catch (error) {
if (isAuthError(error)) {
return { data: { user: null }, error }
}
throw error
}
}
/**
* Delete a user. Requires a `service_role` key.
*
* @param id The user id you want to remove.
* @param shouldSoftDelete If true, then the user will be soft-deleted from the auth schema. Soft deletion allows user identification from the hashed user ID but is not reversible.
* Defaults to false for backward compatibility.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
async deleteUser(id: string, shouldSoftDelete = false): Promise<UserResponse> {
validateUUID(id)
try {
return await _request(this.fetch, 'DELETE', `${this.url}/admin/users/${id}`, {
headers: this.headers,
body: {
should_soft_delete: shouldSoftDelete,
},
xform: _userResponse,
})
} catch (error) {
if (isAuthError(error)) {
return { data: { user: null }, error }
}
throw error
}
}
private async _listFactors(
params: AuthMFAAdminListFactorsParams
): Promise<AuthMFAAdminListFactorsResponse> {
validateUUID(params.userId)
try {
const { data, error } = await _request(
this.fetch,
'GET',
`${this.url}/admin/users/${params.userId}/factors`,
{
headers: this.headers,
xform: (factors: any) => {
return { data: { factors }, error: null }
},
}
)
return { data, error }
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
private async _deleteFactor(
params: AuthMFAAdminDeleteFactorParams
): Promise<AuthMFAAdminDeleteFactorResponse> {
validateUUID(params.userId)
validateUUID(params.id)
try {
const data = await _request(
this.fetch,
'DELETE',
`${this.url}/admin/users/${params.userId}/factors/${params.id}`,
{
headers: this.headers,
}
)
return { data, error: null }
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Lists all OAuth clients with optional pagination.
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _listOAuthClients(params?: PageParams): Promise<OAuthClientListResponse> {
try {
const pagination: Pagination = { nextPage: null, lastPage: 0, total: 0 }
const response = await _request(this.fetch, 'GET', `${this.url}/admin/oauth/clients`, {
headers: this.headers,
noResolveJson: true,
query: {
page: params?.page?.toString() ?? '',
per_page: params?.perPage?.toString() ?? '',
},
xform: _noResolveJsonResponse,
})
if (response.error) throw response.error
const clients = await response.json()
const total = response.headers.get('x-total-count') ?? 0
const links = response.headers.get('link')?.split(',') ?? []
if (links.length > 0) {
links.forEach((link: string) => {
const page = parseInt(link.split(';')[0].split('=')[1].substring(0, 1))
const rel = JSON.parse(link.split(';')[1].split('=')[1])
pagination[`${rel}Page`] = page
})
pagination.total = parseInt(total)
}
return { data: { ...clients, ...pagination }, error: null }
} catch (error) {
if (isAuthError(error)) {
return { data: { clients: [] }, error }
}
throw error
}
}
/**
* Creates a new OAuth client.
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _createOAuthClient(params: CreateOAuthClientParams): Promise<OAuthClientResponse> {
try {
return await _request(this.fetch, 'POST', `${this.url}/admin/oauth/clients`, {
body: params,
headers: this.headers,
xform: (client: any) => {
return { data: client, error: null }
},
})
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Gets details of a specific OAuth client.
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _getOAuthClient(clientId: string): Promise<OAuthClientResponse> {
try {
return await _request(this.fetch, 'GET', `${this.url}/admin/oauth/clients/${clientId}`, {
headers: this.headers,
xform: (client: any) => {
return { data: client, error: null }
},
})
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Updates an existing OAuth client.
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _updateOAuthClient(
clientId: string,
params: UpdateOAuthClientParams
): Promise<OAuthClientResponse> {
try {
return await _request(this.fetch, 'PUT', `${this.url}/admin/oauth/clients/${clientId}`, {
body: params,
headers: this.headers,
xform: (client: any) => {
return { data: client, error: null }
},
})
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Deletes an OAuth client.
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _deleteOAuthClient(
clientId: string
): Promise<{ data: null; error: AuthError | null }> {
try {
await _request(this.fetch, 'DELETE', `${this.url}/admin/oauth/clients/${clientId}`, {
headers: this.headers,
noResolveJson: true,
})
return { data: null, error: null }
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Regenerates the secret for an OAuth client.
* Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _regenerateOAuthClientSecret(clientId: string): Promise<OAuthClientResponse> {
try {
return await _request(
this.fetch,
'POST',
`${this.url}/admin/oauth/clients/${clientId}/regenerate_secret`,
{
headers: this.headers,
xform: (client: any) => {
return { data: client, error: null }
},
}
)
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Lists all custom providers with optional type filter.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _listCustomProviders(
params?: ListCustomProvidersParams
): Promise<CustomProviderListResponse> {
try {
const query: Record<string, string> = {}
if (params?.type) {
query.type = params.type
}
return await _request(this.fetch, 'GET', `${this.url}/admin/custom-providers`, {
headers: this.headers,
query,
xform: (data: any) => {
return { data: { providers: data?.providers ?? [] }, error: null }
},
})
} catch (error) {
if (isAuthError(error)) {
return { data: { providers: [] }, error }
}
throw error
}
}
/**
* Creates a new custom OIDC/OAuth provider.
*
* For OIDC providers, the server fetches and validates the OpenID Connect discovery document
* from the issuer's well-known endpoint (or the provided `discovery_url`) at creation time.
* This may return a validation error (`error_code: "validation_failed"`) if the discovery
* document is unreachable, not valid JSON, missing required fields, or if the issuer
* in the document does not match the expected issuer.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _createCustomProvider(
params: CreateCustomProviderParams
): Promise<CustomProviderResponse> {
try {
return await _request(this.fetch, 'POST', `${this.url}/admin/custom-providers`, {
body: params,
headers: this.headers,
xform: (provider: any) => {
return { data: provider, error: null }
},
})
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Gets details of a specific custom provider by identifier.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _getCustomProvider(identifier: string): Promise<CustomProviderResponse> {
try {
return await _request(this.fetch, 'GET', `${this.url}/admin/custom-providers/${identifier}`, {
headers: this.headers,
xform: (provider: any) => {
return { data: provider, error: null }
},
})
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Updates an existing custom provider.
*
* When `issuer` or `discovery_url` is changed on an OIDC provider, the server re-fetches and
* validates the discovery document before persisting. This may return a validation error
* (`error_code: "validation_failed"`) if the discovery document is unreachable, invalid, or
* the issuer does not match.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _updateCustomProvider(
identifier: string,
params: UpdateCustomProviderParams
): Promise<CustomProviderResponse> {
try {
return await _request(this.fetch, 'PUT', `${this.url}/admin/custom-providers/${identifier}`, {
body: params,
headers: this.headers,
xform: (provider: any) => {
return { data: provider, error: null }
},
})
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Deletes a custom provider.
*
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
private async _deleteCustomProvider(
identifier: string
): Promise<{ data: null; error: AuthError | null }> {
try {
await _request(this.fetch, 'DELETE', `${this.url}/admin/custom-providers/${identifier}`, {
headers: this.headers,
noResolveJson: true,
})
return { data: null, error: null }
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
throw error
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
import GoTrueAdminApi from './GoTrueAdminApi'
import GoTrueClient from './GoTrueClient'
import AuthAdminApi from './AuthAdminApi'
import AuthClient from './AuthClient'
export { GoTrueAdminApi, GoTrueClient, AuthAdminApi, AuthClient }
export * from './lib/types'
export * from './lib/errors'
export {
navigatorLock,
NavigatorLockAcquireTimeoutError,
internals as lockInternals,
processLock,
} from './lib/locks'

View File

@@ -0,0 +1,308 @@
/**
* Avoid modifying this file. It's part of
* https://github.com/supabase-community/base64url-js. Submit all fixes on
* that repo!
*/
import { Uint8Array_ } from './webauthn.dom'
/**
* An array of characters that encode 6 bits into a Base64-URL alphabet
* character.
*/
const TO_BASE64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'.split('')
/**
* An array of characters that can appear in a Base64-URL encoded string but
* should be ignored.
*/
const IGNORE_BASE64URL = ' \t\n\r='.split('')
/**
* An array of 128 numbers that map a Base64-URL character to 6 bits, or if -2
* used to skip the character, or if -1 used to error out.
*/
const FROM_BASE64URL = (() => {
const charMap: number[] = new Array(128)
for (let i = 0; i < charMap.length; i += 1) {
charMap[i] = -1
}
for (let i = 0; i < IGNORE_BASE64URL.length; i += 1) {
charMap[IGNORE_BASE64URL[i].charCodeAt(0)] = -2
}
for (let i = 0; i < TO_BASE64URL.length; i += 1) {
charMap[TO_BASE64URL[i].charCodeAt(0)] = i
}
return charMap
})()
/**
* Converts a byte to a Base64-URL string.
*
* @param byte The byte to convert, or null to flush at the end of the byte sequence.
* @param state The Base64 conversion state. Pass an initial value of `{ queue: 0, queuedBits: 0 }`.
* @param emit A function called with the next Base64 character when ready.
*/
export function byteToBase64URL(
byte: number | null,
state: { queue: number; queuedBits: number },
emit: (char: string) => void
) {
if (byte !== null) {
state.queue = (state.queue << 8) | byte
state.queuedBits += 8
while (state.queuedBits >= 6) {
const pos = (state.queue >> (state.queuedBits - 6)) & 63
emit(TO_BASE64URL[pos])
state.queuedBits -= 6
}
} else if (state.queuedBits > 0) {
state.queue = state.queue << (6 - state.queuedBits)
state.queuedBits = 6
while (state.queuedBits >= 6) {
const pos = (state.queue >> (state.queuedBits - 6)) & 63
emit(TO_BASE64URL[pos])
state.queuedBits -= 6
}
}
}
/**
* Converts a String char code (extracted using `string.charCodeAt(position)`) to a sequence of Base64-URL characters.
*
* @param charCode The char code of the JavaScript string.
* @param state The Base64 state. Pass an initial value of `{ queue: 0, queuedBits: 0 }`.
* @param emit A function called with the next byte.
*/
export function byteFromBase64URL(
charCode: number,
state: { queue: number; queuedBits: number },
emit: (byte: number) => void
) {
const bits = FROM_BASE64URL[charCode]
if (bits > -1) {
// valid Base64-URL character
state.queue = (state.queue << 6) | bits
state.queuedBits += 6
while (state.queuedBits >= 8) {
emit((state.queue >> (state.queuedBits - 8)) & 0xff)
state.queuedBits -= 8
}
} else if (bits === -2) {
// ignore spaces, tabs, newlines, =
return
} else {
throw new Error(`Invalid Base64-URL character "${String.fromCharCode(charCode)}"`)
}
}
/**
* Converts a JavaScript string (which may include any valid character) into a
* Base64-URL encoded string. The string is first encoded in UTF-8 which is
* then encoded as Base64-URL.
*
* @param str The string to convert.
*/
export function stringToBase64URL(str: string) {
const base64: string[] = []
const emitter = (char: string) => {
base64.push(char)
}
const state = { queue: 0, queuedBits: 0 }
stringToUTF8(str, (byte: number) => {
byteToBase64URL(byte, state, emitter)
})
byteToBase64URL(null, state, emitter)
return base64.join('')
}
/**
* Converts a Base64-URL encoded string into a JavaScript string. It is assumed
* that the underlying string has been encoded as UTF-8.
*
* @param str The Base64-URL encoded string.
*/
export function stringFromBase64URL(str: string) {
const conv: string[] = []
const utf8Emit = (codepoint: number) => {
conv.push(String.fromCodePoint(codepoint))
}
const utf8State = {
utf8seq: 0,
codepoint: 0,
}
const b64State = { queue: 0, queuedBits: 0 }
const byteEmit = (byte: number) => {
stringFromUTF8(byte, utf8State, utf8Emit)
}
for (let i = 0; i < str.length; i += 1) {
byteFromBase64URL(str.charCodeAt(i), b64State, byteEmit)
}
return conv.join('')
}
/**
* Converts a Unicode codepoint to a multi-byte UTF-8 sequence.
*
* @param codepoint The Unicode codepoint.
* @param emit Function which will be called for each UTF-8 byte that represents the codepoint.
*/
export function codepointToUTF8(codepoint: number, emit: (byte: number) => void) {
if (codepoint <= 0x7f) {
emit(codepoint)
return
} else if (codepoint <= 0x7ff) {
emit(0xc0 | (codepoint >> 6))
emit(0x80 | (codepoint & 0x3f))
return
} else if (codepoint <= 0xffff) {
emit(0xe0 | (codepoint >> 12))
emit(0x80 | ((codepoint >> 6) & 0x3f))
emit(0x80 | (codepoint & 0x3f))
return
} else if (codepoint <= 0x10ffff) {
emit(0xf0 | (codepoint >> 18))
emit(0x80 | ((codepoint >> 12) & 0x3f))
emit(0x80 | ((codepoint >> 6) & 0x3f))
emit(0x80 | (codepoint & 0x3f))
return
}
throw new Error(`Unrecognized Unicode codepoint: ${codepoint.toString(16)}`)
}
/**
* Converts a JavaScript string to a sequence of UTF-8 bytes.
*
* @param str The string to convert to UTF-8.
* @param emit Function which will be called for each UTF-8 byte of the string.
*/
export function stringToUTF8(str: string, emit: (byte: number) => void) {
for (let i = 0; i < str.length; i += 1) {
let codepoint = str.charCodeAt(i)
if (codepoint > 0xd7ff && codepoint <= 0xdbff) {
// most UTF-16 codepoints are Unicode codepoints, except values in this
// range where the next UTF-16 codepoint needs to be combined with the
// current one to get the Unicode codepoint
const highSurrogate = ((codepoint - 0xd800) * 0x400) & 0xffff
const lowSurrogate = (str.charCodeAt(i + 1) - 0xdc00) & 0xffff
codepoint = (lowSurrogate | highSurrogate) + 0x10000
i += 1
}
codepointToUTF8(codepoint, emit)
}
}
/**
* Converts a UTF-8 byte to a Unicode codepoint.
*
* @param byte The UTF-8 byte next in the sequence.
* @param state The shared state between consecutive UTF-8 bytes in the
* sequence, an object with the shape `{ utf8seq: 0, codepoint: 0 }`.
* @param emit Function which will be called for each codepoint.
*/
export function stringFromUTF8(
byte: number,
state: { utf8seq: number; codepoint: number },
emit: (codepoint: number) => void
) {
if (state.utf8seq === 0) {
if (byte <= 0x7f) {
emit(byte)
return
}
// count the number of 1 leading bits until you reach 0
for (let leadingBit = 1; leadingBit < 6; leadingBit += 1) {
if (((byte >> (7 - leadingBit)) & 1) === 0) {
state.utf8seq = leadingBit
break
}
}
if (state.utf8seq === 2) {
state.codepoint = byte & 31
} else if (state.utf8seq === 3) {
state.codepoint = byte & 15
} else if (state.utf8seq === 4) {
state.codepoint = byte & 7
} else {
throw new Error('Invalid UTF-8 sequence')
}
state.utf8seq -= 1
} else if (state.utf8seq > 0) {
if (byte <= 0x7f) {
throw new Error('Invalid UTF-8 sequence')
}
state.codepoint = (state.codepoint << 6) | (byte & 63)
state.utf8seq -= 1
if (state.utf8seq === 0) {
emit(state.codepoint)
}
}
}
/**
* Helper functions to convert different types of strings to Uint8Array
*/
export function base64UrlToUint8Array(str: string): Uint8Array_ {
const result: number[] = []
const state = { queue: 0, queuedBits: 0 }
const onByte = (byte: number) => {
result.push(byte)
}
for (let i = 0; i < str.length; i += 1) {
byteFromBase64URL(str.charCodeAt(i), state, onByte)
}
return new Uint8Array(result)
}
export function stringToUint8Array(str: string): Uint8Array_ {
const result: number[] = []
stringToUTF8(str, (byte: number) => result.push(byte))
return new Uint8Array(result)
}
export function bytesToBase64URL(bytes: Uint8Array) {
const result: string[] = []
const state = { queue: 0, queuedBits: 0 }
const onChar = (char: string) => {
result.push(char)
}
bytes.forEach((byte) => byteToBase64URL(byte, state, onChar))
// always call with `null` after processing all bytes
byteToBase64URL(null, state, onChar)
return result.join('')
}

View File

@@ -0,0 +1,34 @@
import { version } from './version'
/** Current session will be checked for refresh at this interval. */
export const AUTO_REFRESH_TICK_DURATION_MS = 30 * 1000
/**
* A token refresh will be attempted this many ticks before the current session expires. */
export const AUTO_REFRESH_TICK_THRESHOLD = 3
/*
* Earliest time before an access token expires that the session should be refreshed.
*/
export const EXPIRY_MARGIN_MS = AUTO_REFRESH_TICK_THRESHOLD * AUTO_REFRESH_TICK_DURATION_MS
export const GOTRUE_URL = 'http://localhost:9999'
export const STORAGE_KEY = 'supabase.auth.token'
export const AUDIENCE = ''
export const DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version}` }
export const NETWORK_FAILURE = {
MAX_RETRIES: 10,
RETRY_INTERVAL: 2, // in deciseconds
}
export const API_VERSION_HEADER_NAME = 'X-Supabase-Api-Version'
export const API_VERSIONS = {
'2024-01-01': {
timestamp: Date.parse('2024-01-01T00:00:00.0Z'),
name: '2024-01-01',
},
}
export const BASE64URL_REGEX = /^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$/i
export const JWKS_TTL = 10 * 60 * 1000 // 10 minutes

View File

@@ -0,0 +1,90 @@
/**
* Known error codes. Note that the server may also return other error codes
* not included in this list (if the SDK is older than the version
* on the server).
*/
export type ErrorCode =
| 'unexpected_failure'
| 'validation_failed'
| 'bad_json'
| 'email_exists'
| 'phone_exists'
| 'bad_jwt'
| 'not_admin'
| 'no_authorization'
| 'user_not_found'
| 'session_not_found'
| 'session_expired'
| 'refresh_token_not_found'
| 'refresh_token_already_used'
| 'flow_state_not_found'
| 'flow_state_expired'
| 'signup_disabled'
| 'user_banned'
| 'provider_email_needs_verification'
| 'invite_not_found'
| 'bad_oauth_state'
| 'bad_oauth_callback'
| 'oauth_provider_not_supported'
| 'unexpected_audience'
| 'single_identity_not_deletable'
| 'email_conflict_identity_not_deletable'
| 'identity_already_exists'
| 'email_provider_disabled'
| 'phone_provider_disabled'
| 'too_many_enrolled_mfa_factors'
| 'mfa_factor_name_conflict'
| 'mfa_factor_not_found'
| 'mfa_ip_address_mismatch'
| 'mfa_challenge_expired'
| 'mfa_verification_failed'
| 'mfa_verification_rejected'
| 'insufficient_aal'
| 'captcha_failed'
| 'saml_provider_disabled'
| 'manual_linking_disabled'
| 'sms_send_failed'
| 'email_not_confirmed'
| 'phone_not_confirmed'
| 'reauth_nonce_missing'
| 'saml_relay_state_not_found'
| 'saml_relay_state_expired'
| 'saml_idp_not_found'
| 'saml_assertion_no_user_id'
| 'saml_assertion_no_email'
| 'user_already_exists'
| 'sso_provider_not_found'
| 'saml_metadata_fetch_failed'
| 'saml_idp_already_exists'
| 'sso_domain_already_exists'
| 'saml_entity_id_mismatch'
| 'conflict'
| 'provider_disabled'
| 'user_sso_managed'
| 'reauthentication_needed'
| 'same_password'
| 'reauthentication_not_valid'
| 'otp_expired'
| 'otp_disabled'
| 'identity_not_found'
| 'weak_password'
| 'over_request_rate_limit'
| 'over_email_send_rate_limit'
| 'over_sms_send_rate_limit'
| 'bad_code_verifier'
| 'anonymous_provider_disabled'
| 'hook_timeout'
| 'hook_timeout_after_retry'
| 'hook_payload_over_size_limit'
| 'hook_payload_invalid_content_type'
| 'request_timeout'
| 'mfa_phone_enroll_not_enabled'
| 'mfa_phone_verify_not_enabled'
| 'mfa_totp_enroll_not_enabled'
| 'mfa_totp_verify_not_enabled'
| 'mfa_webauthn_enroll_not_enabled'
| 'mfa_webauthn_verify_not_enabled'
| 'mfa_verified_factor_exists'
| 'invalid_credentials'
| 'email_address_not_authorized'
| 'email_address_invalid'

View File

@@ -0,0 +1,324 @@
import { WeakPasswordReasons } from './types'
import { ErrorCode } from './error-codes'
/**
* Base error thrown by Supabase Auth helpers.
*
* @example
* ```ts
* import { AuthError } from '@supabase/auth-js'
*
* throw new AuthError('Unexpected auth error', 500, 'unexpected')
* ```
*/
export class AuthError extends Error {
/**
* Error code associated with the error. Most errors coming from
* HTTP responses will have a code, though some errors that occur
* before a response is received will not have one present. In that
* case {@link #status} will also be undefined.
*/
code: ErrorCode | (string & {}) | undefined
/** HTTP status code that caused the error. */
status: number | undefined
protected __isAuthError = true
constructor(message: string, status?: number, code?: string) {
super(message)
this.name = 'AuthError'
this.status = status
this.code = code
}
}
export function isAuthError(error: unknown): error is AuthError {
return typeof error === 'object' && error !== null && '__isAuthError' in error
}
/**
* Error returned directly from the GoTrue REST API.
*
* @example
* ```ts
* import { AuthApiError } from '@supabase/auth-js'
*
* throw new AuthApiError('Invalid credentials', 400, 'invalid_credentials')
* ```
*/
export class AuthApiError extends AuthError {
status: number
constructor(message: string, status: number, code: string | undefined) {
super(message, status, code)
this.name = 'AuthApiError'
this.status = status
this.code = code
}
}
export function isAuthApiError(error: unknown): error is AuthApiError {
return isAuthError(error) && error.name === 'AuthApiError'
}
/**
* Wraps non-standard errors so callers can inspect the root cause.
*
* @example
* ```ts
* import { AuthUnknownError } from '@supabase/auth-js'
*
* try {
* await someAuthCall()
* } catch (err) {
* throw new AuthUnknownError('Auth failed', err)
* }
* ```
*/
export class AuthUnknownError extends AuthError {
originalError: unknown
constructor(message: string, originalError: unknown) {
super(message)
this.name = 'AuthUnknownError'
this.originalError = originalError
}
}
/**
* Flexible error class used to create named auth errors at runtime.
*
* @example
* ```ts
* import { CustomAuthError } from '@supabase/auth-js'
*
* throw new CustomAuthError('My custom auth error', 'MyAuthError', 400, 'custom_code')
* ```
*/
export class CustomAuthError extends AuthError {
name: string
status: number
constructor(message: string, name: string, status: number, code: string | undefined) {
super(message, status, code)
this.name = name
this.status = status
}
}
/**
* Error thrown when an operation requires a session but none is present.
*
* @example
* ```ts
* import { AuthSessionMissingError } from '@supabase/auth-js'
*
* throw new AuthSessionMissingError()
* ```
*/
export class AuthSessionMissingError extends CustomAuthError {
constructor() {
super('Auth session missing!', 'AuthSessionMissingError', 400, undefined)
}
}
export function isAuthSessionMissingError(error: any): error is AuthSessionMissingError {
return isAuthError(error) && error.name === 'AuthSessionMissingError'
}
/**
* Error thrown when the token response is malformed.
*
* @example
* ```ts
* import { AuthInvalidTokenResponseError } from '@supabase/auth-js'
*
* throw new AuthInvalidTokenResponseError()
* ```
*/
export class AuthInvalidTokenResponseError extends CustomAuthError {
constructor() {
super('Auth session or user missing', 'AuthInvalidTokenResponseError', 500, undefined)
}
}
/**
* Error thrown when email/password credentials are invalid.
*
* @example
* ```ts
* import { AuthInvalidCredentialsError } from '@supabase/auth-js'
*
* throw new AuthInvalidCredentialsError('Email or password is incorrect')
* ```
*/
export class AuthInvalidCredentialsError extends CustomAuthError {
constructor(message: string) {
super(message, 'AuthInvalidCredentialsError', 400, undefined)
}
}
/**
* Error thrown when implicit grant redirects contain an error.
*
* @example
* ```ts
* import { AuthImplicitGrantRedirectError } from '@supabase/auth-js'
*
* throw new AuthImplicitGrantRedirectError('OAuth redirect failed', {
* error: 'access_denied',
* code: 'oauth_error',
* })
* ```
*/
export class AuthImplicitGrantRedirectError extends CustomAuthError {
details: { error: string; code: string } | null = null
constructor(message: string, details: { error: string; code: string } | null = null) {
super(message, 'AuthImplicitGrantRedirectError', 500, undefined)
this.details = details
}
toJSON() {
return {
name: this.name,
message: this.message,
status: this.status,
details: this.details,
}
}
}
export function isAuthImplicitGrantRedirectError(
error: any
): error is AuthImplicitGrantRedirectError {
return isAuthError(error) && error.name === 'AuthImplicitGrantRedirectError'
}
/**
* Error thrown during PKCE code exchanges.
*
* @example
* ```ts
* import { AuthPKCEGrantCodeExchangeError } from '@supabase/auth-js'
*
* throw new AuthPKCEGrantCodeExchangeError('PKCE exchange failed')
* ```
*/
export class AuthPKCEGrantCodeExchangeError extends CustomAuthError {
details: { error: string; code: string } | null = null
constructor(message: string, details: { error: string; code: string } | null = null) {
super(message, 'AuthPKCEGrantCodeExchangeError', 500, undefined)
this.details = details
}
toJSON() {
return {
name: this.name,
message: this.message,
status: this.status,
details: this.details,
}
}
}
/**
* Error thrown when the PKCE code verifier is not found in storage.
* This typically happens when the auth flow was initiated in a different
* browser, device, or the storage was cleared.
*
* @example
* ```ts
* import { AuthPKCECodeVerifierMissingError } from '@supabase/auth-js'
*
* throw new AuthPKCECodeVerifierMissingError()
* ```
*/
export class AuthPKCECodeVerifierMissingError extends CustomAuthError {
constructor() {
super(
'PKCE code verifier not found in storage. ' +
'This can happen if the auth flow was initiated in a different browser or device, ' +
'or if the storage was cleared. For SSR frameworks (Next.js, SvelteKit, etc.), ' +
'use @supabase/ssr on both the server and client to store the code verifier in cookies.',
'AuthPKCECodeVerifierMissingError',
400,
'pkce_code_verifier_not_found'
)
}
}
export function isAuthPKCECodeVerifierMissingError(
error: unknown
): error is AuthPKCECodeVerifierMissingError {
return isAuthError(error) && error.name === 'AuthPKCECodeVerifierMissingError'
}
/**
* Error thrown when a transient fetch issue occurs.
*
* @example
* ```ts
* import { AuthRetryableFetchError } from '@supabase/auth-js'
*
* throw new AuthRetryableFetchError('Service temporarily unavailable', 503)
* ```
*/
export class AuthRetryableFetchError extends CustomAuthError {
constructor(message: string, status: number) {
super(message, 'AuthRetryableFetchError', status, undefined)
}
}
export function isAuthRetryableFetchError(error: unknown): error is AuthRetryableFetchError {
return isAuthError(error) && error.name === 'AuthRetryableFetchError'
}
/**
* This error is thrown on certain methods when the password used is deemed
* weak. Inspect the reasons to identify what password strength rules are
* inadequate.
*/
/**
* Error thrown when a supplied password is considered weak.
*
* @example
* ```ts
* import { AuthWeakPasswordError } from '@supabase/auth-js'
*
* throw new AuthWeakPasswordError('Password too short', 400, ['min_length'])
* ```
*/
export class AuthWeakPasswordError extends CustomAuthError {
/**
* Reasons why the password is deemed weak.
*/
reasons: WeakPasswordReasons[]
constructor(message: string, status: number, reasons: WeakPasswordReasons[]) {
super(message, 'AuthWeakPasswordError', status, 'weak_password')
this.reasons = reasons
}
}
export function isAuthWeakPasswordError(error: unknown): error is AuthWeakPasswordError {
return isAuthError(error) && error.name === 'AuthWeakPasswordError'
}
/**
* Error thrown when a JWT cannot be verified or parsed.
*
* @example
* ```ts
* import { AuthInvalidJwtError } from '@supabase/auth-js'
*
* throw new AuthInvalidJwtError('Token signature is invalid')
* ```
*/
export class AuthInvalidJwtError extends CustomAuthError {
constructor(message: string) {
super(message, 'AuthInvalidJwtError', 400, 'invalid_jwt')
}
}

View File

@@ -0,0 +1,283 @@
import { API_VERSIONS, API_VERSION_HEADER_NAME } from './constants'
import { expiresAt, looksLikeFetchResponse, parseResponseAPIVersion } from './helpers'
import {
AuthResponse,
AuthResponsePassword,
SSOResponse,
GenerateLinkProperties,
GenerateLinkResponse,
User,
UserResponse,
} from './types'
import {
AuthApiError,
AuthRetryableFetchError,
AuthWeakPasswordError,
AuthUnknownError,
AuthSessionMissingError,
} from './errors'
export type Fetch = typeof fetch
export interface FetchOptions {
headers?: {
[key: string]: string
}
noResolveJson?: boolean
}
export interface FetchParameters {
signal?: AbortSignal
}
export type RequestMethodType = 'GET' | 'POST' | 'PUT' | 'DELETE'
const _getErrorMessage = (err: any): string =>
err.msg || err.message || err.error_description || err.error || JSON.stringify(err)
const NETWORK_ERROR_CODES = [502, 503, 504]
export async function handleError(error: unknown) {
if (!looksLikeFetchResponse(error)) {
throw new AuthRetryableFetchError(_getErrorMessage(error), 0)
}
if (NETWORK_ERROR_CODES.includes(error.status)) {
// status in 500...599 range - server had an error, request might be retryed.
throw new AuthRetryableFetchError(_getErrorMessage(error), error.status)
}
let data: any
try {
data = await error.json()
} catch (e: any) {
throw new AuthUnknownError(_getErrorMessage(e), e)
}
let errorCode: string | undefined = undefined
const responseAPIVersion = parseResponseAPIVersion(error)
if (
responseAPIVersion &&
responseAPIVersion.getTime() >= API_VERSIONS['2024-01-01'].timestamp &&
typeof data === 'object' &&
data &&
typeof data.code === 'string'
) {
errorCode = data.code
} else if (typeof data === 'object' && data && typeof data.error_code === 'string') {
errorCode = data.error_code
}
if (!errorCode) {
// Legacy support for weak password errors, when there were no error codes
if (
typeof data === 'object' &&
data &&
typeof data.weak_password === 'object' &&
data.weak_password &&
Array.isArray(data.weak_password.reasons) &&
data.weak_password.reasons.length &&
data.weak_password.reasons.reduce((a: boolean, i: any) => a && typeof i === 'string', true)
) {
throw new AuthWeakPasswordError(
_getErrorMessage(data),
error.status,
data.weak_password.reasons
)
}
} else if (errorCode === 'weak_password') {
throw new AuthWeakPasswordError(
_getErrorMessage(data),
error.status,
data.weak_password?.reasons || []
)
} else if (errorCode === 'session_not_found') {
// The `session_id` inside the JWT does not correspond to a row in the
// `sessions` table. This usually means the user has signed out, has been
// deleted, or their session has somehow been terminated.
throw new AuthSessionMissingError()
}
throw new AuthApiError(_getErrorMessage(data), error.status || 500, errorCode)
}
const _getRequestParams = (
method: RequestMethodType,
options?: FetchOptions,
parameters?: FetchParameters,
body?: object
) => {
const params: { [k: string]: any } = { method, headers: options?.headers || {} }
if (method === 'GET') {
return params
}
params.headers = { 'Content-Type': 'application/json;charset=UTF-8', ...options?.headers }
params.body = JSON.stringify(body)
return { ...params, ...parameters }
}
interface GotrueRequestOptions extends FetchOptions {
jwt?: string
redirectTo?: string
body?: object
query?: { [key: string]: string }
/**
* Function that transforms api response from gotrue into a desirable / standardised format
*/
xform?: (data: any) => any
}
export async function _request(
fetcher: Fetch,
method: RequestMethodType,
url: string,
options?: GotrueRequestOptions
) {
const headers = {
...options?.headers,
}
if (!headers[API_VERSION_HEADER_NAME]) {
headers[API_VERSION_HEADER_NAME] = API_VERSIONS['2024-01-01'].name
}
if (options?.jwt) {
headers['Authorization'] = `Bearer ${options.jwt}`
}
const qs = options?.query ?? {}
if (options?.redirectTo) {
qs['redirect_to'] = options.redirectTo
}
const queryString = Object.keys(qs).length ? '?' + new URLSearchParams(qs).toString() : ''
const data = await _handleRequest(
fetcher,
method,
url + queryString,
{
headers,
noResolveJson: options?.noResolveJson,
},
{},
options?.body
)
return options?.xform ? options?.xform(data) : { data: { ...data }, error: null }
}
async function _handleRequest(
fetcher: Fetch,
method: RequestMethodType,
url: string,
options?: FetchOptions,
parameters?: FetchParameters,
body?: object
): Promise<any> {
const requestParams = _getRequestParams(method, options, parameters, body)
let result: any
try {
result = await fetcher(url, {
...requestParams,
})
} catch (e) {
console.error(e)
// fetch failed, likely due to a network or CORS error
throw new AuthRetryableFetchError(_getErrorMessage(e), 0)
}
if (!result.ok) {
await handleError(result)
}
if (options?.noResolveJson) {
return result
}
try {
return await result.json()
} catch (e: any) {
await handleError(e)
}
}
export function _sessionResponse(data: any): AuthResponse {
let session = null
if (hasSession(data)) {
session = { ...data }
if (!data.expires_at) {
session.expires_at = expiresAt(data.expires_in)
}
}
const user: User = data.user ?? (data as User)
return { data: { session, user }, error: null }
}
export function _sessionResponsePassword(data: any): AuthResponsePassword {
const response = _sessionResponse(data) as AuthResponsePassword
if (
!response.error &&
data.weak_password &&
typeof data.weak_password === 'object' &&
Array.isArray(data.weak_password.reasons) &&
data.weak_password.reasons.length &&
data.weak_password.message &&
typeof data.weak_password.message === 'string' &&
data.weak_password.reasons.reduce((a: boolean, i: any) => a && typeof i === 'string', true)
) {
response.data.weak_password = data.weak_password
}
return response
}
export function _userResponse(data: any): UserResponse {
const user: User = data.user ?? (data as User)
return { data: { user }, error: null }
}
export function _ssoResponse(data: any): SSOResponse {
return { data, error: null }
}
export function _generateLinkResponse(data: any): GenerateLinkResponse {
const { action_link, email_otp, hashed_token, redirect_to, verification_type, ...rest } = data
const properties: GenerateLinkProperties = {
action_link,
email_otp,
hashed_token,
redirect_to,
verification_type,
}
const user: User = { ...rest }
return {
data: {
properties,
user,
},
error: null,
}
}
export function _noResolveJsonResponse(data: any): Response {
return data
}
/**
* hasSession checks if the response object contains a valid session
* @param data A response object
* @returns true if a session is in the response
*/
function hasSession(data: any): boolean {
return data.access_token && data.refresh_token && data.expires_in
}

View File

@@ -0,0 +1,463 @@
import { API_VERSION_HEADER_NAME, BASE64URL_REGEX } from './constants'
import { AuthInvalidJwtError } from './errors'
import { base64UrlToUint8Array, stringFromBase64URL } from './base64url'
import { JwtHeader, JwtPayload, SupportedStorage, User } from './types'
import { Uint8Array_ } from './webauthn.dom'
export function expiresAt(expiresIn: number) {
const timeNow = Math.round(Date.now() / 1000)
return timeNow + expiresIn
}
/**
* Generates a unique identifier for internal callback subscriptions.
*
* This function uses JavaScript Symbols to create guaranteed-unique identifiers
* for auth state change callbacks. Symbols are ideal for this use case because:
* - They are guaranteed unique by the JavaScript runtime
* - They work in all environments (browser, SSR, Node.js)
* - They avoid issues with Next.js 16 deterministic rendering requirements
* - They are perfect for internal, non-serializable identifiers
*
* Note: This function is only used for internal subscription management,
* not for security-critical operations like session tokens.
*/
export function generateCallbackId(): symbol {
return Symbol('auth-callback')
}
export const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined'
const localStorageWriteTests = {
tested: false,
writable: false,
}
/**
* Checks whether localStorage is supported on this browser.
*/
export const supportsLocalStorage = () => {
if (!isBrowser()) {
return false
}
try {
if (typeof globalThis.localStorage !== 'object') {
return false
}
} catch (e) {
// DOM exception when accessing `localStorage`
return false
}
if (localStorageWriteTests.tested) {
return localStorageWriteTests.writable
}
const randomKey = `lswt-${Math.random()}${Math.random()}`
try {
globalThis.localStorage.setItem(randomKey, randomKey)
globalThis.localStorage.removeItem(randomKey)
localStorageWriteTests.tested = true
localStorageWriteTests.writable = true
} catch (e) {
// localStorage can't be written to
// 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
localStorageWriteTests.tested = true
localStorageWriteTests.writable = false
}
return localStorageWriteTests.writable
}
/**
* Extracts parameters encoded in the URL both in the query and fragment.
*/
export function parseParametersFromURL(href: string) {
const result: { [parameter: string]: string } = {}
const url = new URL(href)
if (url.hash && url.hash[0] === '#') {
try {
const hashSearchParams = new URLSearchParams(url.hash.substring(1))
hashSearchParams.forEach((value, key) => {
result[key] = value
})
} catch (e: any) {
// hash is not a query string
}
}
// search parameters take precedence over hash parameters
url.searchParams.forEach((value, key) => {
result[key] = value
})
return result
}
type Fetch = typeof fetch
export const resolveFetch = (customFetch?: Fetch): Fetch => {
if (customFetch) {
return (...args) => customFetch(...args)
}
return (...args) => fetch(...args)
}
export const looksLikeFetchResponse = (maybeResponse: unknown): maybeResponse is Response => {
return (
typeof maybeResponse === 'object' &&
maybeResponse !== null &&
'status' in maybeResponse &&
'ok' in maybeResponse &&
'json' in maybeResponse &&
typeof (maybeResponse as any).json === 'function'
)
}
// Storage helpers
export const setItemAsync = async (
storage: SupportedStorage,
key: string,
data: any
): Promise<void> => {
await storage.setItem(key, JSON.stringify(data))
}
export const getItemAsync = async (storage: SupportedStorage, key: string): Promise<unknown> => {
const value = await storage.getItem(key)
if (!value) {
return null
}
try {
return JSON.parse(value)
} catch {
return value
}
}
export const removeItemAsync = async (storage: SupportedStorage, key: string): Promise<void> => {
await storage.removeItem(key)
}
/**
* A deferred represents some asynchronous work that is not yet finished, which
* may or may not culminate in a value.
* Taken from: https://github.com/mike-north/types/blob/master/src/async.ts
*/
export class Deferred<T = any> {
public static promiseConstructor: PromiseConstructor = Promise
public readonly promise!: PromiseLike<T>
public readonly resolve!: (value?: T | PromiseLike<T>) => void
public readonly reject!: (reason?: any) => any
public constructor() {
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;(this as any).promise = new Deferred.promiseConstructor((res, rej) => {
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;(this as any).resolve = res
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;(this as any).reject = rej
})
}
}
export function decodeJWT(token: string): {
header: JwtHeader
payload: JwtPayload
signature: Uint8Array_
raw: {
header: string
payload: string
}
} {
const parts = token.split('.')
if (parts.length !== 3) {
throw new AuthInvalidJwtError('Invalid JWT structure')
}
// Regex checks for base64url format
for (let i = 0; i < parts.length; i++) {
if (!BASE64URL_REGEX.test(parts[i] as string)) {
throw new AuthInvalidJwtError('JWT not in base64url format')
}
}
const data = {
// using base64url lib
header: JSON.parse(stringFromBase64URL(parts[0])),
payload: JSON.parse(stringFromBase64URL(parts[1])),
signature: base64UrlToUint8Array(parts[2]),
raw: {
header: parts[0],
payload: parts[1],
},
}
return data
}
/**
* Creates a promise that resolves to null after some time.
*/
export async function sleep(time: number): Promise<null> {
return await new Promise((accept) => {
setTimeout(() => accept(null), time)
})
}
/**
* Converts the provided async function into a retryable function. Each result
* or thrown error is sent to the isRetryable function which should return true
* if the function should run again.
*/
export function retryable<T>(
fn: (attempt: number) => Promise<T>,
isRetryable: (attempt: number, error: any | null, result?: T) => boolean
): Promise<T> {
const promise = new Promise<T>((accept, reject) => {
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;(async () => {
for (let attempt = 0; attempt < Infinity; attempt++) {
try {
const result = await fn(attempt)
if (!isRetryable(attempt, null, result)) {
accept(result)
return
}
} catch (e: any) {
if (!isRetryable(attempt, e)) {
reject(e)
return
}
}
}
})()
})
return promise
}
function dec2hex(dec: number) {
return ('0' + dec.toString(16)).substr(-2)
}
// Functions below taken from: https://stackoverflow.com/questions/63309409/creating-a-code-verifier-and-challenge-for-pkce-auth-on-spotify-api-in-reactjs
export function generatePKCEVerifier() {
const verifierLength = 56
const array = new Uint32Array(verifierLength)
if (typeof crypto === 'undefined') {
const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
const charSetLen = charSet.length
let verifier = ''
for (let i = 0; i < verifierLength; i++) {
verifier += charSet.charAt(Math.floor(Math.random() * charSetLen))
}
return verifier
}
crypto.getRandomValues(array)
return Array.from(array, dec2hex).join('')
}
async function sha256(randomString: string) {
const encoder = new TextEncoder()
const encodedData = encoder.encode(randomString)
const hash = await crypto.subtle.digest('SHA-256', encodedData)
const bytes = new Uint8Array(hash)
return Array.from(bytes)
.map((c) => String.fromCharCode(c))
.join('')
}
export async function generatePKCEChallenge(verifier: string) {
const hasCryptoSupport =
typeof crypto !== 'undefined' &&
typeof crypto.subtle !== 'undefined' &&
typeof TextEncoder !== 'undefined'
if (!hasCryptoSupport) {
console.warn(
'WebCrypto API is not supported. Code challenge method will default to use plain instead of sha256.'
)
return verifier
}
const hashed = await sha256(verifier)
return btoa(hashed).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
export async function getCodeChallengeAndMethod(
storage: SupportedStorage,
storageKey: string,
isPasswordRecovery = false
) {
const codeVerifier = generatePKCEVerifier()
let storedCodeVerifier = codeVerifier
if (isPasswordRecovery) {
storedCodeVerifier += '/PASSWORD_RECOVERY'
}
await setItemAsync(storage, `${storageKey}-code-verifier`, storedCodeVerifier)
const codeChallenge = await generatePKCEChallenge(codeVerifier)
const codeChallengeMethod = codeVerifier === codeChallenge ? 'plain' : 's256'
return [codeChallenge, codeChallengeMethod]
}
/** Parses the API version which is 2YYY-MM-DD. */
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
export function parseResponseAPIVersion(response: Response) {
const apiVersion = response.headers.get(API_VERSION_HEADER_NAME)
if (!apiVersion) {
return null
}
if (!apiVersion.match(API_VERSION_REGEX)) {
return null
}
try {
const date = new Date(`${apiVersion}T00:00:00.0Z`)
return date
} catch (e: any) {
return null
}
}
export function validateExp(exp: number) {
if (!exp) {
throw new Error('Missing exp claim')
}
const timeNow = Math.floor(Date.now() / 1000)
if (exp <= timeNow) {
throw new Error('JWT has expired')
}
}
export function getAlgorithm(
alg: 'HS256' | 'RS256' | 'ES256'
): RsaHashedImportParams | EcKeyImportParams {
switch (alg) {
case 'RS256':
return {
name: 'RSASSA-PKCS1-v1_5',
hash: { name: 'SHA-256' },
}
case 'ES256':
return {
name: 'ECDSA',
namedCurve: 'P-256',
hash: { name: 'SHA-256' },
}
default:
throw new Error('Invalid alg claim')
}
}
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
export function validateUUID(str: string) {
if (!UUID_REGEX.test(str)) {
throw new Error('@supabase/auth-js: Expected parameter to be UUID but is not')
}
}
export function userNotAvailableProxy(): User {
const proxyTarget = {} as User
return new Proxy(proxyTarget, {
get: (target: any, prop: string) => {
if (prop === '__isUserNotAvailableProxy') {
return true
}
// Preventative check for common problematic symbols during cloning/inspection
// These symbols might be accessed by structuredClone or other internal mechanisms.
if (typeof prop === 'symbol') {
const sProp = (prop as symbol).toString()
if (
sProp === 'Symbol(Symbol.toPrimitive)' ||
sProp === 'Symbol(Symbol.toStringTag)' ||
sProp === 'Symbol(util.inspect.custom)'
) {
// Node.js util.inspect
return undefined
}
}
throw new Error(
`@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.`
)
},
set: (_target: any, prop: string) => {
throw new Error(
`@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.`
)
},
deleteProperty: (_target: any, prop: string) => {
throw new Error(
`@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.`
)
},
})
}
/**
* Creates a proxy around a user object that warns when properties are accessed on the server.
* This is used to alert developers that using user data from getSession() on the server is insecure.
*
* @param user The actual user object to wrap
* @param suppressWarningRef An object with a 'value' property that controls warning suppression
* @returns A proxied user object that warns on property access
*/
export function insecureUserWarningProxy(user: User, suppressWarningRef: { value: boolean }): User {
return new Proxy(user, {
get: (target: any, prop: string | symbol, receiver: any) => {
// Allow internal checks without warning
if (prop === '__isInsecureUserWarningProxy') {
return true
}
// Preventative check for common problematic symbols during cloning/inspection
// These symbols might be accessed by structuredClone or other internal mechanisms
if (typeof prop === 'symbol') {
const sProp = prop.toString()
if (
sProp === 'Symbol(Symbol.toPrimitive)' ||
sProp === 'Symbol(Symbol.toStringTag)' ||
sProp === 'Symbol(util.inspect.custom)' ||
sProp === 'Symbol(nodejs.util.inspect.custom)'
) {
// Return the actual value for these symbols to allow proper inspection
return Reflect.get(target, prop, receiver)
}
}
// Emit warning on first property access
if (!suppressWarningRef.value && typeof prop === 'string') {
console.warn(
'Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and may not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.'
)
suppressWarningRef.value = true
}
return Reflect.get(target, prop, receiver)
},
})
}
/**
* Deep clones a JSON-serializable object using JSON.parse(JSON.stringify(obj)).
* Note: Only works for JSON-safe data.
*/
export function deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj))
}

View File

@@ -0,0 +1,21 @@
import { SupportedStorage } from './types'
/**
* Returns a localStorage-like object that stores the key-value pairs in
* memory.
*/
export function memoryLocalStorageAdapter(store: { [key: string]: string } = {}): SupportedStorage {
return {
getItem: (key) => {
return store[key] || null
},
setItem: (key, value) => {
store[key] = value
},
removeItem: (key) => {
delete store[key]
},
}
}

View File

@@ -0,0 +1,375 @@
import { supportsLocalStorage } from './helpers'
/**
* @experimental
*/
export const internals = {
/**
* @experimental
*/
debug: !!(
globalThis &&
supportsLocalStorage() &&
globalThis.localStorage &&
globalThis.localStorage.getItem('supabase.gotrue-js.locks.debug') === 'true'
),
}
/**
* An error thrown when a lock cannot be acquired after some amount of time.
*
* Use the {@link #isAcquireTimeout} property instead of checking with `instanceof`.
*
* @example
* ```ts
* import { LockAcquireTimeoutError } from '@supabase/auth-js'
*
* class CustomLockError extends LockAcquireTimeoutError {
* constructor() {
* super('Lock timed out')
* }
* }
* ```
*/
export abstract class LockAcquireTimeoutError extends Error {
public readonly isAcquireTimeout = true
constructor(message: string) {
super(message)
}
}
/**
* Error thrown when the browser Navigator Lock API fails to acquire a lock.
*
* @example
* ```ts
* import { NavigatorLockAcquireTimeoutError } from '@supabase/auth-js'
*
* throw new NavigatorLockAcquireTimeoutError('Lock timed out')
* ```
*/
export class NavigatorLockAcquireTimeoutError extends LockAcquireTimeoutError {}
/**
* Error thrown when the process-level lock helper cannot acquire a lock.
*
* @example
* ```ts
* import { ProcessLockAcquireTimeoutError } from '@supabase/auth-js'
*
* throw new ProcessLockAcquireTimeoutError('Lock timed out')
* ```
*/
export class ProcessLockAcquireTimeoutError extends LockAcquireTimeoutError {}
/**
* Implements a global exclusive lock using the Navigator LockManager API. It
* is available on all browsers released after 2022-03-15 with Safari being the
* last one to release support. If the API is not available, this function will
* throw. Make sure you check availablility before configuring {@link
* GoTrueClient}.
*
* You can turn on debugging by setting the `supabase.gotrue-js.locks.debug`
* local storage item to `true`.
*
* Internals:
*
* Since the LockManager API does not preserve stack traces for the async
* function passed in the `request` method, a trick is used where acquiring the
* lock releases a previously started promise to run the operation in the `fn`
* function. The lock waits for that promise to finish (with or without error),
* while the function will finally wait for the result anyway.
*
* @param name Name of the lock to be acquired.
* @param acquireTimeout If negative, no timeout. If 0 an error is thrown if
* the lock can't be acquired without waiting. If positive, the lock acquire
* will time out after so many milliseconds. An error is
* a timeout if it has `isAcquireTimeout` set to true.
* @param fn The operation to run once the lock is acquired.
* @example
* ```ts
* await navigatorLock('sync-user', 1000, async () => {
* await refreshSession()
* })
* ```
*/
export async function navigatorLock<R>(
name: string,
acquireTimeout: number,
fn: () => Promise<R>
): Promise<R> {
if (internals.debug) {
console.log('@supabase/gotrue-js: navigatorLock: acquire lock', name, acquireTimeout)
}
const abortController = new globalThis.AbortController()
if (acquireTimeout > 0) {
setTimeout(() => {
abortController.abort()
if (internals.debug) {
console.log('@supabase/gotrue-js: navigatorLock acquire timed out', name)
}
}, acquireTimeout)
}
// MDN article: https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request
// Wrapping navigator.locks.request() with a plain Promise is done as some
// libraries like zone.js patch the Promise object to track the execution
// context. However, it appears that most browsers use an internal promise
// implementation when using the navigator.locks.request() API causing them
// to lose context and emit confusing log messages or break certain features.
// This wrapping is believed to help zone.js track the execution context
// better.
await Promise.resolve()
try {
return await globalThis.navigator.locks.request(
name,
acquireTimeout === 0
? {
mode: 'exclusive',
ifAvailable: true,
}
: {
mode: 'exclusive',
signal: abortController.signal,
},
async (lock) => {
if (lock) {
if (internals.debug) {
console.log('@supabase/gotrue-js: navigatorLock: acquired', name, lock.name)
}
try {
return await fn()
} finally {
if (internals.debug) {
console.log('@supabase/gotrue-js: navigatorLock: released', name, lock.name)
}
}
} else {
if (acquireTimeout === 0) {
if (internals.debug) {
console.log('@supabase/gotrue-js: navigatorLock: not immediately available', name)
}
throw new NavigatorLockAcquireTimeoutError(
`Acquiring an exclusive Navigator LockManager lock "${name}" immediately failed`
)
} else {
if (internals.debug) {
try {
const result = await globalThis.navigator.locks.query()
console.log(
'@supabase/gotrue-js: Navigator LockManager state',
JSON.stringify(result, null, ' ')
)
} catch (e: any) {
console.warn(
'@supabase/gotrue-js: Error when querying Navigator LockManager state',
e
)
}
}
// Browser is not following the Navigator LockManager spec, it
// returned a null lock when we didn't use ifAvailable. So we can
// pretend the lock is acquired in the name of backward compatibility
// and user experience and just run the function.
console.warn(
'@supabase/gotrue-js: Navigator LockManager returned a null lock when using #request without ifAvailable set to true, it appears this browser is not following the LockManager spec https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request'
)
return await fn()
}
}
}
)
} catch (e: any) {
if (e?.name === 'AbortError' && acquireTimeout > 0) {
// The lock acquisition was aborted because the timeout fired while the
// request was still pending. This typically means another lock holder is
// not releasing the lock, possibly due to React Strict Mode's
// double-mount/unmount behavior or a component unmounting mid-operation,
// leaving an orphaned lock.
//
// Recovery: use { steal: true } to forcefully acquire the lock. Per the
// Web Locks API spec, this releases any currently held lock with the same
// name and grants the request immediately, preempting any queued requests.
// The previous holder's callback continues running to completion but no
// longer holds the lock for exclusion purposes.
//
// See: https://github.com/supabase/supabase/issues/42505
if (internals.debug) {
console.log(
'@supabase/gotrue-js: navigatorLock: acquire timeout, recovering by stealing lock',
name
)
}
console.warn(
`@supabase/gotrue-js: Lock "${name}" was not released within ${acquireTimeout}ms. ` +
'This may indicate an orphaned lock from a component unmount (e.g., React Strict Mode). ' +
'Forcefully acquiring the lock to recover.'
)
return await Promise.resolve().then(() =>
globalThis.navigator.locks.request(
name,
{
mode: 'exclusive',
steal: true,
},
async (lock) => {
if (lock) {
if (internals.debug) {
console.log(
'@supabase/gotrue-js: navigatorLock: recovered (stolen)',
name,
lock.name
)
}
try {
return await fn()
} finally {
if (internals.debug) {
console.log(
'@supabase/gotrue-js: navigatorLock: released (stolen)',
name,
lock.name
)
}
}
} else {
// This should not happen with steal: true, but handle gracefully.
console.warn(
'@supabase/gotrue-js: Navigator LockManager returned null lock even with steal: true'
)
return await fn()
}
}
)
)
}
throw e
}
}
const PROCESS_LOCKS: { [name: string]: Promise<any> } = {}
/**
* Implements a global exclusive lock that works only in the current process.
* Useful for environments like React Native or other non-browser
* single-process (i.e. no concept of "tabs") environments.
*
* Use {@link #navigatorLock} in browser environments.
*
* @param name Name of the lock to be acquired.
* @param acquireTimeout If negative, no timeout. If 0 an error is thrown if
* the lock can't be acquired without waiting. If positive, the lock acquire
* will time out after so many milliseconds. An error is
* a timeout if it has `isAcquireTimeout` set to true.
* @param fn The operation to run once the lock is acquired.
* @example
* ```ts
* await processLock('migrate', 5000, async () => {
* await runMigration()
* })
* ```
*/
export async function processLock<R>(
name: string,
acquireTimeout: number,
fn: () => Promise<R>
): Promise<R> {
const previousOperation = PROCESS_LOCKS[name] ?? Promise.resolve()
// Wrap previousOperation to handle errors without using .catch()
// This avoids Firefox content script security errors
const previousOperationHandled = (async () => {
try {
await previousOperation
return null
} catch (e) {
// ignore error of previous operation that we're waiting to finish
return null
}
})()
const currentOperation = (async () => {
let timeoutId: ReturnType<typeof setTimeout> | null = null
try {
// Wait for either previous operation or timeout
const timeoutPromise =
acquireTimeout >= 0
? new Promise((_, reject) => {
timeoutId = setTimeout(() => {
console.warn(
`@supabase/gotrue-js: Lock "${name}" acquisition timed out after ${acquireTimeout}ms. ` +
'This may be caused by another operation holding the lock. ' +
'Consider increasing lockAcquireTimeout or checking for stuck operations.'
)
reject(
new ProcessLockAcquireTimeoutError(
`Acquiring process lock with name "${name}" timed out`
)
)
}, acquireTimeout)
})
: null
await Promise.race([previousOperationHandled, timeoutPromise].filter((x) => x))
// If we reach here, previousOperationHandled won the race
// Clear the timeout to prevent false warnings
if (timeoutId !== null) {
clearTimeout(timeoutId)
}
} catch (e: any) {
// Clear the timeout on error path as well
if (timeoutId !== null) {
clearTimeout(timeoutId)
}
// Re-throw timeout errors, ignore others
if (e && e.isAcquireTimeout) {
throw e
}
// Fall through to run fn() - previous operation finished with error
}
// Previous operations finished and we didn't get a race on the acquire
// timeout, so the current operation can finally start
return await fn()
})()
PROCESS_LOCKS[name] = (async () => {
try {
return await currentOperation
} catch (e: any) {
if (e && e.isAcquireTimeout) {
// if the current operation timed out, it doesn't mean that the previous
// operation finished, so we need continue waiting for it to finish
try {
await previousOperation
} catch (prevError) {
// Ignore previous operation errors
}
return null
}
throw e
}
})()
// finally wait for the current operation to finish successfully, with an
// error or with an acquire timeout error
return await currentOperation
}

View File

@@ -0,0 +1,23 @@
/**
* https://mathiasbynens.be/notes/globalthis
*/
export function polyfillGlobalThis() {
if (typeof globalThis === 'object') return
try {
Object.defineProperty(Object.prototype, '__magic__', {
get: function () {
return this
},
configurable: true,
})
// @ts-expect-error 'Allow access to magic'
__magic__.globalThis = __magic__
// @ts-expect-error 'Allow access to magic'
delete Object.prototype.__magic__
} catch (e) {
if (typeof self !== 'undefined') {
// @ts-expect-error 'Allow access to globals'
self.globalThis = self
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
// Generated automatically during releases by scripts/update-version-files.ts
// This file provides runtime access to the package version for:
// - HTTP request headers (e.g., X-Client-Info header for API requests)
// - Debugging and support (identifying which version is running)
// - Telemetry and logging (version reporting in errors/analytics)
// - Ensuring build artifacts match the published package version
export const version = '2.99.1'

View File

@@ -0,0 +1,184 @@
// types and functions copied over from viem so this library doesn't depend on it
export type Hex = `0x${string}`
export type Address = Hex
export type EIP1193EventMap = {
accountsChanged(accounts: Address[]): void
chainChanged(chainId: string): void
connect(connectInfo: { chainId: string }): void
disconnect(error: { code: number; message: string }): void
message(message: { type: string; data: unknown }): void
}
export type EIP1193Events = {
on<event extends keyof EIP1193EventMap>(event: event, listener: EIP1193EventMap[event]): void
removeListener<event extends keyof EIP1193EventMap>(
event: event,
listener: EIP1193EventMap[event]
): void
}
export type EIP1193RequestFn = (args: { method: string; params?: unknown }) => Promise<unknown>
export type EIP1193Provider = EIP1193Events & {
address: string
request: EIP1193RequestFn
}
export type EthereumWallet = EIP1193Provider
/**
* EIP-4361 message fields
*/
export type SiweMessage = {
/**
* The Ethereum address performing the signing.
*/
address: Address
/**
* The [EIP-155](https://eips.ethereum.org/EIPS/eip-155) Chain ID to which the session is bound,
*/
chainId: number
/**
* [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) authority that is requesting the signing.
*/
domain: string
/**
* Time when the signed authentication message is no longer valid.
*/
expirationTime?: Date | undefined
/**
* Time when the message was generated, typically the current time.
*/
issuedAt?: Date | undefined
/**
* A random string typically chosen by the relying party and used to prevent replay attacks.
*/
nonce?: string
/**
* Time when the signed authentication message will become valid.
*/
notBefore?: Date | undefined
/**
* A system-specific identifier that may be used to uniquely refer to the sign-in request.
*/
requestId?: string | undefined
/**
* A list of information or references to information the user wishes to have resolved as part of authentication by the relying party.
*/
resources?: string[] | undefined
/**
* [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) URI scheme of the origin of the request.
*/
scheme?: string | undefined
/**
* A human-readable ASCII assertion that the user will sign.
*/
statement?: string | undefined
/**
* [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) URI referring to the resource that is the subject of the signing (as in the subject of a claim).
*/
uri: string
/**
* The current version of the SIWE Message.
*/
version: '1'
}
export type EthereumSignInInput = SiweMessage
export function getAddress(address: string): Address {
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
throw new Error(`@supabase/auth-js: Address "${address}" is invalid.`)
}
return address.toLowerCase() as Address
}
export function fromHex(hex: Hex): number {
return parseInt(hex, 16)
}
export function toHex(value: string): Hex {
const bytes = new TextEncoder().encode(value)
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
return ('0x' + hex) as Hex
}
/**
* Creates EIP-4361 formatted message.
*/
export function createSiweMessage(parameters: SiweMessage): string {
const {
chainId,
domain,
expirationTime,
issuedAt = new Date(),
nonce,
notBefore,
requestId,
resources,
scheme,
uri,
version,
} = parameters
// Validate fields
{
if (!Number.isInteger(chainId))
throw new Error(
`@supabase/auth-js: Invalid SIWE message field "chainId". Chain ID must be a EIP-155 chain ID. Provided value: ${chainId}`
)
if (!domain)
throw new Error(
`@supabase/auth-js: Invalid SIWE message field "domain". Domain must be provided.`
)
if (nonce && nonce.length < 8)
throw new Error(
`@supabase/auth-js: Invalid SIWE message field "nonce". Nonce must be at least 8 characters. Provided value: ${nonce}`
)
if (!uri)
throw new Error(`@supabase/auth-js: Invalid SIWE message field "uri". URI must be provided.`)
if (version !== '1')
throw new Error(
`@supabase/auth-js: Invalid SIWE message field "version". Version must be '1'. Provided value: ${version}`
)
if (parameters.statement?.includes('\n'))
throw new Error(
`@supabase/auth-js: Invalid SIWE message field "statement". Statement must not include '\\n'. Provided value: ${parameters.statement}`
)
}
// Construct message
const address = getAddress(parameters.address)
const origin = scheme ? `${scheme}://${domain}` : domain
const statement = parameters.statement ? `${parameters.statement}\n` : ''
const prefix = `${origin} wants you to sign in with your Ethereum account:\n${address}\n\n${statement}`
let suffix = `URI: ${uri}\nVersion: ${version}\nChain ID: ${chainId}${
nonce ? `\nNonce: ${nonce}` : ''
}\nIssued At: ${issuedAt.toISOString()}`
if (expirationTime) suffix += `\nExpiration Time: ${expirationTime.toISOString()}`
if (notBefore) suffix += `\nNot Before: ${notBefore.toISOString()}`
if (requestId) suffix += `\nRequest ID: ${requestId}`
if (resources) {
let content = '\nResources:'
for (const resource of resources) {
if (!resource || typeof resource !== 'string')
throw new Error(
`@supabase/auth-js: Invalid SIWE message field "resources". Every resource must be a valid string. Provided value: ${resource}`
)
content += `\n- ${resource}`
}
suffix += content
}
return `${prefix}\n${suffix}`
}

View File

@@ -0,0 +1,186 @@
// types copied over from @solana/wallet-standard-features and @wallet-standard/base so this library doesn't depend on them
/**
* A namespaced identifier in the format `${namespace}:${reference}`.
*
* Used by {@link IdentifierArray} and {@link IdentifierRecord}.
*
* @group Identifier
*/
export type IdentifierString = `${string}:${string}`
/**
* A read-only array of namespaced identifiers in the format `${namespace}:${reference}`.
*
* Used by {@link Wallet.chains | Wallet::chains}, {@link WalletAccount.chains | WalletAccount::chains}, and
* {@link WalletAccount.features | WalletAccount::features}.
*
* @group Identifier
*/
export type IdentifierArray = readonly IdentifierString[]
/**
* Version of the Wallet Standard implemented by a {@link Wallet}.
*
* Used by {@link Wallet.version | Wallet::version}.
*
* Note that this is _NOT_ a version of the Wallet, but a version of the Wallet Standard itself that the Wallet
* supports.
*
* This may be used by the app to determine compatibility and feature detect.
*
* @group Wallet
*/
export type WalletVersion = '1.0.0'
/**
* A data URI containing a base64-encoded SVG, WebP, PNG, or GIF image.
*
* Used by {@link Wallet.icon | Wallet::icon} and {@link WalletAccount.icon | WalletAccount::icon}.
*
* @group Wallet
*/
export type WalletIcon = `data:image/${'svg+xml' | 'webp' | 'png' | 'gif'};base64,${string}`
/**
* Interface of a **WalletAccount**, also referred to as an **Account**.
*
* An account is a _read-only data object_ that is provided from the Wallet to the app, authorizing the app to use it.
*
* The app can use an account to display and query information from a chain.
*
* The app can also act using an account by passing it to {@link Wallet.features | features} of the Wallet.
*
* Wallets may use or extend {@link "@wallet-standard/wallet".ReadonlyWalletAccount} which implements this interface.
*
* @group Wallet
*/
export interface WalletAccount {
/** Address of the account, corresponding with a public key. */
readonly address: string
/** Public key of the account, corresponding with a secret key to use. */
readonly publicKey: Uint8Array
/**
* Chains supported by the account.
*
* This must be a subset of the {@link Wallet.chains | chains} of the Wallet.
*/
readonly chains: IdentifierArray
/**
* Feature names supported by the account.
*
* This must be a subset of the names of {@link Wallet.features | features} of the Wallet.
*/
readonly features: IdentifierArray
/** Optional user-friendly descriptive label or name for the account. This may be displayed by the app. */
readonly label?: string
/** Optional user-friendly icon for the account. This may be displayed by the app. */
readonly icon?: WalletIcon
}
/** Input for signing in. */
export interface SolanaSignInInput {
/**
* Optional EIP-4361 Domain.
* If not provided, the wallet must determine the Domain to include in the message.
*/
readonly domain?: string
/**
* Optional EIP-4361 Address.
* If not provided, the wallet must determine the Address to include in the message.
*/
readonly address?: string
/**
* Optional EIP-4361 Statement.
* If not provided, the wallet must not include Statement in the message.
*/
readonly statement?: string
/**
* Optional EIP-4361 URI.
* If not provided, the wallet must not include URI in the message.
*/
readonly uri?: string
/**
* Optional EIP-4361 Version.
* If not provided, the wallet must not include Version in the message.
*/
readonly version?: string
/**
* Optional EIP-4361 Chain ID.
* If not provided, the wallet must not include Chain ID in the message.
*/
readonly chainId?: string
/**
* Optional EIP-4361 Nonce.
* If not provided, the wallet must not include Nonce in the message.
*/
readonly nonce?: string
/**
* Optional EIP-4361 Issued At.
* If not provided, the wallet must not include Issued At in the message.
*/
readonly issuedAt?: string
/**
* Optional EIP-4361 Expiration Time.
* If not provided, the wallet must not include Expiration Time in the message.
*/
readonly expirationTime?: string
/**
* Optional EIP-4361 Not Before.
* If not provided, the wallet must not include Not Before in the message.
*/
readonly notBefore?: string
/**
* Optional EIP-4361 Request ID.
* If not provided, the wallet must not include Request ID in the message.
*/
readonly requestId?: string
/**
* Optional EIP-4361 Resources.
* If not provided, the wallet must not include Resources in the message.
*/
readonly resources?: readonly string[]
}
/** Output of signing in. */
export interface SolanaSignInOutput {
/**
* Account that was signed in.
* The address of the account may be different from the provided input Address.
*/
readonly account: WalletAccount
/**
* Message bytes that were signed.
* The wallet may prefix or otherwise modify the message before signing it.
*/
readonly signedMessage: Uint8Array
/**
* Message signature produced.
* If the signature type is provided, the signature must be Ed25519.
*/
readonly signature: Uint8Array
/**
* Optional type of the message signature produced.
* If not provided, the signature must be Ed25519.
*/
readonly signatureType?: 'ed25519'
}

View File

@@ -0,0 +1,636 @@
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/browser/src/types/index.ts
import { StrictOmit } from './types'
/**
* A variant of PublicKeyCredentialCreationOptions suitable for JSON transmission to the browser to
* (eventually) get passed into navigator.credentials.create(...) in the browser.
*
* This should eventually get replaced with official TypeScript DOM types when WebAuthn Level 3 types
* eventually make it into the language:
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson W3C WebAuthn Spec - PublicKeyCredentialCreationOptionsJSON}
*/
export interface PublicKeyCredentialCreationOptionsJSON {
/**
* Information about the Relying Party responsible for the request.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-rp W3C - rp}
*/
rp: PublicKeyCredentialRpEntity
/**
* Information about the user account for which the credential is being created.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-user W3C - user}
*/
user: PublicKeyCredentialUserEntityJSON
/**
* A server-generated challenge in base64url format.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-challenge W3C - challenge}
*/
challenge: Base64URLString
/**
* Information about desired properties of the credential to be created.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-pubkeycredparams W3C - pubKeyCredParams}
*/
pubKeyCredParams: PublicKeyCredentialParameters[]
/**
* Time in milliseconds that the caller is willing to wait for the operation to complete.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-timeout W3C - timeout}
*/
timeout?: number
/**
* Credentials that the authenticator should not create a new credential for.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-excludecredentials W3C - excludeCredentials}
*/
excludeCredentials?: PublicKeyCredentialDescriptorJSON[]
/**
* Criteria for authenticator selection.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-authenticatorselection W3C - authenticatorSelection}
*/
authenticatorSelection?: AuthenticatorSelectionCriteria
/**
* Hints about what types of authenticators the user might want to use.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-hints W3C - hints}
*/
hints?: PublicKeyCredentialHint[]
/**
* How the attestation statement should be transported.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-attestation W3C - attestation}
*/
attestation?: AttestationConveyancePreference
/**
* The attestation statement formats that the Relying Party will accept.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-attestationformats W3C - attestationFormats}
*/
attestationFormats?: AttestationFormat[]
/**
* Additional parameters requesting additional processing by the client and authenticator.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-extensions W3C - extensions}
*/
extensions?: AuthenticationExtensionsClientInputs
}
/**
* A variant of PublicKeyCredentialRequestOptions suitable for JSON transmission to the browser to
* (eventually) get passed into navigator.credentials.get(...) in the browser.
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson W3C WebAuthn Spec - PublicKeyCredentialRequestOptionsJSON}
*/
export interface PublicKeyCredentialRequestOptionsJSON {
/**
* A server-generated challenge in base64url format.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-challenge W3C - challenge}
*/
challenge: Base64URLString
/**
* Time in milliseconds that the caller is willing to wait for the operation to complete.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-timeout W3C - timeout}
*/
timeout?: number
/**
* The relying party identifier claimed by the caller.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-rpid W3C - rpId}
*/
rpId?: string
/**
* A list of credentials acceptable for authentication.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-allowcredentials W3C - allowCredentials}
*/
allowCredentials?: PublicKeyCredentialDescriptorJSON[]
/**
* Whether user verification should be performed by the authenticator.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-userverification W3C - userVerification}
*/
userVerification?: UserVerificationRequirement
/**
* Hints about what types of authenticators the user might want to use.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-hints W3C - hints}
*/
hints?: PublicKeyCredentialHint[]
/**
* Additional parameters requesting additional processing by the client and authenticator.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-extensions W3C - extensions}
*/
extensions?: AuthenticationExtensionsClientInputs
}
/**
* Represents a public key credential descriptor in JSON format.
* Used to identify credentials for exclusion or allowance during WebAuthn ceremonies.
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-publickeycredentialdescriptorjson W3C WebAuthn Spec - PublicKeyCredentialDescriptorJSON}
*/
export interface PublicKeyCredentialDescriptorJSON {
/**
* The credential ID in base64url format.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialdescriptor-id W3C - id}
*/
id: Base64URLString
/**
* The type of the public key credential (always "public-key").
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialdescriptor-type W3C - type}
*/
type: PublicKeyCredentialType
/**
* How the authenticator communicates with clients.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialdescriptor-transports W3C - transports}
*/
transports?: AuthenticatorTransportFuture[]
}
/**
* Represents user account information in JSON format for WebAuthn registration.
* Contains identifiers and display information for the user being registered.
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-publickeycredentialuserentityjson W3C WebAuthn Spec - PublicKeyCredentialUserEntityJSON}
*/
export interface PublicKeyCredentialUserEntityJSON {
/**
* A unique identifier for the user account (base64url encoded).
* Maximum 64 bytes. Should not contain PII.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-id W3C - user.id}
*/
id: string
/**
* A human-readable identifier for the account (e.g., email, username).
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialentity-name W3C - user.name}
*/
name: string
/**
* A human-friendly display name for the user (e.g., "John Doe").
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-displayname W3C - user.displayName}
*/
displayName: string
}
/**
* Represents user account information for WebAuthn registration with binary data.
* Contains identifiers and display information for the user being registered.
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-publickeycredentialuserentity W3C WebAuthn Spec - PublicKeyCredentialUserEntity}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialUserEntity MDN - PublicKeyCredentialUserEntity}
*/
export interface PublicKeyCredentialUserEntity {
/**
* A unique identifier for the user account.
* Maximum 64 bytes. Should not contain PII.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-id W3C - user.id}
*/
id: BufferSource // ArrayBuffer | TypedArray | DataView
/**
* A human-readable identifier for the account.
* Typically an email, username, or phone number.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialentity-name W3C - user.name}
*/
name: string
/**
* A human-friendly display name for the user.
* Example: "John Doe"
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-displayname W3C - user.displayName}
*/
displayName: string
}
/**
* The credential returned from navigator.credentials.create() during WebAuthn registration.
* Contains the new credential's public key and attestation information.
*
* @see {@link https://w3c.github.io/webauthn/#registrationceremony W3C WebAuthn Spec - Registration}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential MDN - PublicKeyCredential}
*/
export interface RegistrationCredential
extends PublicKeyCredentialFuture<RegistrationResponseJSON> {
response: AuthenticatorAttestationResponseFuture
}
/**
* A slightly-modified RegistrationCredential to simplify working with ArrayBuffers that
* are Base64URL-encoded in the browser so that they can be sent as JSON to the server.
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-registrationresponsejson W3C WebAuthn Spec - RegistrationResponseJSON}
*/
export interface RegistrationResponseJSON {
/**
* The credential ID (same as rawId for JSON).
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-id W3C - id}
*/
id: Base64URLString
/**
* The raw credential ID in base64url format.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-rawid W3C - rawId}
*/
rawId: Base64URLString
/**
* The authenticator's response to the client's request to create a credential.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-response W3C - response}
*/
response: AuthenticatorAttestationResponseJSON
/**
* The authenticator attachment modality in effect at the time of credential creation.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-authenticatorattachment W3C - authenticatorAttachment}
*/
authenticatorAttachment?: AuthenticatorAttachment
/**
* The results of processing client extensions.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-getclientextensionresults W3C - getClientExtensionResults}
*/
clientExtensionResults: AuthenticationExtensionsClientOutputs
/**
* The type of the credential (always "public-key").
* @see {@link https://w3c.github.io/webauthn/#dom-credential-type W3C - type}
*/
type: PublicKeyCredentialType
}
/**
* The credential returned from navigator.credentials.get() during WebAuthn authentication.
* Contains the assertion signature proving possession of the private key.
*
* @see {@link https://w3c.github.io/webauthn/#authentication W3C WebAuthn Spec - Authentication}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential MDN - PublicKeyCredential}
*/
export interface AuthenticationCredential
extends PublicKeyCredentialFuture<AuthenticationResponseJSON> {
response: AuthenticatorAssertionResponse
}
/**
* A slightly-modified AuthenticationCredential to simplify working with ArrayBuffers that
* are Base64URL-encoded in the browser so that they can be sent as JSON to the server.
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-authenticationresponsejson W3C WebAuthn Spec - AuthenticationResponseJSON}
*/
export interface AuthenticationResponseJSON {
/**
* The credential ID (same as rawId for JSON).
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-id W3C - id}
*/
id: Base64URLString
/**
* The raw credential ID in base64url format.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-rawid W3C - rawId}
*/
rawId: Base64URLString
/**
* The authenticator's response to the client's request to authenticate.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-response W3C - response}
*/
response: AuthenticatorAssertionResponseJSON
/**
* The authenticator attachment modality in effect at the time of authentication.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-authenticatorattachment W3C - authenticatorAttachment}
*/
authenticatorAttachment?: AuthenticatorAttachment
/**
* The results of processing client extensions.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-getclientextensionresults W3C - getClientExtensionResults}
*/
clientExtensionResults: AuthenticationExtensionsClientOutputs
/**
* The type of the credential (always "public-key").
* @see {@link https://w3c.github.io/webauthn/#dom-credential-type W3C - type}
*/
type: PublicKeyCredentialType
}
/**
* A slightly-modified AuthenticatorAttestationResponse to simplify working with ArrayBuffers that
* are Base64URL-encoded in the browser so that they can be sent as JSON to the server.
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-authenticatorattestationresponsejson W3C WebAuthn Spec - AuthenticatorAttestationResponseJSON}
*/
export interface AuthenticatorAttestationResponseJSON {
/**
* JSON-serialized client data passed to the authenticator.
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorresponse-clientdatajson W3C - clientDataJSON}
*/
clientDataJSON: Base64URLString
/**
* The attestation object in base64url format.
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-attestationobject W3C - attestationObject}
*/
attestationObject: Base64URLString
/**
* The authenticator data contained within the attestation object.
* Optional in L2, but becomes required in L3. Play it safe until L3 becomes Recommendation
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-getauthenticatordata W3C - getAuthenticatorData}
*/
authenticatorData?: Base64URLString
/**
* The transports that the authenticator supports.
* Optional in L2, but becomes required in L3. Play it safe until L3 becomes Recommendation
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-gettransports W3C - getTransports}
*/
transports?: AuthenticatorTransportFuture[]
/**
* The COSEAlgorithmIdentifier for the public key.
* Optional in L2, but becomes required in L3. Play it safe until L3 becomes Recommendation
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-getpublickeyalgorithm W3C - getPublicKeyAlgorithm}
*/
publicKeyAlgorithm?: COSEAlgorithmIdentifier
/**
* The public key in base64url format.
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-getpublickey W3C - getPublicKey}
*/
publicKey?: Base64URLString
}
/**
* A slightly-modified AuthenticatorAssertionResponse to simplify working with ArrayBuffers that
* are Base64URL-encoded in the browser so that they can be sent as JSON to the server.
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-authenticatorassertionresponsejson W3C WebAuthn Spec - AuthenticatorAssertionResponseJSON}
*/
export interface AuthenticatorAssertionResponseJSON {
/**
* JSON-serialized client data passed to the authenticator.
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorresponse-clientdatajson W3C - clientDataJSON}
*/
clientDataJSON: Base64URLString
/**
* The authenticator data returned by the authenticator.
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-authenticatordata W3C - authenticatorData}
*/
authenticatorData: Base64URLString
/**
* The signature generated by the authenticator.
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-signature W3C - signature}
*/
signature: Base64URLString
/**
* The user handle returned by the authenticator, if any.
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-userhandle W3C - userHandle}
*/
userHandle?: Base64URLString
}
/**
* Public key credential information needed to verify authentication responses.
* Stores the credential's public key and metadata for server-side verification.
*
* @see {@link https://w3c.github.io/webauthn/#sctn-credential-storage-modality W3C WebAuthn Spec - Credential Storage}
*/
export type WebAuthnCredential = {
/**
* The credential ID in base64url format.
* @see {@link https://w3c.github.io/webauthn/#credential-id W3C - Credential ID}
*/
id: Base64URLString
/**
* The credential's public key.
* @see {@link https://w3c.github.io/webauthn/#credential-public-key W3C - Credential Public Key}
*/
publicKey: Uint8Array_
/**
* Number of times this authenticator is expected to have been used.
* @see {@link https://w3c.github.io/webauthn/#signature-counter W3C - Signature Counter}
*/
counter: number
/**
* The transports that the authenticator supports.
* From browser's `startRegistration()` -> RegistrationCredentialJSON.transports (API L2 and up)
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-gettransports W3C - getTransports}
*/
transports?: AuthenticatorTransportFuture[]
}
/**
* An attempt to communicate that this isn't just any string, but a Base64URL-encoded string.
* Base64URL encoding is used throughout WebAuthn for binary data transmission.
*
* @see {@link https://datatracker.ietf.org/doc/html/rfc4648#section-5 RFC 4648 - Base64URL Encoding}
*/
export type Base64URLString = string
/**
* AuthenticatorAttestationResponse in TypeScript's DOM lib is outdated (up through v3.9.7).
* Maintain an augmented version here so we can implement additional properties as the WebAuthn
* spec evolves.
*
* Properties marked optional are not supported in all browsers.
*
* @see {@link https://www.w3.org/TR/webauthn-2/#iface-authenticatorattestationresponse W3C WebAuthn Spec - AuthenticatorAttestationResponse}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse MDN - AuthenticatorAttestationResponse}
*/
export interface AuthenticatorAttestationResponseFuture extends AuthenticatorAttestationResponse {
/**
* Returns the transports that the authenticator supports.
* @see {@link https://w3c.github.io/webauthn/#dom-authenticatorattestationresponse-gettransports W3C - getTransports}
*/
getTransports(): AuthenticatorTransportFuture[]
}
/**
* A super class of TypeScript's `AuthenticatorTransport` that includes support for the latest
* transports. Should eventually be replaced by TypeScript's when TypeScript gets updated to
* know about it (sometime after 4.6.3)
*
* @see {@link https://w3c.github.io/webauthn/#enum-transport W3C WebAuthn Spec - AuthenticatorTransport}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse/getTransports MDN - getTransports}
*/
export type AuthenticatorTransportFuture =
| 'ble'
| 'cable'
| 'hybrid'
| 'internal'
| 'nfc'
| 'smart-card'
| 'usb'
/**
* A super class of TypeScript's `PublicKeyCredentialDescriptor` that knows about the latest
* transports. Should eventually be replaced by TypeScript's when TypeScript gets updated to
* know about it (sometime after 4.6.3)
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-publickeycredentialdescriptor W3C WebAuthn Spec - PublicKeyCredentialDescriptor}
*/
export interface PublicKeyCredentialDescriptorFuture
extends Omit<PublicKeyCredentialDescriptor, 'transports'> {
/**
* How the authenticator communicates with clients.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialdescriptor-transports W3C - transports}
*/
transports?: AuthenticatorTransportFuture[]
}
/**
* Enhanced PublicKeyCredentialCreationOptions that knows about the latest features.
* Used for WebAuthn registration ceremonies.
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions W3C WebAuthn Spec - PublicKeyCredentialCreationOptions}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions MDN - PublicKeyCredentialCreationOptions}
*/
export interface PublicKeyCredentialCreationOptionsFuture
extends StrictOmit<PublicKeyCredentialCreationOptions, 'excludeCredentials' | 'user'> {
/**
* Credentials that the authenticator should not create a new credential for.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-excludecredentials W3C - excludeCredentials}
*/
excludeCredentials?: PublicKeyCredentialDescriptorFuture[]
/**
* Information about the user account for which the credential is being created.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-user W3C - user}
*/
user: PublicKeyCredentialUserEntity
/**
* Hints about what types of authenticators the user might want to use.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-hints W3C - hints}
*/
hints?: PublicKeyCredentialHint[]
/**
* Criteria for authenticator selection.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-authenticatorselection W3C - authenticatorSelection}
*/
authenticatorSelection?: AuthenticatorSelectionCriteria
/**
* Information about desired properties of the credential to be created.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-pubkeycredparams W3C - pubKeyCredParams}
*/
pubKeyCredParams: PublicKeyCredentialParameters[]
/**
* Information about the Relying Party responsible for the request.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-rp W3C - rp}
*/
rp: PublicKeyCredentialRpEntity
}
/**
* Enhanced PublicKeyCredentialRequestOptions that knows about the latest features.
* Used for WebAuthn authentication ceremonies.
*
* @see {@link https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptions W3C WebAuthn Spec - PublicKeyCredentialRequestOptions}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions MDN - PublicKeyCredentialRequestOptions}
*/
export interface PublicKeyCredentialRequestOptionsFuture
extends StrictOmit<PublicKeyCredentialRequestOptions, 'allowCredentials'> {
/**
* A list of credentials acceptable for authentication.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-allowcredentials W3C - allowCredentials}
*/
allowCredentials?: PublicKeyCredentialDescriptorFuture[]
/**
* Hints about what types of authenticators the user might want to use.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialrequestoptions-hints W3C - hints}
*/
hints?: PublicKeyCredentialHint[]
/**
* The attestation conveyance preference for the authentication ceremony.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-attestation W3C - attestation}
*/
attestation?: AttestationConveyancePreference
}
/**
* Union type for all WebAuthn credential responses in JSON format.
* Can be either a registration response (for new credentials) or authentication response (for existing credentials).
*/
export type PublicKeyCredentialJSON = RegistrationResponseJSON | AuthenticationResponseJSON
/**
* A super class of TypeScript's `PublicKeyCredential` that knows about upcoming WebAuthn features.
* Includes WebAuthn Level 3 methods for JSON serialization and parsing.
*
* @see {@link https://w3c.github.io/webauthn/#publickeycredential W3C WebAuthn Spec - PublicKeyCredential}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential MDN - PublicKeyCredential}
*/
export interface PublicKeyCredentialFuture<
T extends PublicKeyCredentialJSON = PublicKeyCredentialJSON,
> extends PublicKeyCredential {
/**
* The type of the credential (always "public-key").
* @see {@link https://w3c.github.io/webauthn/#dom-credential-type W3C - type}
*/
type: PublicKeyCredentialType
/**
* Checks if conditional mediation is available.
* @see {@link https://github.com/w3c/webauthn/issues/1745 GitHub - Conditional Mediation}
*/
isConditionalMediationAvailable?(): Promise<boolean>
/**
* Parses JSON to create PublicKeyCredentialCreationOptions.
* @see {@link https://w3c.github.io/webauthn/#sctn-parseCreationOptionsFromJSON W3C - parseCreationOptionsFromJSON}
*/
parseCreationOptionsFromJSON(
options: PublicKeyCredentialCreationOptionsJSON
): PublicKeyCredentialCreationOptionsFuture
/**
* Parses JSON to create PublicKeyCredentialRequestOptions.
* @see {@link https://w3c.github.io/webauthn/#sctn-parseRequestOptionsFromJSON W3C - parseRequestOptionsFromJSON}
*/
parseRequestOptionsFromJSON(
options: PublicKeyCredentialRequestOptionsJSON
): PublicKeyCredentialRequestOptionsFuture
/**
* Serializes the credential to JSON format.
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-tojson W3C - toJSON}
*/
toJSON(): T
}
/**
* The two types of credentials as defined by bit 3 ("Backup Eligibility") in authenticator data:
* - `"singleDevice"` credentials will never be backed up
* - `"multiDevice"` credentials can be backed up
*
* @see {@link https://w3c.github.io/webauthn/#sctn-authenticator-data W3C WebAuthn Spec - Authenticator Data}
*/
export type CredentialDeviceType = 'singleDevice' | 'multiDevice'
/**
* Categories of authenticators that Relying Parties can pass along to browsers during
* registration. Browsers that understand these values can optimize their modal experience to
* start the user off in a particular registration flow:
*
* - `hybrid`: A platform authenticator on a mobile device
* - `security-key`: A portable FIDO2 authenticator capable of being used on multiple devices via a USB or NFC connection
* - `client-device`: The device that WebAuthn is being called on. Typically synonymous with platform authenticators
*
* These values are less strict than `authenticatorAttachment`
*
* @see {@link https://w3c.github.io/webauthn/#enumdef-publickeycredentialhint W3C WebAuthn Spec - PublicKeyCredentialHint}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions#hints MDN - hints}
*/
export type PublicKeyCredentialHint = 'hybrid' | 'security-key' | 'client-device'
/**
* Values for an attestation object's `fmt`.
* Defines the format of the attestation statement from the authenticator.
*
* @see {@link https://www.iana.org/assignments/webauthn/webauthn.xhtml#webauthn-attestation-statement-format-ids IANA - WebAuthn Attestation Statement Format Identifiers}
* @see {@link https://w3c.github.io/webauthn/#sctn-attestation-formats W3C WebAuthn Spec - Attestation Statement Formats}
*/
export type AttestationFormat =
| 'fido-u2f'
| 'packed'
| 'android-safetynet'
| 'android-key'
| 'tpm'
| 'apple'
| 'none'
/**
* Equivalent to `Uint8Array` before TypeScript 5.7, and `Uint8Array<ArrayBuffer>` in TypeScript 5.7
* and beyond.
*
* **Context**
*
* `Uint8Array` became a generic type in TypeScript 5.7, requiring types defined simply as
* `Uint8Array` to be refactored to `Uint8Array<ArrayBuffer>` starting in Deno 2.2. `Uint8Array` is
* _not_ generic in Deno 2.1.x and earlier, though, so this type helps bridge this gap.
*
* Inspired by Deno's std library:
*
* https://github.com/denoland/std/blob/b5a5fe4f96b91c1fe8dba5cc0270092dd11d3287/bytes/_types.ts#L11
*/
export type Uint8Array_ = ReturnType<Uint8Array['slice']>
/**
* Specifies the preferred authenticator attachment modality.
* - `platform`: A platform authenticator attached to the client device (e.g., Touch ID, Windows Hello)
* - `cross-platform`: A roaming authenticator not attached to the client device (e.g., USB security key)
*
* @see {@link https://w3c.github.io/webauthn/#enum-attachment W3C WebAuthn Spec - AuthenticatorAttachment}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions/authenticatorSelection#authenticatorattachment MDN - authenticatorAttachment}
*/
export type AuthenticatorAttachment = 'cross-platform' | 'platform'

View File

@@ -0,0 +1,317 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { StrictOmit } from './types'
import { isValidDomain } from './webauthn'
import {
PublicKeyCredentialCreationOptionsFuture,
PublicKeyCredentialRequestOptionsFuture,
} from './webauthn.dom'
/**
* A custom Error used to return a more nuanced error detailing _why_ one of the eight documented
* errors in the spec was raised after calling `navigator.credentials.create()` or
* `navigator.credentials.get()`:
*
* - `AbortError`
* - `ConstraintError`
* - `InvalidStateError`
* - `NotAllowedError`
* - `NotSupportedError`
* - `SecurityError`
* - `TypeError`
* - `UnknownError`
*
* Error messages were determined through investigation of the spec to determine under which
* scenarios a given error would be raised.
*/
export class WebAuthnError extends Error {
code: WebAuthnErrorCode
protected __isWebAuthnError = true
constructor({
message,
code,
cause,
name,
}: {
message: string
code: WebAuthnErrorCode
cause?: Error | unknown
name?: string
}) {
// @ts-ignore: help Rollup understand that `cause` is okay to set
super(message, { cause })
this.name = name ?? (cause instanceof Error ? cause.name : undefined) ?? 'Unknown Error'
this.code = code
}
}
/**
* Error class for unknown WebAuthn errors.
* Wraps unexpected errors that don't match known WebAuthn error conditions.
*/
export class WebAuthnUnknownError extends WebAuthnError {
originalError: unknown
constructor(message: string, originalError: unknown) {
super({
code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY',
cause: originalError,
message,
})
this.name = 'WebAuthnUnknownError'
this.originalError = originalError
}
}
/**
* Type guard to check if an error is a WebAuthnError.
* @param {unknown} error - The error to check
* @returns {boolean} True if the error is a WebAuthnError
*/
export function isWebAuthnError(error: unknown): error is WebAuthnError {
return typeof error === 'object' && error !== null && '__isWebAuthnError' in error
}
/**
* Error codes for WebAuthn operations.
* These codes provide specific information about why a WebAuthn ceremony failed.
* @see {@link https://w3c.github.io/webauthn/#sctn-defined-errors W3C WebAuthn Spec - Defined Errors}
*/
export type WebAuthnErrorCode =
| 'ERROR_CEREMONY_ABORTED'
| 'ERROR_INVALID_DOMAIN'
| 'ERROR_INVALID_RP_ID'
| 'ERROR_INVALID_USER_ID_LENGTH'
| 'ERROR_MALFORMED_PUBKEYCREDPARAMS'
| 'ERROR_AUTHENTICATOR_GENERAL_ERROR'
| 'ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT'
| 'ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT'
| 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED'
| 'ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG'
| 'ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE'
| 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY'
/**
* Attempt to intuit _why_ an error was raised after calling `navigator.credentials.create()`.
* Maps browser errors to specific WebAuthn error codes for better debugging.
* @param {Object} params - Error identification parameters
* @param {Error} params.error - The error thrown by the browser
* @param {CredentialCreationOptions} params.options - The options passed to credentials.create()
* @returns {WebAuthnError} A WebAuthnError with a specific error code
* @see {@link https://w3c.github.io/webauthn/#sctn-createCredential W3C WebAuthn Spec - Create Credential}
*/
export function identifyRegistrationError({
error,
options,
}: {
error: Error
options: StrictOmit<CredentialCreationOptions, 'publicKey'> & {
publicKey: PublicKeyCredentialCreationOptionsFuture
}
}): WebAuthnError {
const { publicKey } = options
if (!publicKey) {
throw Error('options was missing required publicKey property')
}
if (error.name === 'AbortError') {
if (options.signal instanceof AbortSignal) {
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 16)
return new WebAuthnError({
message: 'Registration ceremony was sent an abort signal',
code: 'ERROR_CEREMONY_ABORTED',
cause: error,
})
}
} else if (error.name === 'ConstraintError') {
if (publicKey.authenticatorSelection?.requireResidentKey === true) {
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 4)
return new WebAuthnError({
message:
'Discoverable credentials were required but no available authenticator supported it',
code: 'ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT',
cause: error,
})
} else if (
// @ts-ignore: `mediation` doesn't yet exist on CredentialCreationOptions but it's possible as of Sept 2024
options.mediation === 'conditional' &&
publicKey.authenticatorSelection?.userVerification === 'required'
) {
// https://w3c.github.io/webauthn/#sctn-createCredential (Step 22.4)
return new WebAuthnError({
message:
'User verification was required during automatic registration but it could not be performed',
code: 'ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE',
cause: error,
})
} else if (publicKey.authenticatorSelection?.userVerification === 'required') {
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 5)
return new WebAuthnError({
message: 'User verification was required but no available authenticator supported it',
code: 'ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT',
cause: error,
})
}
} else if (error.name === 'InvalidStateError') {
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 20)
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 3)
return new WebAuthnError({
message: 'The authenticator was previously registered',
code: 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED',
cause: error,
})
} else if (error.name === 'NotAllowedError') {
/**
* Pass the error directly through. Platforms are overloading this error beyond what the spec
* defines and we don't want to overwrite potentially useful error messages.
*/
return new WebAuthnError({
message: error.message,
code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY',
cause: error,
})
} else if (error.name === 'NotSupportedError') {
const validPubKeyCredParams = publicKey.pubKeyCredParams.filter(
(param) => param.type === 'public-key'
)
if (validPubKeyCredParams.length === 0) {
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 10)
return new WebAuthnError({
message: 'No entry in pubKeyCredParams was of type "public-key"',
code: 'ERROR_MALFORMED_PUBKEYCREDPARAMS',
cause: error,
})
}
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 2)
return new WebAuthnError({
message:
'No available authenticator supported any of the specified pubKeyCredParams algorithms',
code: 'ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG',
cause: error,
})
} else if (error.name === 'SecurityError') {
const effectiveDomain = window.location.hostname
if (!isValidDomain(effectiveDomain)) {
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 7)
return new WebAuthnError({
message: `${window.location.hostname} is an invalid domain`,
code: 'ERROR_INVALID_DOMAIN',
cause: error,
})
} else if (publicKey.rp.id !== effectiveDomain) {
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 8)
return new WebAuthnError({
message: `The RP ID "${publicKey.rp.id}" is invalid for this domain`,
code: 'ERROR_INVALID_RP_ID',
cause: error,
})
}
} else if (error.name === 'TypeError') {
if (publicKey.user.id.byteLength < 1 || publicKey.user.id.byteLength > 64) {
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 5)
return new WebAuthnError({
message: 'User ID was not between 1 and 64 characters',
code: 'ERROR_INVALID_USER_ID_LENGTH',
cause: error,
})
}
} else if (error.name === 'UnknownError') {
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 1)
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 8)
return new WebAuthnError({
message:
'The authenticator was unable to process the specified options, or could not create a new credential',
code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR',
cause: error,
})
}
return new WebAuthnError({
message: 'a Non-Webauthn related error has occurred',
code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY',
cause: error,
})
}
/**
* Attempt to intuit _why_ an error was raised after calling `navigator.credentials.get()`.
* Maps browser errors to specific WebAuthn error codes for better debugging.
* @param {Object} params - Error identification parameters
* @param {Error} params.error - The error thrown by the browser
* @param {CredentialRequestOptions} params.options - The options passed to credentials.get()
* @returns {WebAuthnError} A WebAuthnError with a specific error code
* @see {@link https://w3c.github.io/webauthn/#sctn-getAssertion W3C WebAuthn Spec - Get Assertion}
*/
export function identifyAuthenticationError({
error,
options,
}: {
error: Error
options: StrictOmit<CredentialRequestOptions, 'publicKey'> & {
publicKey: PublicKeyCredentialRequestOptionsFuture
}
}): WebAuthnError {
const { publicKey } = options
if (!publicKey) {
throw Error('options was missing required publicKey property')
}
if (error.name === 'AbortError') {
if (options.signal instanceof AbortSignal) {
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 16)
return new WebAuthnError({
message: 'Authentication ceremony was sent an abort signal',
code: 'ERROR_CEREMONY_ABORTED',
cause: error,
})
}
} else if (error.name === 'NotAllowedError') {
/**
* Pass the error directly through. Platforms are overloading this error beyond what the spec
* defines and we don't want to overwrite potentially useful error messages.
*/
return new WebAuthnError({
message: error.message,
code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY',
cause: error,
})
} else if (error.name === 'SecurityError') {
const effectiveDomain = window.location.hostname
if (!isValidDomain(effectiveDomain)) {
// https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 5)
return new WebAuthnError({
message: `${window.location.hostname} is an invalid domain`,
code: 'ERROR_INVALID_DOMAIN',
cause: error,
})
} else if (publicKey.rpId !== effectiveDomain) {
// https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 6)
return new WebAuthnError({
message: `The RP ID "${publicKey.rpId}" is invalid for this domain`,
code: 'ERROR_INVALID_RP_ID',
cause: error,
})
}
} else if (error.name === 'UnknownError') {
// https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (Step 1)
// https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (Step 12)
return new WebAuthnError({
message:
'The authenticator was unable to process the specified options, or could not create a new assertion signature',
code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR',
cause: error,
})
}
return new WebAuthnError({
message: 'a Non-Webauthn related error has occurred',
code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY',
cause: error,
})
}

View File

@@ -0,0 +1,946 @@
import GoTrueClient from '../GoTrueClient'
import { base64UrlToUint8Array, bytesToBase64URL } from './base64url'
import { AuthError, AuthUnknownError, isAuthError } from './errors'
import {
AuthMFAEnrollWebauthnResponse,
AuthMFAVerifyResponse,
AuthMFAVerifyResponseData,
MFAChallengeWebauthnParams,
MFAEnrollWebauthnParams,
MFAVerifyWebauthnParamFields,
MFAVerifyWebauthnParams,
RequestResult,
StrictOmit,
} from './types'
import { isBrowser } from './helpers'
import type {
AuthenticationCredential,
AuthenticationResponseJSON,
AuthenticatorAttachment,
PublicKeyCredentialCreationOptionsFuture,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialFuture,
PublicKeyCredentialRequestOptionsFuture,
PublicKeyCredentialRequestOptionsJSON,
RegistrationCredential,
RegistrationResponseJSON,
} from './webauthn.dom'
import {
identifyAuthenticationError,
identifyRegistrationError,
isWebAuthnError,
WebAuthnError,
WebAuthnUnknownError,
} from './webauthn.errors'
export { WebAuthnError, isWebAuthnError, identifyRegistrationError, identifyAuthenticationError }
// Re-export the JSON types for use in other files
export type { RegistrationResponseJSON, AuthenticationResponseJSON }
/**
* WebAuthn abort service to manage ceremony cancellation.
* Ensures only one WebAuthn ceremony is active at a time to prevent "operation already in progress" errors.
*
* @experimental This class is experimental and may change in future releases
* @see {@link https://w3c.github.io/webauthn/#sctn-automation-webdriver-capability W3C WebAuthn Spec - Aborting Ceremonies}
*/
export class WebAuthnAbortService {
private controller: AbortController | undefined
/**
* Create an abort signal for a new WebAuthn operation.
* Automatically cancels any existing operation.
*
* @returns {AbortSignal} Signal to pass to navigator.credentials.create() or .get()
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal MDN - AbortSignal}
*/
createNewAbortSignal(): AbortSignal {
// Abort any existing calls to navigator.credentials.create() or navigator.credentials.get()
if (this.controller) {
const abortError = new Error('Cancelling existing WebAuthn API call for new one')
abortError.name = 'AbortError'
this.controller.abort(abortError)
}
const newController = new AbortController()
this.controller = newController
return newController.signal
}
/**
* Manually cancel the current WebAuthn operation.
* Useful for cleaning up when user cancels or navigates away.
*
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort MDN - AbortController.abort}
*/
cancelCeremony(): void {
if (this.controller) {
const abortError = new Error('Manually cancelling existing WebAuthn API call')
abortError.name = 'AbortError'
this.controller.abort(abortError)
this.controller = undefined
}
}
}
/**
* Singleton instance to ensure only one WebAuthn ceremony is active at a time.
* This prevents "operation already in progress" errors when retrying WebAuthn operations.
*
* @experimental This instance is experimental and may change in future releases
*/
export const webAuthnAbortService = new WebAuthnAbortService()
/**
* Server response format for WebAuthn credential creation options.
* Uses W3C standard JSON format with base64url-encoded binary fields.
*/
export type ServerCredentialCreationOptions = PublicKeyCredentialCreationOptionsJSON
/**
* Server response format for WebAuthn credential request options.
* Uses W3C standard JSON format with base64url-encoded binary fields.
*/
export type ServerCredentialRequestOptions = PublicKeyCredentialRequestOptionsJSON
/**
* Convert base64url encoded strings in WebAuthn credential creation options to ArrayBuffers
* as required by the WebAuthn browser API.
* Supports both native WebAuthn Level 3 parseCreationOptionsFromJSON and manual fallback.
*
* @param {ServerCredentialCreationOptions} options - JSON options from server with base64url encoded fields
* @returns {PublicKeyCredentialCreationOptionsFuture} Options ready for navigator.credentials.create()
* @see {@link https://w3c.github.io/webauthn/#sctn-parseCreationOptionsFromJSON W3C WebAuthn Spec - parseCreationOptionsFromJSON}
*/
export function deserializeCredentialCreationOptions(
options: ServerCredentialCreationOptions
): PublicKeyCredentialCreationOptionsFuture {
if (!options) {
throw new Error('Credential creation options are required')
}
// Check if the native parseCreationOptionsFromJSON method is available
if (
typeof PublicKeyCredential !== 'undefined' &&
'parseCreationOptionsFromJSON' in PublicKeyCredential &&
typeof (PublicKeyCredential as unknown as PublicKeyCredentialFuture)
.parseCreationOptionsFromJSON === 'function'
) {
// Use the native WebAuthn Level 3 method
return (
PublicKeyCredential as unknown as PublicKeyCredentialFuture
).parseCreationOptionsFromJSON(
/** we assert the options here as typescript still doesn't know about future webauthn types */
options as any
) as PublicKeyCredentialCreationOptionsFuture
}
// Fallback to manual parsing for browsers that don't support the native method
// Destructure to separate fields that need transformation
const { challenge: challengeStr, user: userOpts, excludeCredentials, ...restOptions } = options
// Convert challenge from base64url to ArrayBuffer
const challenge = base64UrlToUint8Array(challengeStr).buffer as ArrayBuffer
// Convert user.id from base64url to ArrayBuffer
const user: PublicKeyCredentialUserEntity = {
...userOpts,
id: base64UrlToUint8Array(userOpts.id).buffer as ArrayBuffer,
}
// Build the result object
const result: PublicKeyCredentialCreationOptionsFuture = {
...restOptions,
challenge,
user,
}
// Only add excludeCredentials if it exists
if (excludeCredentials && excludeCredentials.length > 0) {
result.excludeCredentials = new Array(excludeCredentials.length)
for (let i = 0; i < excludeCredentials.length; i++) {
const cred = excludeCredentials[i]
result.excludeCredentials[i] = {
...cred,
id: base64UrlToUint8Array(cred.id).buffer,
type: cred.type || 'public-key',
// Cast transports to handle future transport types like "cable"
transports: cred.transports,
}
}
}
return result
}
/**
* Convert base64url encoded strings in WebAuthn credential request options to ArrayBuffers
* as required by the WebAuthn browser API.
* Supports both native WebAuthn Level 3 parseRequestOptionsFromJSON and manual fallback.
*
* @param {ServerCredentialRequestOptions} options - JSON options from server with base64url encoded fields
* @returns {PublicKeyCredentialRequestOptionsFuture} Options ready for navigator.credentials.get()
* @see {@link https://w3c.github.io/webauthn/#sctn-parseRequestOptionsFromJSON W3C WebAuthn Spec - parseRequestOptionsFromJSON}
*/
export function deserializeCredentialRequestOptions(
options: ServerCredentialRequestOptions
): PublicKeyCredentialRequestOptionsFuture {
if (!options) {
throw new Error('Credential request options are required')
}
// Check if the native parseRequestOptionsFromJSON method is available
if (
typeof PublicKeyCredential !== 'undefined' &&
'parseRequestOptionsFromJSON' in PublicKeyCredential &&
typeof (PublicKeyCredential as unknown as PublicKeyCredentialFuture)
.parseRequestOptionsFromJSON === 'function'
) {
// Use the native WebAuthn Level 3 method
return (
PublicKeyCredential as unknown as PublicKeyCredentialFuture
).parseRequestOptionsFromJSON(options) as PublicKeyCredentialRequestOptionsFuture
}
// Fallback to manual parsing for browsers that don't support the native method
// Destructure to separate fields that need transformation
const { challenge: challengeStr, allowCredentials, ...restOptions } = options
// Convert challenge from base64url to ArrayBuffer
const challenge = base64UrlToUint8Array(challengeStr).buffer as ArrayBuffer
// Build the result object
const result: PublicKeyCredentialRequestOptionsFuture = {
...restOptions,
challenge,
}
// Only add allowCredentials if it exists
if (allowCredentials && allowCredentials.length > 0) {
result.allowCredentials = new Array(allowCredentials.length)
for (let i = 0; i < allowCredentials.length; i++) {
const cred = allowCredentials[i]
result.allowCredentials[i] = {
...cred,
id: base64UrlToUint8Array(cred.id).buffer,
type: cred.type || 'public-key',
// Cast transports to handle future transport types like "cable"
transports: cred.transports,
}
}
}
return result
}
/**
* Server format for credential response with base64url-encoded binary fields
* Can be either a registration or authentication response
*/
export type ServerCredentialResponse = RegistrationResponseJSON | AuthenticationResponseJSON
/**
* Convert a registration/enrollment credential response to server format.
* Serializes binary fields to base64url for JSON transmission.
* Supports both native WebAuthn Level 3 toJSON and manual fallback.
*
* @param {RegistrationCredential} credential - Credential from navigator.credentials.create()
* @returns {RegistrationResponseJSON} JSON-serializable credential for server
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-tojson W3C WebAuthn Spec - toJSON}
*/
export function serializeCredentialCreationResponse(
credential: RegistrationCredential
): RegistrationResponseJSON {
// Check if the credential instance has the toJSON method
if ('toJSON' in credential && typeof credential.toJSON === 'function') {
// Use the native WebAuthn Level 3 method
return (credential as RegistrationCredential).toJSON()
}
const credentialWithAttachment = credential as PublicKeyCredential & {
response: AuthenticatorAttestationResponse
authenticatorAttachment?: string | null
}
return {
id: credential.id,
rawId: credential.id,
response: {
attestationObject: bytesToBase64URL(new Uint8Array(credential.response.attestationObject)),
clientDataJSON: bytesToBase64URL(new Uint8Array(credential.response.clientDataJSON)),
},
type: 'public-key',
clientExtensionResults: credential.getClientExtensionResults(),
// Convert null to undefined and cast to AuthenticatorAttachment type
authenticatorAttachment: (credentialWithAttachment.authenticatorAttachment ?? undefined) as
| AuthenticatorAttachment
| undefined,
}
}
/**
* Convert an authentication/verification credential response to server format.
* Serializes binary fields to base64url for JSON transmission.
* Supports both native WebAuthn Level 3 toJSON and manual fallback.
*
* @param {AuthenticationCredential} credential - Credential from navigator.credentials.get()
* @returns {AuthenticationResponseJSON} JSON-serializable credential for server
* @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-tojson W3C WebAuthn Spec - toJSON}
*/
export function serializeCredentialRequestResponse(
credential: AuthenticationCredential
): AuthenticationResponseJSON {
// Check if the credential instance has the toJSON method
if ('toJSON' in credential && typeof credential.toJSON === 'function') {
// Use the native WebAuthn Level 3 method
return (credential as AuthenticationCredential).toJSON()
}
// Fallback to manual conversion for browsers that don't support toJSON
// Access authenticatorAttachment via type assertion to handle TypeScript version differences
// @simplewebauthn/types includes this property but base TypeScript 4.7.4 doesn't
const credentialWithAttachment = credential as PublicKeyCredential & {
response: AuthenticatorAssertionResponse
authenticatorAttachment?: string | null
}
const clientExtensionResults = credential.getClientExtensionResults()
const assertionResponse = credential.response
return {
id: credential.id,
rawId: credential.id, // W3C spec expects rawId to match id for JSON format
response: {
authenticatorData: bytesToBase64URL(new Uint8Array(assertionResponse.authenticatorData)),
clientDataJSON: bytesToBase64URL(new Uint8Array(assertionResponse.clientDataJSON)),
signature: bytesToBase64URL(new Uint8Array(assertionResponse.signature)),
userHandle: assertionResponse.userHandle
? bytesToBase64URL(new Uint8Array(assertionResponse.userHandle))
: undefined,
},
type: 'public-key',
clientExtensionResults,
// Convert null to undefined and cast to AuthenticatorAttachment type
authenticatorAttachment: (credentialWithAttachment.authenticatorAttachment ?? undefined) as
| AuthenticatorAttachment
| undefined,
}
}
/**
* A simple test to determine if a hostname is a properly-formatted domain name.
* Considers localhost valid for development environments.
*
* A "valid domain" is defined here: https://url.spec.whatwg.org/#valid-domain
*
* Regex sourced from here:
* https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch08s15.html
*
* @param {string} hostname - The hostname to validate
* @returns {boolean} True if valid domain or localhost
* @see {@link https://url.spec.whatwg.org/#valid-domain WHATWG URL Spec - Valid Domain}
*/
export function isValidDomain(hostname: string): boolean {
return (
// Consider localhost valid as well since it's okay wrt Secure Contexts
hostname === 'localhost' || /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(hostname)
)
}
/**
* Determine if the browser is capable of WebAuthn.
* Checks for necessary Web APIs: PublicKeyCredential and Credential Management.
*
* @returns {boolean} True if browser supports WebAuthn
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential#browser_compatibility MDN - PublicKeyCredential Browser Compatibility}
*/
function browserSupportsWebAuthn(): boolean {
return !!(
isBrowser() &&
'PublicKeyCredential' in window &&
window.PublicKeyCredential &&
'credentials' in navigator &&
typeof navigator?.credentials?.create === 'function' &&
typeof navigator?.credentials?.get === 'function'
)
}
/**
* Create a WebAuthn credential using the browser's credentials API.
* Wraps navigator.credentials.create() with error handling.
*
* @param {CredentialCreationOptions} options - Options including publicKey parameters
* @returns {Promise<RequestResult<RegistrationCredential, WebAuthnError>>} Created credential or error
* @see {@link https://w3c.github.io/webauthn/#sctn-createCredential W3C WebAuthn Spec - Create Credential}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create MDN - credentials.create}
*/
export async function createCredential(
options: StrictOmit<CredentialCreationOptions, 'publicKey'> & {
publicKey: PublicKeyCredentialCreationOptionsFuture
}
): Promise<RequestResult<RegistrationCredential, WebAuthnError>> {
try {
const response = await navigator.credentials.create(
/** we assert the type here until typescript types are updated */
options as Parameters<typeof navigator.credentials.create>[0]
)
if (!response) {
return {
data: null,
error: new WebAuthnUnknownError('Empty credential response', response),
}
}
if (!(response instanceof PublicKeyCredential)) {
return {
data: null,
error: new WebAuthnUnknownError('Browser returned unexpected credential type', response),
}
}
return { data: response as RegistrationCredential, error: null }
} catch (err) {
return {
data: null,
error: identifyRegistrationError({
error: err as Error,
options,
}),
}
}
}
/**
* Get a WebAuthn credential using the browser's credentials API.
* Wraps navigator.credentials.get() with error handling.
*
* @param {CredentialRequestOptions} options - Options including publicKey parameters
* @returns {Promise<RequestResult<AuthenticationCredential, WebAuthnError>>} Retrieved credential or error
* @see {@link https://w3c.github.io/webauthn/#sctn-getAssertion W3C WebAuthn Spec - Get Assertion}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get MDN - credentials.get}
*/
export async function getCredential(
options: StrictOmit<CredentialRequestOptions, 'publicKey'> & {
publicKey: PublicKeyCredentialRequestOptionsFuture
}
): Promise<RequestResult<AuthenticationCredential, WebAuthnError>> {
try {
const response = await navigator.credentials.get(
/** we assert the type here until typescript types are updated */
options as Parameters<typeof navigator.credentials.get>[0]
)
if (!response) {
return {
data: null,
error: new WebAuthnUnknownError('Empty credential response', response),
}
}
if (!(response instanceof PublicKeyCredential)) {
return {
data: null,
error: new WebAuthnUnknownError('Browser returned unexpected credential type', response),
}
}
return { data: response as AuthenticationCredential, error: null }
} catch (err) {
return {
data: null,
error: identifyAuthenticationError({
error: err as Error,
options,
}),
}
}
}
export const DEFAULT_CREATION_OPTIONS: Partial<PublicKeyCredentialCreationOptionsFuture> = {
hints: ['security-key'],
authenticatorSelection: {
authenticatorAttachment: 'cross-platform',
requireResidentKey: false,
/** set to preferred because older yubikeys don't have PIN/Biometric */
userVerification: 'preferred',
residentKey: 'discouraged',
},
attestation: 'direct',
}
export const DEFAULT_REQUEST_OPTIONS: Partial<PublicKeyCredentialRequestOptionsFuture> = {
/** set to preferred because older yubikeys don't have PIN/Biometric */
userVerification: 'preferred',
hints: ['security-key'],
attestation: 'direct',
}
function deepMerge<T>(...sources: Partial<T>[]): T {
const isObject = (val: unknown): val is Record<string, unknown> =>
val !== null && typeof val === 'object' && !Array.isArray(val)
const isArrayBufferLike = (val: unknown): val is ArrayBuffer | ArrayBufferView =>
val instanceof ArrayBuffer || ArrayBuffer.isView(val)
const result: Partial<T> = {}
for (const source of sources) {
if (!source) continue
for (const key in source) {
const value = source[key]
if (value === undefined) continue
if (Array.isArray(value)) {
// preserve array reference, including unions like AuthenticatorTransport[]
result[key] = value as T[typeof key]
} else if (isArrayBufferLike(value)) {
result[key] = value as T[typeof key]
} else if (isObject(value)) {
const existing = result[key]
if (isObject(existing)) {
result[key] = deepMerge(existing, value) as unknown as T[typeof key]
} else {
result[key] = deepMerge(value) as unknown as T[typeof key]
}
} else {
result[key] = value as T[typeof key]
}
}
}
return result as T
}
/**
* Merges WebAuthn credential creation options with overrides.
* Sets sensible defaults for authenticator selection and extensions.
*
* @param {PublicKeyCredentialCreationOptionsFuture} baseOptions - The base options from the server
* @param {PublicKeyCredentialCreationOptionsFuture} overrides - Optional overrides to apply
* @param {string} friendlyName - Optional friendly name for the credential
* @returns {PublicKeyCredentialCreationOptionsFuture} Merged credential creation options
* @see {@link https://w3c.github.io/webauthn/#dictdef-authenticatorselectioncriteria W3C WebAuthn Spec - AuthenticatorSelectionCriteria}
*/
export function mergeCredentialCreationOptions(
baseOptions: PublicKeyCredentialCreationOptionsFuture,
overrides?: Partial<PublicKeyCredentialCreationOptionsFuture>
): PublicKeyCredentialCreationOptionsFuture {
return deepMerge(DEFAULT_CREATION_OPTIONS, baseOptions, overrides || {})
}
/**
* Merges WebAuthn credential request options with overrides.
* Sets sensible defaults for user verification and hints.
*
* @param {PublicKeyCredentialRequestOptionsFuture} baseOptions - The base options from the server
* @param {PublicKeyCredentialRequestOptionsFuture} overrides - Optional overrides to apply
* @returns {PublicKeyCredentialRequestOptionsFuture} Merged credential request options
* @see {@link https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptions W3C WebAuthn Spec - PublicKeyCredentialRequestOptions}
*/
export function mergeCredentialRequestOptions(
baseOptions: PublicKeyCredentialRequestOptionsFuture,
overrides?: Partial<PublicKeyCredentialRequestOptionsFuture>
): PublicKeyCredentialRequestOptionsFuture {
return deepMerge(DEFAULT_REQUEST_OPTIONS, baseOptions, overrides || {})
}
/**
* WebAuthn API wrapper for Supabase Auth.
* Provides methods for enrolling, challenging, verifying, authenticating, and registering WebAuthn credentials.
*
* @experimental This API is experimental and may change in future releases
* @see {@link https://w3c.github.io/webauthn/ W3C WebAuthn Specification}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API MDN - Web Authentication API}
*/
export class WebAuthnApi {
public enroll: typeof WebAuthnApi.prototype._enroll
public challenge: typeof WebAuthnApi.prototype._challenge
public verify: typeof WebAuthnApi.prototype._verify
public authenticate: typeof WebAuthnApi.prototype._authenticate
public register: typeof WebAuthnApi.prototype._register
constructor(private client: GoTrueClient) {
// Bind all methods so they can be destructured
this.enroll = this._enroll.bind(this)
this.challenge = this._challenge.bind(this)
this.verify = this._verify.bind(this)
this.authenticate = this._authenticate.bind(this)
this.register = this._register.bind(this)
}
/**
* Enroll a new WebAuthn factor.
* Creates an unverified WebAuthn factor that must be verified with a credential.
*
* @experimental This method is experimental and may change in future releases
* @param {Omit<MFAEnrollWebauthnParams, 'factorType'>} params - Enrollment parameters (friendlyName required)
* @returns {Promise<AuthMFAEnrollWebauthnResponse>} Enrolled factor details or error
* @see {@link https://w3c.github.io/webauthn/#sctn-registering-a-new-credential W3C WebAuthn Spec - Registering a New Credential}
*/
public async _enroll(
params: Omit<MFAEnrollWebauthnParams, 'factorType'>
): Promise<AuthMFAEnrollWebauthnResponse> {
return this.client.mfa.enroll({ ...params, factorType: 'webauthn' })
}
/**
* Challenge for WebAuthn credential creation or authentication.
* Combines server challenge with browser credential operations.
* Handles both registration (create) and authentication (request) flows.
*
* @experimental This method is experimental and may change in future releases
* @param {MFAChallengeWebauthnParams & { friendlyName?: string; signal?: AbortSignal }} params - Challenge parameters including factorId
* @param {Object} overrides - Allows you to override the parameters passed to navigator.credentials
* @param {PublicKeyCredentialCreationOptionsFuture} overrides.create - Override options for credential creation
* @param {PublicKeyCredentialRequestOptionsFuture} overrides.request - Override options for credential request
* @returns {Promise<RequestResult>} Challenge response with credential or error
* @see {@link https://w3c.github.io/webauthn/#sctn-credential-creation W3C WebAuthn Spec - Credential Creation}
* @see {@link https://w3c.github.io/webauthn/#sctn-verifying-assertion W3C WebAuthn Spec - Verifying Assertion}
*/
public async _challenge(
{
factorId,
webauthn,
friendlyName,
signal,
}: MFAChallengeWebauthnParams & { friendlyName?: string; signal?: AbortSignal },
overrides?:
| {
create?: Partial<PublicKeyCredentialCreationOptionsFuture>
request?: never
}
| {
create?: never
request?: Partial<PublicKeyCredentialRequestOptionsFuture>
}
): Promise<
RequestResult<
{ factorId: string; challengeId: string } & {
webauthn: StrictOmit<
MFAVerifyWebauthnParamFields<'create' | 'request'>['webauthn'],
'rpId' | 'rpOrigins'
>
},
WebAuthnError | AuthError
>
> {
try {
// Get challenge from server using the client's MFA methods
const { data: challengeResponse, error: challengeError } = await this.client.mfa.challenge({
factorId,
webauthn,
})
if (!challengeResponse) {
return { data: null, error: challengeError }
}
const abortSignal = signal ?? webAuthnAbortService.createNewAbortSignal()
/** webauthn will fail if either of the name/displayname are blank */
if (challengeResponse.webauthn.type === 'create') {
const { user } = challengeResponse.webauthn.credential_options.publicKey
if (!user.name) {
// Preserve original format: use friendlyName if provided, otherwise fetch fallback
// This maintains backward compatibility with the ${user.id}:${name} format
const nameToUse = friendlyName
if (!nameToUse) {
// Only fetch user data if friendlyName is not provided (bug fix for null friendlyName)
const currentUser = await this.client.getUser()
const userData = currentUser.data.user
const fallbackName =
userData?.user_metadata?.name || userData?.email || userData?.id || 'User'
user.name = `${user.id}:${fallbackName}`
} else {
user.name = `${user.id}:${nameToUse}`
}
}
if (!user.displayName) {
user.displayName = user.name
}
}
switch (challengeResponse.webauthn.type) {
case 'create': {
const options = mergeCredentialCreationOptions(
challengeResponse.webauthn.credential_options.publicKey,
overrides?.create
)
const { data, error } = await createCredential({
publicKey: options,
signal: abortSignal,
})
if (data) {
return {
data: {
factorId,
challengeId: challengeResponse.id,
webauthn: {
type: challengeResponse.webauthn.type,
credential_response: data,
},
},
error: null,
}
}
return { data: null, error }
}
case 'request': {
const options = mergeCredentialRequestOptions(
challengeResponse.webauthn.credential_options.publicKey,
overrides?.request
)
const { data, error } = await getCredential({
...challengeResponse.webauthn.credential_options,
publicKey: options,
signal: abortSignal,
})
if (data) {
return {
data: {
factorId,
challengeId: challengeResponse.id,
webauthn: {
type: challengeResponse.webauthn.type,
credential_response: data,
},
},
error: null,
}
}
return { data: null, error }
}
}
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
return {
data: null,
error: new AuthUnknownError('Unexpected error in challenge', error),
}
}
}
/**
* Verify a WebAuthn credential with the server.
* Completes the WebAuthn ceremony by sending the credential to the server for verification.
*
* @experimental This method is experimental and may change in future releases
* @param {Object} params - Verification parameters
* @param {string} params.challengeId - ID of the challenge being verified
* @param {string} params.factorId - ID of the WebAuthn factor
* @param {MFAVerifyWebauthnParams<T>['webauthn']} params.webauthn - WebAuthn credential response
* @returns {Promise<AuthMFAVerifyResponse>} Verification result with session or error
* @see {@link https://w3c.github.io/webauthn/#sctn-verifying-assertion W3C WebAuthn Spec - Verifying an Authentication Assertion}
* */
public async _verify<T extends 'create' | 'request'>({
challengeId,
factorId,
webauthn,
}: {
challengeId: string
factorId: string
webauthn: MFAVerifyWebauthnParams<T>['webauthn']
}): Promise<AuthMFAVerifyResponse> {
return this.client.mfa.verify({
factorId,
challengeId,
webauthn: webauthn,
})
}
/**
* Complete WebAuthn authentication flow.
* Performs challenge and verification in a single operation for existing credentials.
*
* @experimental This method is experimental and may change in future releases
* @param {Object} params - Authentication parameters
* @param {string} params.factorId - ID of the WebAuthn factor to authenticate with
* @param {Object} params.webauthn - WebAuthn configuration
* @param {string} params.webauthn.rpId - Relying Party ID (defaults to current hostname)
* @param {string[]} params.webauthn.rpOrigins - Allowed origins (defaults to current origin)
* @param {AbortSignal} params.webauthn.signal - Optional abort signal
* @param {PublicKeyCredentialRequestOptionsFuture} overrides - Override options for navigator.credentials.get
* @returns {Promise<RequestResult<AuthMFAVerifyResponseData, WebAuthnError | AuthError>>} Authentication result
* @see {@link https://w3c.github.io/webauthn/#sctn-authentication W3C WebAuthn Spec - Authentication Ceremony}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions MDN - PublicKeyCredentialRequestOptions}
*/
public async _authenticate(
{
factorId,
webauthn: {
rpId = typeof window !== 'undefined' ? window.location.hostname : undefined,
rpOrigins = typeof window !== 'undefined' ? [window.location.origin] : undefined,
signal,
} = {},
}: {
factorId: string
webauthn?: {
rpId?: string
rpOrigins?: string[]
signal?: AbortSignal
}
},
overrides?: PublicKeyCredentialRequestOptionsFuture
): Promise<RequestResult<AuthMFAVerifyResponseData, WebAuthnError | AuthError>> {
if (!rpId) {
return {
data: null,
error: new AuthError('rpId is required for WebAuthn authentication'),
}
}
try {
if (!browserSupportsWebAuthn()) {
return {
data: null,
error: new AuthUnknownError('Browser does not support WebAuthn', null),
}
}
// Get challenge and credential
const { data: challengeResponse, error: challengeError } = await this.challenge(
{
factorId,
webauthn: { rpId, rpOrigins },
signal,
},
{ request: overrides }
)
if (!challengeResponse) {
return { data: null, error: challengeError }
}
const { webauthn } = challengeResponse
// Verify credential
return this._verify({
factorId,
challengeId: challengeResponse.challengeId,
webauthn: {
type: webauthn.type,
rpId,
rpOrigins,
credential_response: webauthn.credential_response,
},
})
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
return {
data: null,
error: new AuthUnknownError('Unexpected error in authenticate', error),
}
}
}
/**
* Complete WebAuthn registration flow.
* Performs enrollment, challenge, and verification in a single operation for new credentials.
*
* @experimental This method is experimental and may change in future releases
* @param {Object} params - Registration parameters
* @param {string} params.friendlyName - User-friendly name for the credential
* @param {string} params.rpId - Relying Party ID (defaults to current hostname)
* @param {string[]} params.rpOrigins - Allowed origins (defaults to current origin)
* @param {AbortSignal} params.signal - Optional abort signal
* @param {PublicKeyCredentialCreationOptionsFuture} overrides - Override options for navigator.credentials.create
* @returns {Promise<RequestResult<AuthMFAVerifyResponseData, WebAuthnError | AuthError>>} Registration result
* @see {@link https://w3c.github.io/webauthn/#sctn-registering-a-new-credential W3C WebAuthn Spec - Registration Ceremony}
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions MDN - PublicKeyCredentialCreationOptions}
*/
public async _register(
{
friendlyName,
webauthn: {
rpId = typeof window !== 'undefined' ? window.location.hostname : undefined,
rpOrigins = typeof window !== 'undefined' ? [window.location.origin] : undefined,
signal,
} = {},
}: {
friendlyName: string
webauthn?: {
rpId?: string
rpOrigins?: string[]
signal?: AbortSignal
}
},
overrides?: Partial<PublicKeyCredentialCreationOptionsFuture>
): Promise<RequestResult<AuthMFAVerifyResponseData, WebAuthnError | AuthError>> {
if (!rpId) {
return {
data: null,
error: new AuthError('rpId is required for WebAuthn registration'),
}
}
try {
if (!browserSupportsWebAuthn()) {
return {
data: null,
error: new AuthUnknownError('Browser does not support WebAuthn', null),
}
}
// Enroll factor
const { data: factor, error: enrollError } = await this._enroll({
friendlyName,
})
if (!factor) {
await this.client.mfa
.listFactors()
.then((factors) =>
factors.data?.all.find(
(v) =>
v.factor_type === 'webauthn' &&
v.friendly_name === friendlyName &&
v.status !== 'unverified'
)
)
.then((factor) => (factor ? this.client.mfa.unenroll({ factorId: factor?.id }) : void 0))
return { data: null, error: enrollError }
}
// Get challenge and create credential
const { data: challengeResponse, error: challengeError } = await this._challenge(
{
factorId: factor.id,
friendlyName: factor.friendly_name,
webauthn: { rpId, rpOrigins },
signal,
},
{
create: overrides,
}
)
if (!challengeResponse) {
return { data: null, error: challengeError }
}
return this._verify({
factorId: factor.id,
challengeId: challengeResponse.challengeId,
webauthn: {
rpId,
rpOrigins,
type: challengeResponse.webauthn.type,
credential_response: challengeResponse.webauthn.credential_response,
},
})
} catch (error) {
if (isAuthError(error)) {
return { data: null, error }
}
return {
data: null,
error: new AuthUnknownError('Unexpected error in register', error),
}
}
}
}