import { OAuthProcess, apiURL } from './defines' import { type RegistrationResponseJSON, type AuthenticationResponseJSON, } from '@simplewebauthn/browser' import type { // Core types AuthError, User, Flow, Authenticated, AuthenticationMeta, // Request types LoginRequest, SignupRequest, ProviderSignupRequest, ReauthenticateRequest, ChangePasswordRequest, ResetPasswordRequest, MFAAuthenticateRequest, WebAuthnUpdateRequest, // Response types AllauthResponse, AuthenticatedResponse, AuthenticationRequiredResponse, ReauthenticationRequiredResponse, ConfigurationResponse, EmailListResponse, SessionListResponse, AuthenticatorListResponse, ProviderAccountListResponse, TOTPStatusResponse, RecoveryCodesResponse, WebAuthnCreationOptionsResponse, WebAuthnRequestOptionsResponse, EmailVerificationInfoResponse, ErrorResponse, } from './types' export type { AuthError } from './types' // Registration = creating new credentials (signup, add) // Authentication = verifying existing credentials (login, authenticate, reauthenticate) type RegistrationCredential = RegistrationResponseJSON type AuthenticationCredential = AuthenticationResponseJSON /** * Union of all possible auth responses */ export type AuthResponse = | AuthenticatedResponse | AuthenticationRequiredResponse | ReauthenticationRequiredResponse | ConfigurationResponse | EmailListResponse | SessionListResponse | AuthenticatorListResponse | ProviderAccountListResponse | TOTPStatusResponse | RecoveryCodesResponse | WebAuthnCreationOptionsResponse | WebAuthnRequestOptionsResponse | EmailVerificationInfoResponse | ErrorResponse | AllauthResponse export interface AuthDetails { isAuthenticated: boolean requiresReauthentication: boolean user: User | null pendingFlow: Flow | undefined } export const getAuthDetails = (auth: AllauthResponse | null | undefined): AuthDetails => { const meta = auth?.meta as AuthenticationMeta | undefined const isAuthenticated = !!auth && (auth?.status === 200 || (auth?.status === 401 && !!meta?.is_authenticated)) const requiresReauthentication = !!(isAuthenticated && auth?.status === 401) const data = auth?.data as Authenticated | { flows?: Flow[]; user?: User } | undefined const pendingFlow = (data as { flows?: Flow[] })?.flows?.find((flow: Flow) => flow.is_pending) return { isAuthenticated, requiresReauthentication, user: isAuthenticated ? (data as Authenticated)?.user ?? null : null, pendingFlow } } export type BrowserFormAction = (action: string, data: Record) => void type RequestFn = (method: string, path: string, data?: unknown, headers?: Record) => Promise export const createAPI = ( request: RequestFn, browserFormAction?: BrowserFormAction ) => { return { getConfig: async (): Promise => await request('GET', apiURL.CONFIG) as ConfigurationResponse | ErrorResponse, session: { getStatus: async (): Promise => await request('GET', apiURL.SESSION) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse, list: async (): Promise => await request('GET', apiURL.SESSIONS) as SessionListResponse | ErrorResponse, logout: async (): Promise => await request('DELETE', apiURL.SESSION), remove: async (ids: number[]): Promise => await request('DELETE', apiURL.SESSIONS, { sessions: ids }), }, account: { signup: async (data: SignupRequest): Promise => await request('POST', apiURL.SIGNUP, data) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse, login: async (data: LoginRequest): Promise => await request('POST', apiURL.LOGIN, data) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse, reauthenticate: async (data: ReauthenticateRequest): Promise => await request('POST', apiURL.REAUTHENTICATE, data) as AuthenticatedResponse | ErrorResponse, emails: { list: async (): Promise => await request('GET', apiURL.EMAIL) as EmailListResponse | ErrorResponse, add: async (email: string): Promise => await request('POST', apiURL.EMAIL, { email }) as EmailListResponse | ErrorResponse, remove: async (email: string): Promise => await request('DELETE', apiURL.EMAIL, { email }) as EmailListResponse | ErrorResponse, setPrimary: async (email: string): Promise => await request('PATCH', apiURL.EMAIL, { email, primary: true }) as EmailListResponse | ErrorResponse, verification: { dispatch: async (email: string): Promise => await request('PUT', apiURL.EMAIL, { email }), checkKey: async (key: string): Promise => await request('GET', apiURL.VERIFY_EMAIL, undefined, { 'X-Email-Verification-Key': key }) as EmailVerificationInfoResponse | ErrorResponse, confirmKey: async (key: string): Promise => await request('POST', apiURL.VERIFY_EMAIL, { key }) as AuthenticatedResponse | ErrorResponse, } }, password: { set: async (data: ResetPasswordRequest): Promise => await request('POST', apiURL.RESET_PASSWORD, data) as AuthenticatedResponse | ErrorResponse, change: async (data: ChangePasswordRequest): Promise => await request('POST', apiURL.CHANGE_PASSWORD, data), reset: { dispatch: async (email: string): Promise => await request('POST', apiURL.REQUEST_PASSWORD_RESET, { email }), checkKey: async (key: string): Promise => await request('GET', apiURL.RESET_PASSWORD, undefined, { 'X-Password-Reset-Key': key }), } } }, loginCodes: { request: async (email: string): Promise => await request('POST', apiURL.REQUEST_LOGIN_CODE, { email }) as AuthenticationRequiredResponse | ErrorResponse, confirm: async (code: string): Promise => await request('POST', apiURL.CONFIRM_LOGIN_CODE, { code }) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse, }, oauth: { list: async (): Promise => await request('GET', apiURL.PROVIDERS) as ProviderAccountListResponse | ErrorResponse, signup: async (data: ProviderSignupRequest): Promise => await request('POST', apiURL.PROVIDER_SIGNUP, data) as AuthenticatedResponse | ErrorResponse, provider: (providerID: string) => { const buildAuths = (processType: string) => { return { withToken: async (token: string): Promise => await request( 'POST', apiURL.PROVIDER_TOKEN, { provider: providerID, process: processType, token: token, } ) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse, withRedirect: (endpoint: string): void => { if (browserFormAction) { if (!process.env.NEXT_PUBLIC_HOST_URL) { throw new Error('NEXT_PUBLIC_HOST_URL environment variable is not set. OAuth redirects require this to be set at build time.') } browserFormAction( apiURL.REDIRECT_TO_PROVIDER, { provider: providerID, process: processType, callback_url: new URL(`${process.env.NEXT_PUBLIC_HOST_URL}/${endpoint}`).toString(), } ) } } } } return { removeFrom: async (accountUID: string): Promise => await request('DELETE', apiURL.PROVIDERS, { provider: providerID, account: accountUID }) as ProviderAccountListResponse | ErrorResponse, login: buildAuths(OAuthProcess.LOGIN), connect: buildAuths(OAuthProcess.CONNECT), } } }, mfa: { list: async (): Promise => await request('GET', apiURL.AUTHENTICATORS) as AuthenticatorListResponse | ErrorResponse, authenticate: async (code: string): Promise => await request('POST', apiURL.MFA_AUTHENTICATE, { code } as MFAAuthenticateRequest) as AuthenticatedResponse | ErrorResponse, reauthenticate: async (code: string): Promise => await request('POST', apiURL.MFA_REAUTHENTICATE, { code } as MFAAuthenticateRequest) as AuthenticatedResponse | ErrorResponse, trust: async (trust: boolean): Promise => await request('POST', apiURL.MFA_TRUST, { trust }), totp: { getStatus: async (): Promise => await request('GET', apiURL.TOTP_AUTHENTICATOR) as TOTPStatusResponse | ErrorResponse, activate: async (code: string): Promise => await request('POST', apiURL.TOTP_AUTHENTICATOR, { code }) as TOTPStatusResponse | ErrorResponse, deactivate: async (): Promise => await request('DELETE', apiURL.TOTP_AUTHENTICATOR), }, recoveryCodes: { list: async (): Promise => await request('GET', apiURL.RECOVERY_CODES) as RecoveryCodesResponse | ErrorResponse, regenerate: async (): Promise => await request('POST', apiURL.RECOVERY_CODES) as RecoveryCodesResponse | ErrorResponse, } }, webauthn: { signup: async (name: string, credential: RegistrationCredential): Promise => await request('PUT', apiURL.SIGNUP_WEBAUTHN, { name, credential }) as AuthenticatedResponse | ErrorResponse, add: async (name: string, credential: RegistrationCredential): Promise => await request('POST', apiURL.WEBAUTHN_AUTHENTICATOR, { name, credential }), login: async (credential: AuthenticationCredential): Promise => await request('POST', apiURL.LOGIN_WEBAUTHN, { credential }) as AuthenticatedResponse | AuthenticationRequiredResponse | ErrorResponse, authenticate: async (credential: AuthenticationCredential): Promise => await request('POST', apiURL.AUTHENTICATE_WEBAUTHN, { credential }) as AuthenticatedResponse | ErrorResponse, reauthenticate: async (credential: AuthenticationCredential): Promise => await request('POST', apiURL.REAUTHENTICATE_WEBAUTHN, { credential }) as AuthenticatedResponse | ErrorResponse, update: async (id: number, data: Omit): Promise => await request('PUT', apiURL.WEBAUTHN_AUTHENTICATOR, { id, ...data }), delete: async (ids: number[]): Promise => await request('DELETE', apiURL.WEBAUTHN_AUTHENTICATOR, { authenticators: ids }), passkey: { signup: async (email: string): Promise => await request('POST', apiURL.SIGNUP_WEBAUTHN, { email }), confirm: async (): Promise => await request('PUT', apiURL.SIGNUP_WEBAUTHN) as AuthenticatedResponse | ErrorResponse, }, requestOptions: { creation: async (passwordless: boolean): Promise => await request('GET', apiURL.WEBAUTHN_AUTHENTICATOR + (passwordless ? '?passwordless' : '')) as WebAuthnCreationOptionsResponse | ErrorResponse, creationAtSignup: async (): Promise => await request('GET', apiURL.SIGNUP_WEBAUTHN) as WebAuthnCreationOptionsResponse | ErrorResponse, login: async (): Promise => await request('GET', apiURL.LOGIN_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse, authentication: async (): Promise => await request('GET', apiURL.AUTHENTICATE_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse, reauthentication: async (): Promise => await request('GET', apiURL.REAUTHENTICATE_WEBAUTHN) as WebAuthnRequestOptionsResponse | ErrorResponse, }, } } } export type AllauthAPI = ReturnType