/** * Authentication utilities for session management * * Provides session refresh functionality that should be called before * making authenticated requests to AT Protocol services. */ /** * Session refresh error types */ export class AuthRequiredError extends Error { constructor(message = 'Authentication required') { super(message) this.name = 'AuthRequiredError' } } export class SessionExpiredError extends Error { constructor(message = 'Your session has expired. Please log in again.') { super(message) this.name = 'SessionExpiredError' } } /** * Refreshes the OAuth session token. * * This should be called before any request that uses AT Protocol OAuth tokens. * The server will use the refresh token to obtain new access tokens if needed. * * @throws {AuthRequiredError} If no session exists * @throws {SessionExpiredError} If the session cannot be refreshed */ export async function refreshSession(): Promise { try { const response = await fetch('/oauth/refresh', { method: 'POST', credentials: 'same-origin', }) if (!response.ok) { if (response.status === 401) { throw new AuthRequiredError() } // For other errors, we'll let them pass through silently // The actual request will handle auth failures console.warn('Session refresh returned non-OK status:', response.status) } } catch (e) { if (e instanceof AuthRequiredError) throw e // Network errors during refresh shouldn't block the request // The actual request will handle auth failures console.warn('Session refresh failed:', e) } } /** * Refreshes authentication before an authenticated request. * Provides user-friendly error messages for session expiration. * * @throws {SessionExpiredError} If session has expired and cannot be refreshed */ export async function refreshAuth(): Promise { try { await refreshSession() } catch (e) { if (e instanceof AuthRequiredError) { throw new SessionExpiredError() } // For other errors, let them pass - the actual request will handle it console.warn('Auth refresh warning:', e) } } /** * Options for authenticated fetch requests */ export interface AuthFetchOptions extends RequestInit { /** * Whether to skip session refresh before the request. * Default: false (always refresh) */ skipRefresh?: boolean } /** * Fetch wrapper that refreshes the session before making authenticated requests. * * Use this for any requests that require AT Protocol OAuth authentication. * * @param url - The URL to fetch * @param options - Fetch options with optional skipRefresh flag * @returns The fetch response * @throws {SessionExpiredError} If authentication fails */ export async function authFetch(url: string, options: AuthFetchOptions = {}): Promise { const { skipRefresh = false, ...fetchOptions } = options if (!skipRefresh) { await refreshAuth() } const response = await fetch(url, { ...fetchOptions, credentials: 'same-origin', }) // Handle authentication failures if (response.status === 401) { throw new SessionExpiredError() } return response } /** * JSON fetch helper that refreshes session and handles JSON parsing. * * @param url - The URL to fetch * @param options - Fetch options * @returns Parsed JSON response * @throws {SessionExpiredError} If authentication fails */ export async function authFetchJson(url: string, options: AuthFetchOptions = {}): Promise { const response = await authFetch(url, options) return response.json() as Promise } /** * POST JSON helper that refreshes session before posting. * * @param url - The URL to post to * @param data - Data to send as JSON body * @param options - Additional fetch options * @returns The fetch response * @throws {SessionExpiredError} If authentication fails */ export async function authPostJson( url: string, data: unknown, options: AuthFetchOptions = {} ): Promise { return authFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...((options.headers as Record) || {}), }, body: JSON.stringify(data), ...options, }) }