diff --git a/src/module.ts b/src/module.ts index 658173c3..9171ccc5 100644 --- a/src/module.ts +++ b/src/module.ts @@ -166,6 +166,19 @@ export default defineNuxtModule({ from: resolve( `./runtime/composables/${options.provider.type}/useAuthState` ) + }, + // Error utilities for handling authentication errors + { + name: 'AuthError', + from: resolve('./runtime/utils/authError') + }, + { + name: 'createAuthError', + from: resolve('./runtime/utils/authError') + }, + { + name: 'toAuthError', + from: resolve('./runtime/utils/authError') } ]) @@ -189,7 +202,8 @@ export default defineNuxtModule({ [ '// AUTO-GENERATED BY @sidebase/nuxt-auth', 'declare module \'#auth\' {', - ` const { getServerSession, getToken, NuxtAuthHandler }: typeof import('${resolve('./runtime/server/services')}')`, + ` const { getServerSession, getToken, NuxtAuthHandler, AuthError, createAuthError, toAuthError }: typeof import('${resolve('./runtime/server/services')}')`, + ` export type { AuthErrorCode, AuthErrorData } from '${resolve('./runtime/utils/authError')}'`, ...(options.provider.type === 'local' ? [genInterface( 'SessionData', @@ -270,6 +284,8 @@ export default defineNuxtModule({ // Used by nuxt/module-builder for `types.d.ts` generation export type { ModuleOptions, RefreshHandler } +export { AuthError, createAuthError, toAuthError } from './runtime/utils/authError' +export type { AuthErrorCode, AuthErrorData } from './runtime/utils/authError' export interface ModulePublicRuntimeConfig { auth: ModuleOptionsNormalized } diff --git a/src/runtime/composables/authjs/useAuth.ts b/src/runtime/composables/authjs/useAuth.ts index 97d4a98d..1be67e3d 100644 --- a/src/runtime/composables/authjs/useAuth.ts +++ b/src/runtime/composables/authjs/useAuth.ts @@ -10,6 +10,7 @@ import type { CommonUseAuthReturn, GetSessionOptions, SecondarySignInOptions, Si import { useTypedBackendConfig } from '../../helpers' import { getRequestURLWN } from '../common/getRequestURL' import { determineCallbackUrl } from '../../utils/callbackUrl' +import { createAuthError, toAuthError } from '../../utils/authError' import type { SessionData } from './useAuthState' import { navigateToAuthPageWN } from './utils/navigateToAuthPage' import type { NuxtApp } from '#app/nuxt' @@ -78,7 +79,10 @@ export function useAuth(): UseAuthReturn { data, loading, status, - lastRefreshedAt + lastRefreshedAt, + error, + setError, + clearError } = useAuthState() /** @@ -93,12 +97,18 @@ export function useAuth(): UseAuthReturn { options?: SecondarySignInOptions, authorizationParams?: Record ): Promise { + // Clear previous error + clearError() + // 1. Lead to error page if no providers are available const configuredProviders = await getProviders() if (!configuredProviders) { const errorUrl = resolveApiUrlPath('error', runtimeConfig) const navigationResult = await navigateToAuthPageWN(nuxt, errorUrl, true) + const authError = createAuthError.invalidProvider() + setError(authError) + return { // Future AuthJS compat here and in other places // https://authjs.dev/reference/core/errors#invalidprovider @@ -130,6 +140,9 @@ export function useAuth(): UseAuthReturn { if (!selectedProvider) { const navigationResult = await navigateToAuthPageWN(nuxt, hrefSignInAllProviderPage, true) + const authError = createAuthError.invalidProvider(provider) + setError(authError) + return { // https://authjs.dev/reference/core/errors#invalidprovider error: 'InvalidProvider', @@ -173,19 +186,28 @@ export function useAuth(): UseAuthReturn { }, /* proxyCookies = */ true ) - .catch>((error: { data: any }) => error.data) + .catch>((err: { data: any }) => { + // Set error state for network/server errors + const authError = toAuthError(err) + setError(authError) + return err.data + }) - const data = await callWithNuxt(nuxt, fetchSignIn) + const responseData = await callWithNuxt(nuxt, fetchSignIn) if (redirect || !isSupportingReturn) { - const href = data.url ?? callbackUrl + const href = responseData.url ?? callbackUrl const navigationResult = await navigateToAuthPageWN(nuxt, href) // We use `http://_` as a base to allow relative URLs in `callbackUrl`. We only need the `error` query param - const error = new URL(href, 'http://_').searchParams.get('error') + const urlError = new URL(href, 'http://_').searchParams.get('error') + + if (urlError) { + setError(createAuthError.invalidCredentials(urlError)) + } return { - error, + error: urlError, ok: true, status: 302, url: href, @@ -194,14 +216,19 @@ export function useAuth(): UseAuthReturn { } // At this point the request succeeded (i.e., it went through) - const error = new URL(data.url).searchParams.get('error') + const urlError = new URL(responseData.url).searchParams.get('error') + + if (urlError) { + setError(createAuthError.invalidCredentials(urlError)) + } + await getSessionWithNuxt(nuxt) return { - error, + error: urlError, status: 200, ok: true, - url: error ? null : data.url, + url: urlError ? null : responseData.url, navigationResult: undefined, } } @@ -213,11 +240,18 @@ export function useAuth(): UseAuthReturn { // Pass the `Host` header when making internal requests const headers = await getRequestHeaders(nuxt, false) - return _fetch( - nuxt, - '/providers', - { headers } - ) + try { + return await _fetch( + nuxt, + '/providers', + { headers } + ) + } + catch (err) { + const authError = toAuthError(err) + setError(authError) + return null + } } /** @@ -262,6 +296,11 @@ export function useAuth(): UseAuthReturn { data.value = isNonEmptyObject(sessionData) ? sessionData : null loading.value = false + // Clear error on successful session fetch + if (data.value) { + clearError() + } + if (required && status.value === 'unauthenticated') { return onUnauthenticated() } @@ -276,8 +315,21 @@ export function useAuth(): UseAuthReturn { callbackUrl: callbackUrl || callbackUrlFallback } }, - onRequestError: onError, - onResponseError: onError, + onRequestError: ({ error: fetchError }) => { + onError() + const authError = toAuthError(fetchError) + setError(authError) + }, + onResponseError: ({ error: fetchError, response }) => { + onError() + if (response?.status === 401) { + setError(createAuthError.sessionExpired()) + } + else { + const authError = toAuthError(fetchError) + setError(authError) + } + }, headers }, /* proxyCookies = */ true) } @@ -291,6 +343,9 @@ export function useAuth(): UseAuthReturn { * @param options - Options for sign out, e.g., to `redirect` the user to a specific page after sign out has completed */ async function signOut(options?: SignOutOptions) { + // Clear previous error + clearError() + const { callbackUrl: userCallbackUrl, redirect = true } = options ?? {} const csrfToken = await getCsrfTokenWithNuxt(nuxt) @@ -302,23 +357,33 @@ export function useAuth(): UseAuthReturn { ) if (!csrfToken) { + const authError = createAuthError.unknown('Could not fetch CSRF Token for signing out') + setError(authError) throw createError({ statusCode: 400, message: 'Could not fetch CSRF Token for signing out' }) } - const signoutData = await _fetch<{ url: string }>(nuxt, '/signout', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - ...(await getRequestHeaders(nuxt)) - }, - onRequest: ({ options }) => { - options.body = new URLSearchParams({ - csrfToken: csrfToken as string, - callbackUrl, - json: 'true' - }) - } - }).catch(error => error.data) + let signoutData: { url: string } + try { + signoutData = await _fetch<{ url: string }>(nuxt, '/signout', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + ...(await getRequestHeaders(nuxt)) + }, + onRequest: ({ options }) => { + options.body = new URLSearchParams({ + csrfToken: csrfToken as string, + callbackUrl, + json: 'true' + }) + } + }) + } + catch (err) { + const authError = toAuthError(err) + setError(authError) + signoutData = (err as any).data ?? { url: callbackUrl } + } if (redirect) { const url = signoutData.url ?? callbackUrl @@ -350,7 +415,15 @@ export function useAuth(): UseAuthReturn { */ async function getCsrfToken() { const headers = await getRequestHeaders(nuxt) - return _fetch<{ csrfToken: string }>(nuxt, '/csrf', { headers }).then(response => response.csrfToken) + try { + const response = await _fetch<{ csrfToken: string }>(nuxt, '/csrf', { headers }) + return response.csrfToken + } + catch (err) { + const authError = toAuthError(err) + setError(authError) + throw err + } } function getCsrfTokenWithNuxt(nuxt: NuxtApp) { return callWithNuxt(nuxt, getCsrfToken) @@ -360,6 +433,8 @@ export function useAuth(): UseAuthReturn { status, data: readonly(data) as Readonly>, lastRefreshedAt: readonly(lastRefreshedAt), + error: readonly(error), + clearError, getSession, getCsrfToken, getProviders, diff --git a/src/runtime/composables/commonAuthState.ts b/src/runtime/composables/commonAuthState.ts index 5e42b310..6df43520 100644 --- a/src/runtime/composables/commonAuthState.ts +++ b/src/runtime/composables/commonAuthState.ts @@ -1,5 +1,6 @@ import { computed } from 'vue' import type { SessionLastRefreshedAt, SessionStatus } from '../types' +import type { AuthError } from '../utils/authError' import { useState } from '#imports' export function makeCommonAuthState() { @@ -18,6 +19,10 @@ export function makeCommonAuthState() { // If session exists, initialize as not loading const loading = useState('auth:loading', () => false) + + // Error state for tracking authentication errors + const error = useState('auth:error', () => null) + const status = computed(() => { if (loading.value) { return 'loading' @@ -28,10 +33,27 @@ export function makeCommonAuthState() { return 'unauthenticated' }) + /** + * Set error state + */ + function setError(authError: AuthError | null) { + error.value = authError + } + + /** + * Clear error state + */ + function clearError() { + error.value = null + } + return { data, loading, lastRefreshedAt, status, + error, + setError, + clearError } } diff --git a/src/runtime/composables/local/useAuth.ts b/src/runtime/composables/local/useAuth.ts index be2696c8..96e1816b 100644 --- a/src/runtime/composables/local/useAuth.ts +++ b/src/runtime/composables/local/useAuth.ts @@ -4,7 +4,7 @@ import type { CommonUseAuthReturn, GetSessionOptions, SecondarySignInOptions, Si import { jsonPointerGet, objectFromJsonPointer, useTypedBackendConfig } from '../../helpers' import { _fetch } from '../../utils/fetch' import { getRequestURLWN } from '../common/getRequestURL' -import { ERROR_PREFIX } from '../../utils/logger' +import { createAuthError, toAuthError } from '../../utils/authError' import { determineCallbackUrl } from '../../utils/callbackUrl' import { formatToken } from './utils/token' import { useAuthState } from './useAuthState' @@ -61,6 +61,9 @@ export function useAuth(): UseAuthReturn { refreshToken, rawToken, rawRefreshToken, + error, + setError, + clearError, _internal } = useAuthState() @@ -70,26 +73,45 @@ export function useAuth(): UseAuthReturn { signInParams?: Record, signInHeaders?: Record ): Promise { + // Clear previous error + clearError() + const { path, method } = config.endpoints.signIn - const response = await _fetch(nuxt, path, { - method, - body: credentials, - params: signInParams ?? {}, - headers: signInHeaders ?? {} - }, /* proxyCookies = */ true) + + let response: T + try { + response = await _fetch(nuxt, path, { + method, + body: credentials, + params: signInParams ?? {}, + headers: signInHeaders ?? {} + }, /* proxyCookies = */ true) + } + catch (err) { + const authError = toAuthError(err) + // If it's a 401, likely invalid credentials + if (authError.statusCode === 401) { + setError(createAuthError.invalidCredentials()) + } + else { + setError(authError) + } + return + } if (typeof response !== 'object' || response === null) { - console.error(`${ERROR_PREFIX} signIn returned non-object value`) + const authError = createAuthError.unknown('Sign-in returned non-object value') + setError(authError) return } // Extract the access token const extractedToken = jsonPointerGet(response, config.token.signInResponseTokenPointer) if (typeof extractedToken !== 'string') { - console.error( - `${ERROR_PREFIX} string token expected, received instead: ${JSON.stringify(extractedToken)}. ` - + `Tried to find token at ${config.token.signInResponseTokenPointer} in ${JSON.stringify(response)}` + const authError = createAuthError.tokenParseError( + `Failed to extract token at ${config.token.signInResponseTokenPointer}` ) + setError(authError) return } rawToken.value = extractedToken @@ -100,10 +122,10 @@ export function useAuth(): UseAuthReturn { const extractedRefreshToken = jsonPointerGet(response, refreshTokenPointer) if (typeof extractedRefreshToken !== 'string') { - console.error( - `${ERROR_PREFIX} string token expected, received instead: ${JSON.stringify(extractedRefreshToken)}. ` - + `Tried to find refresh token at ${refreshTokenPointer} in ${JSON.stringify(response)}` + const authError = createAuthError.tokenParseError( + `Failed to extract refresh token at ${refreshTokenPointer}` ) + setError(authError) return } rawRefreshToken.value = extractedRefreshToken @@ -130,6 +152,9 @@ export function useAuth(): UseAuthReturn { } async function signOut(signOutOptions?: SignOutOptions): Promise { + // Clear previous error + clearError() + const signOutConfig = config.endpoints.signOut let headers @@ -151,7 +176,14 @@ export function useAuth(): UseAuthReturn { let res: T | undefined if (signOutConfig) { const { path, method } = signOutConfig - res = await _fetch(nuxt, path, { method, headers, body }) + try { + res = await _fetch(nuxt, path, { method, headers, body }) + } + catch (err) { + // Sign-out errors are usually not critical, just log + const authError = toAuthError(err) + setError(authError) + } } const { redirect = true, external } = signOutOptions ?? {} @@ -190,10 +222,18 @@ export function useAuth(): UseAuthReturn { const result = await _fetch(nuxt, path, { method, headers }, /* proxyCookies = */ true) const { dataResponsePointer: sessionDataResponsePointer } = config.session data.value = jsonPointerGet(result, sessionDataResponsePointer) + // Clear error on successful session fetch + clearError() } catch (err) { - if (!data.value && err instanceof Error) { - console.error(`Session: unable to extract session, ${err.message}`) + const authError = toAuthError(err) + + // Check if it's an authentication error + if (authError.statusCode === 401) { + setError(createAuthError.sessionExpired()) + } + else { + setError(createAuthError.sessionFetchError(authError.message, err)) } // Clear all data: Request failed so we must not be authenticated @@ -215,20 +255,31 @@ export function useAuth(): UseAuthReturn { } async function signUp(credentials: Credentials, signUpOptions?: SignUpOptions): Promise { + // Clear previous error + clearError() + const signUpEndpoint = config.endpoints.signUp if (!signUpEndpoint) { - console.warn(`${ERROR_PREFIX} provider.endpoints.signUp is disabled.`) + const authError = createAuthError.endpointDisabled('signUp') + setError(authError) return } const { path, method } = signUpEndpoint - // Holds result from fetch to be returned if signUpOptions?.preventLoginFlow is true - const result = await _fetch(nuxt, path, { - method, - body: credentials - }) + let result: T + try { + result = await _fetch(nuxt, path, { + method, + body: credentials + }) + } + catch (err) { + const authError = toAuthError(err) + setError(authError) + return + } if (signUpOptions?.preventLoginFlow) { return result @@ -243,6 +294,9 @@ export function useAuth(): UseAuthReturn { return getSession(getSessionOptions) } + // Clear previous error + clearError() + const { path, method } = config.refresh.endpoint const refreshRequestTokenPointer = config.refresh.token.refreshRequestTokenPointer @@ -250,20 +304,33 @@ export function useAuth(): UseAuthReturn { [config.token.headerName]: token.value } as HeadersInit) - const response = await _fetch>(nuxt, path, { - method, - headers, - body: objectFromJsonPointer(refreshRequestTokenPointer, refreshToken.value) - }) + let response: Record + try { + response = await _fetch>(nuxt, path, { + method, + headers, + body: objectFromJsonPointer(refreshRequestTokenPointer, refreshToken.value) + }) + } + catch (err) { + const authError = toAuthError(err) + if (authError.statusCode === 401) { + setError(createAuthError.tokenExpired('Refresh token has expired')) + } + else { + setError(authError) + } + return + } // Extract the new token from the refresh response const tokenPointer = config.refresh.token.refreshResponseTokenPointer || config.token.signInResponseTokenPointer const extractedToken = jsonPointerGet(response, tokenPointer) if (typeof extractedToken !== 'string') { - console.error( - `Auth: string token expected, received instead: ${JSON.stringify(extractedToken)}. ` - + `Tried to find token at ${tokenPointer} in ${JSON.stringify(response)}` + const authError = createAuthError.tokenParseError( + `Failed to extract token at ${tokenPointer}` ) + setError(authError) return } @@ -271,10 +338,10 @@ export function useAuth(): UseAuthReturn { const refreshTokenPointer = config.refresh.token.signInResponseRefreshTokenPointer const extractedRefreshToken = jsonPointerGet(response, refreshTokenPointer) if (typeof extractedRefreshToken !== 'string') { - console.error( - `Auth: string token expected, received instead: ${JSON.stringify(extractedRefreshToken)}. ` - + `Tried to find refresh token at ${refreshTokenPointer} in ${JSON.stringify(response)}` + const authError = createAuthError.tokenParseError( + `Failed to extract refresh token at ${refreshTokenPointer}` ) + setError(authError) return } @@ -294,6 +361,8 @@ export function useAuth(): UseAuthReturn { lastRefreshedAt: readonly(lastRefreshedAt), token: readonly(token), refreshToken: readonly(refreshToken), + error: readonly(error), + clearError, getSession, signIn, signOut, diff --git a/src/runtime/server/services/index.ts b/src/runtime/server/services/index.ts index 96a588c0..91d28527 100644 --- a/src/runtime/server/services/index.ts +++ b/src/runtime/server/services/index.ts @@ -1 +1,3 @@ export { getServerSession, getToken, NuxtAuthHandler } from './authjs/nuxtAuthHandler' +export { AuthError, createAuthError, toAuthError } from '../../utils/authError' +export type { AuthErrorCode, AuthErrorData } from '../../utils/authError' diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 0c702a72..4d52322a 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -543,6 +543,10 @@ export interface CommonUseAuthReturn { data: Readonly> lastRefreshedAt: Readonly> status: ComputedRef + /** Current authentication error, if any */ + error: Readonly> + /** Clear the current error */ + clearError: () => void signIn: SignIn signOut: SignOut getSession: GetSessionFunc @@ -554,6 +558,12 @@ export interface CommonUseAuthStateReturn { loading: Ref lastRefreshedAt: Ref status: ComputedRef + /** Current authentication error, if any */ + error: Ref + /** Set an authentication error */ + setError: (error: import('./utils/authError').AuthError | null) => void + /** Clear the current error */ + clearError: () => void } // Common `useAuth` method-types diff --git a/src/runtime/utils/authError.ts b/src/runtime/utils/authError.ts new file mode 100644 index 00000000..81b0351d --- /dev/null +++ b/src/runtime/utils/authError.ts @@ -0,0 +1,277 @@ +/** + * Error codes for authentication operations + */ +export type AuthErrorCode = + // Sign-in errors + | 'INVALID_CREDENTIALS' + | 'INVALID_PROVIDER' + | 'ACCOUNT_DISABLED' + | 'ACCOUNT_LOCKED' + // Token errors + | 'TOKEN_INVALID' + | 'TOKEN_EXPIRED' + | 'TOKEN_MISSING' + | 'TOKEN_PARSE_ERROR' + | 'REFRESH_TOKEN_EXPIRED' + // Session errors + | 'SESSION_EXPIRED' + | 'SESSION_INVALID' + | 'SESSION_FETCH_ERROR' + // Network errors + | 'NETWORK_ERROR' + | 'TIMEOUT' + | 'SERVER_ERROR' + // Configuration errors + | 'ENDPOINT_DISABLED' + | 'MISSING_SECRET' + | 'INVALID_CONFIG' + // Generic + | 'UNKNOWN_ERROR' + +/** + * Authentication error with structured information + */ +export interface AuthErrorData { + /** Error code for programmatic handling */ + code: AuthErrorCode + /** Human-readable error message */ + message: string + /** HTTP status code if applicable */ + statusCode?: number + /** Original error for debugging */ + cause?: Error | unknown + /** Whether the error is recoverable (e.g., can retry) */ + recoverable: boolean + /** Timestamp when error occurred */ + timestamp: Date +} + +/** + * Custom error class for authentication errors + */ +export class AuthError extends Error { + readonly code: AuthErrorCode + readonly statusCode?: number + readonly cause?: Error | unknown + readonly recoverable: boolean + readonly timestamp: Date + + constructor(data: Omit) { + super(data.message) + this.name = 'AuthError' + this.code = data.code + this.statusCode = data.statusCode + this.cause = data.cause + this.recoverable = data.recoverable + this.timestamp = new Date() + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, AuthError) + } + } + + /** + * Convert to plain object for serialization + */ + toJSON(): AuthErrorData { + return { + code: this.code, + message: this.message, + statusCode: this.statusCode, + recoverable: this.recoverable, + timestamp: this.timestamp, + cause: this.cause instanceof Error ? this.cause.message : this.cause + } + } + + /** + * Check if error is of a specific code + */ + is(code: AuthErrorCode): boolean { + return this.code === code + } + + /** + * Check if error is related to authentication (credentials, token, session) + */ + isAuthRelated(): boolean { + return [ + 'INVALID_CREDENTIALS', + 'TOKEN_INVALID', + 'TOKEN_EXPIRED', + 'TOKEN_MISSING', + 'SESSION_EXPIRED', + 'SESSION_INVALID' + ].includes(this.code) + } + + /** + * Check if error is a network issue + */ + isNetworkError(): boolean { + return ['NETWORK_ERROR', 'TIMEOUT', 'SERVER_ERROR'].includes(this.code) + } +} + +/** + * Factory functions for common errors + */ +export const createAuthError = { + invalidCredentials: (message = 'Invalid username or password') => + new AuthError({ + code: 'INVALID_CREDENTIALS', + message, + statusCode: 401, + recoverable: true + }), + + invalidProvider: (provider?: string) => + new AuthError({ + code: 'INVALID_PROVIDER', + message: provider ? `Invalid provider: ${provider}` : 'No valid provider configured', + statusCode: 400, + recoverable: false + }), + + tokenExpired: (message = 'Authentication token has expired') => + new AuthError({ + code: 'TOKEN_EXPIRED', + message, + statusCode: 401, + recoverable: true + }), + + tokenInvalid: (message = 'Invalid authentication token') => + new AuthError({ + code: 'TOKEN_INVALID', + message, + statusCode: 401, + recoverable: true + }), + + tokenMissing: (message = 'Authentication token is missing') => + new AuthError({ + code: 'TOKEN_MISSING', + message, + statusCode: 401, + recoverable: true + }), + + tokenParseError: (message = 'Failed to parse token from response', cause?: unknown) => + new AuthError({ + code: 'TOKEN_PARSE_ERROR', + message, + statusCode: 500, + recoverable: false, + cause + }), + + sessionExpired: (message = 'Your session has expired') => + new AuthError({ + code: 'SESSION_EXPIRED', + message, + statusCode: 401, + recoverable: true + }), + + sessionFetchError: (message = 'Failed to fetch session', cause?: unknown) => + new AuthError({ + code: 'SESSION_FETCH_ERROR', + message, + statusCode: 500, + recoverable: true, + cause + }), + + networkError: (message = 'Network request failed', cause?: unknown) => + new AuthError({ + code: 'NETWORK_ERROR', + message, + recoverable: true, + cause + }), + + serverError: (message = 'Server error occurred', statusCode = 500, cause?: unknown) => + new AuthError({ + code: 'SERVER_ERROR', + message, + statusCode, + recoverable: true, + cause + }), + + endpointDisabled: (endpoint: string) => + new AuthError({ + code: 'ENDPOINT_DISABLED', + message: `Endpoint '${endpoint}' is disabled`, + statusCode: 400, + recoverable: false + }), + + unknown: (message = 'An unknown error occurred', cause?: unknown) => + new AuthError({ + code: 'UNKNOWN_ERROR', + message, + statusCode: 500, + recoverable: false, + cause + }) +} + +/** + * Convert any error to AuthError + */ +export function toAuthError(error: unknown): AuthError { + if (error instanceof AuthError) { + return error + } + + if (error instanceof Error) { + // Check for common error patterns + const message = error.message.toLowerCase() + + if (message.includes('network') || message.includes('fetch')) { + return createAuthError.networkError(error.message, error) + } + + if (message.includes('timeout')) { + return new AuthError({ + code: 'TIMEOUT', + message: error.message, + recoverable: true, + cause: error + }) + } + + return createAuthError.unknown(error.message, error) + } + + // Handle fetch response errors + if (typeof error === 'object' && error !== null) { + const errorObj = error as Record + + if ('statusCode' in errorObj || 'status' in errorObj) { + const statusCode = (errorObj.statusCode || errorObj.status) as number + const message = (errorObj.message || errorObj.statusMessage || 'Request failed') as string + + if (statusCode === 401) { + return createAuthError.tokenInvalid(message) + } + + if (statusCode >= 500) { + return createAuthError.serverError(message, statusCode, error) + } + + return new AuthError({ + code: 'UNKNOWN_ERROR', + message, + statusCode, + recoverable: statusCode < 500, + cause: error + }) + } + } + + return createAuthError.unknown(String(error), error) +}