/** * Type-safe API client for barazo-api. * Server-side: uses fetch directly with the internal API URL. * Client-side: uses fetch with the public API URL. */ import type { AgeDeclarationResponse, AuthorProfile, AuthSession, AuthUser, CategoriesResponse, CategoryTreeNode, CategoryWithTopicCount, CommunityPreferencesResponse, CommunitySettings, CommunityStats, CommunityPreferenceOverride, CreatePageInput, CreateTopicInput, CreateTopicResponse, InitializeCommunityInput, InitializeResponse, Page, PagesResponse, PublicSettings, SetupStatus, Topic, TopicsResponse, UpdateCommunityPreferenceInput, UpdatePageInput, UpdatePreferencesInput, UpdateTopicInput, UserPreferences, Reply, RepliesResponse, CreateReplyInput, CreateReplyResponse, UpdateReplyInput, SearchResponse, NotificationsResponse, PaginationParams, ModerationReportsResponse, FirstPostQueueResponse, ModerationLogResponse, ModerationThresholds, ReportedUsersResponse, AdminUsersResponse, MaturityRating, Plugin, PluginsResponse, RegistrySearchResponse, OnboardingField, AdminOnboardingFieldsResponse, CreateOnboardingFieldInput, UpdateOnboardingFieldInput, OnboardingStatus, SubmitOnboardingInput, MyReport, MyReportsResponse, UserProfile, CommunityProfile, UpdateCommunityProfileInput, UploadResponse, SybilClustersResponse, SybilClusterDetail, SybilCluster, TrustSeedsResponse, TrustSeed, CreateTrustSeedInput, PdsTrustFactorsResponse, PdsTrustFactor, TrustGraphStatus, BehavioralFlagsResponse, BehavioralFlag, ReactionsResponse, CommunityRule, CommunityRulesResponse, CreateRuleInput, UpdateRuleInput, ReorderRulesInput, } from './types' /** Client: relative URLs (empty string). Server: internal Docker network URL. */ const API_URL = typeof window === 'undefined' ? (process.env.API_INTERNAL_URL ?? 'http://localhost:3000') : '' interface FetchOptions { headers?: Record signal?: AbortSignal method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' body?: unknown } class ApiError extends Error { /** The parsed error code from the API response body (e.g., "Onboarding required") */ public readonly errorCode: string | undefined constructor( public readonly status: number, message: string, errorCode?: string ) { super(message) this.name = 'ApiError' this.errorCode = errorCode } } async function throwApiError(response: Response): Promise { const body = await response.text().catch(() => 'Unknown error') let message = `Request failed (${response.status})` let errorCode: string | undefined try { const parsed = JSON.parse(body) as { error?: string } if (parsed.error) { message = parsed.error errorCode = parsed.error } } catch { if (body && body !== 'Unknown error') { message = body } } throw new ApiError(response.status, message, errorCode) } async function apiFetch(path: string, options: FetchOptions = {}): Promise { const url = `${API_URL}${path}` const hasBody = options.body !== undefined const response = await fetch(url, { method: options.method ?? 'GET', headers: { ...(hasBody ? { 'Content-Type': 'application/json' } : {}), ...options.headers, }, signal: options.signal, ...(hasBody ? { body: JSON.stringify(options.body) } : {}), }) if (!response.ok) { await throwApiError(response) } const contentLength = response.headers.get('content-length') if (response.status === 204 || contentLength === '0') { return undefined as T } const text = await response.text() if (!text) { return undefined as T } return JSON.parse(text) as T } function buildQuery(params: Record): string { const entries = Object.entries(params).filter( (entry): entry is [string, string | number] => entry[1] !== undefined ) if (entries.length === 0) return '' return '?' + new URLSearchParams(entries.map(([k, v]) => [k, String(v)])).toString() } // --- Auth endpoints --- export async function initiateLogin(handle: string): Promise<{ url: string }> { const query = buildQuery({ handle }) const result = await apiFetch<{ url: string }>(`/api/auth/login${query}`) if (!result?.url) { throw new ApiError(502, 'Login endpoint did not return a redirect URL') } return result } export async function initiateCrossPostAuth(token: string): Promise<{ url: string }> { const result = await apiFetch<{ url: string }>('/api/auth/crosspost-authorize', { headers: { Authorization: `Bearer ${token}` }, }) if (!result?.url) { throw new ApiError(502, 'Cross-post auth endpoint did not return a redirect URL') } return result } export function handleCallback(code: string, state: string): Promise { const query = buildQuery({ code, state }) return apiFetch(`/api/auth/callback${query}`) } export async function refreshSession(): Promise { const url = `${API_URL}/api/auth/refresh` const response = await fetch(url, { method: 'POST', credentials: 'include', }) if (!response.ok) { await throwApiError(response) } return response.json() as Promise } export async function logout(accessToken: string): Promise { const url = `${API_URL}/api/auth/session` const response = await fetch(url, { method: 'DELETE', headers: { Authorization: `Bearer ${accessToken}` }, credentials: 'include', }) if (!response.ok && response.status !== 204) { await throwApiError(response) } } export function getCurrentUser(accessToken: string): Promise { return apiFetch('/api/auth/me', { headers: { Authorization: `Bearer ${accessToken}` }, }) } // --- Category endpoints --- export function getCategories(options?: FetchOptions): Promise { return apiFetch('/api/categories', options) } export function getCategoryBySlug( slug: string, options?: FetchOptions ): Promise { return apiFetch(`/api/categories/${encodeURIComponent(slug)}`, options) } // --- Topic endpoints --- export interface GetTopicsParams extends PaginationParams { category?: string sort?: 'latest' | 'popular' } export function getTopics( params: GetTopicsParams = {}, options?: FetchOptions ): Promise { const query = buildQuery({ limit: params.limit, cursor: params.cursor, category: params.category, sort: params.sort, }) return apiFetch(`/api/topics${query}`, options) } /** @deprecated Use getTopicByAuthorAndRkey for author-scoped lookup */ export function getTopicByRkey(rkey: string, options?: FetchOptions): Promise { return apiFetch(`/api/topics/by-rkey/${encodeURIComponent(rkey)}`, options) } export function getTopicByAuthorAndRkey( handle: string, rkey: string, options?: FetchOptions ): Promise { return apiFetch( `/api/topics/by-author-rkey/${encodeURIComponent(handle)}/${encodeURIComponent(rkey)}`, options ) } export function createTopic( input: CreateTopicInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/topics', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}`, }, body: input, }) } export function updateTopic( rkey: string, input: UpdateTopicInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/topics/${encodeURIComponent(rkey)}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}`, }, body: input, }) } // --- Reply endpoints --- export function getReplies( topicUri: string, params: PaginationParams & { depth?: number } = {}, options?: FetchOptions ): Promise { const query = buildQuery({ limit: params.limit, cursor: params.cursor, depth: params.depth, }) return apiFetch( `/api/topics/${encodeURIComponent(topicUri)}/replies${query}`, options ) } export function createReply( topicUri: string, input: CreateReplyInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/topics/${encodeURIComponent(topicUri)}/replies`, { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}`, }, body: input, }) } export function updateReply( uri: string, input: UpdateReplyInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/replies/${encodeURIComponent(uri)}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}`, }, body: input, }) } export function getReplyByAuthorAndRkey( handle: string, rkey: string, options?: FetchOptions ): Promise { return apiFetch( `/api/replies/by-author-rkey/${encodeURIComponent(handle)}/${encodeURIComponent(rkey)}`, options ) } export function deleteReply( uri: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/replies/${encodeURIComponent(uri)}`, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}`, }, }) } // --- Search endpoints --- export interface SearchParams extends PaginationParams { q: string } export function searchContent( params: SearchParams, options?: FetchOptions ): Promise { const query = buildQuery({ q: params.q, limit: params.limit, cursor: params.cursor, }) return apiFetch(`/api/search${query}`, options) } // --- Notification endpoints --- export function getNotifications( accessToken: string, params: PaginationParams = {}, options?: FetchOptions ): Promise { const query = buildQuery({ limit: params.limit, cursor: params.cursor, }) return apiFetch(`/api/notifications${query}`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}`, }, }) } export function markNotificationsRead( accessToken: string, ids: string[], options?: FetchOptions ): Promise { return apiFetch('/api/notifications/read', { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}`, }, body: { ids }, }) } // --- Community endpoints --- export function getCommunitySettings( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/settings', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function getPublicSettings(options?: FetchOptions): Promise { return apiFetch('/api/settings/public', options) } export function getCommunityStats( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/stats', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}`, }, }) } // --- Admin category endpoints --- export function createCategory( input: { name: string slug: string description: string | null parentId: string | null sortOrder: number maturityRating: MaturityRating }, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/categories', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } export function updateCategory( id: string, input: Partial<{ name: string slug: string description: string | null parentId: string | null sortOrder: number maturityRating: MaturityRating }>, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/admin/categories/${encodeURIComponent(id)}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } export function deleteCategory( id: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/admin/categories/${encodeURIComponent(id)}`, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } // --- Moderation endpoints --- export function getModerationReports( accessToken: string, params: PaginationParams = {}, options?: FetchOptions ): Promise { const query = buildQuery({ limit: params.limit, cursor: params.cursor }) return apiFetch(`/api/moderation/reports${query}`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function resolveReport( id: string, resolution: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/moderation/reports/${encodeURIComponent(id)}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: { resolution }, }) } export function getFirstPostQueue( accessToken: string, params: PaginationParams = {}, options?: FetchOptions ): Promise { const query = buildQuery({ limit: params.limit, cursor: params.cursor }) return apiFetch(`/api/moderation/queue${query}`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function resolveFirstPost( id: string, action: 'approved' | 'rejected', accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/moderation/queue/${encodeURIComponent(id)}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: { action }, }) } export function getModerationLog( accessToken: string, params: PaginationParams = {}, options?: FetchOptions ): Promise { const query = buildQuery({ limit: params.limit, cursor: params.cursor }) return apiFetch(`/api/moderation/log${query}`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function getModerationThresholds( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/moderation/thresholds', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function updateModerationThresholds( thresholds: Partial, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/moderation/thresholds', { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: thresholds, }) } export function getReportedUsers( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/reports/users', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } // --- Admin settings endpoints --- export function updateCommunitySettings( settings: Partial, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/settings', { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: settings, }) } // --- Admin user endpoints --- export function getAdminUsers( accessToken: string, params: PaginationParams = {}, options?: FetchOptions ): Promise { const query = buildQuery({ limit: params.limit, cursor: params.cursor }) return apiFetch(`/api/admin/users${query}`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function banUser( did: string, reason: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/moderation/ban', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: { did, action: 'ban', reason }, }) } export function unbanUser(did: string, accessToken: string, options?: FetchOptions): Promise { return apiFetch('/api/moderation/ban', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: { did, action: 'unban' }, }) } // --- Plugin endpoints --- export function getPlugins(accessToken: string, options?: FetchOptions): Promise { return apiFetch('/api/plugins', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function togglePlugin( id: string, enabled: boolean, accessToken: string, options?: FetchOptions ): Promise { return apiFetch( `/api/plugins/${encodeURIComponent(id)}/${enabled ? 'enable' : 'disable'}`, { ...options, method: 'PATCH', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, } ) } export function updatePluginSettings( id: string, settings: Record, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/plugins/${encodeURIComponent(id)}/settings`, { ...options, method: 'PATCH', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: settings, }) } export function uninstallPlugin( id: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/plugins/${encodeURIComponent(id)}`, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function installPlugin( packageName: string, version: string | undefined, accessToken: string, options?: FetchOptions ): Promise<{ plugin: Plugin }> { return apiFetch<{ plugin: Plugin }>('/api/plugins/install', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: { packageName, ...(version ? { version } : {}) }, }) } export function searchPluginRegistry( params: { q?: string; category?: string; source?: string }, options?: FetchOptions ): Promise { const searchParams = new URLSearchParams() if (params.q) searchParams.set('q', params.q) if (params.category) searchParams.set('category', params.category) if (params.source) searchParams.set('source', params.source) const query = searchParams.toString() return apiFetch( `/api/plugins/registry/search${query ? `?${query}` : ''}`, { ...options, } ) } export function getFeaturedPlugins(options?: FetchOptions): Promise { return apiFetch('/api/plugins/registry/featured', { ...options, }) } // --- User Preference endpoints --- export function getPreferences( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/users/me/preferences', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function updatePreferences( input: UpdatePreferencesInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/users/me/preferences', { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } export function declareAge( declaredAge: number, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/users/me/age-declaration', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: { declaredAge }, }) } export function resolveHandles( handles: string[], accessToken: string, options?: FetchOptions ): Promise<{ users: AuthorProfile[] }> { const qs = encodeURIComponent(handles.join(',')) return apiFetch<{ users: AuthorProfile[] }>(`/api/users/resolve-handles?handles=${qs}`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } // --- Per-Community Preference endpoints --- export function getCommunityPreferences( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/users/me/preferences/communities', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function updateCommunityPreference( communityDid: string, input: UpdateCommunityPreferenceInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch( `/api/users/me/preferences/communities/${encodeURIComponent(communityDid)}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, } ) } // --- Block/Mute endpoints --- export function blockUser( did: string, accessToken: string, options?: FetchOptions ): Promise<{ success: boolean }> { return apiFetch<{ success: boolean }>(`/api/users/me/block/${encodeURIComponent(did)}`, { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function unblockUser( did: string, accessToken: string, options?: FetchOptions ): Promise<{ success: boolean }> { return apiFetch<{ success: boolean }>(`/api/users/me/block/${encodeURIComponent(did)}`, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function muteUser( did: string, accessToken: string, options?: FetchOptions ): Promise<{ success: boolean }> { return apiFetch<{ success: boolean }>(`/api/users/me/mute/${encodeURIComponent(did)}`, { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function unmuteUser( did: string, accessToken: string, options?: FetchOptions ): Promise<{ success: boolean }> { return apiFetch<{ success: boolean }>(`/api/users/me/mute/${encodeURIComponent(did)}`, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } // --- Admin onboarding field endpoints --- export function getOnboardingFields( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/onboarding-fields', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function createOnboardingField( input: CreateOnboardingFieldInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/onboarding-fields', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } export function updateOnboardingField( id: string, input: UpdateOnboardingFieldInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/admin/onboarding-fields/${encodeURIComponent(id)}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } export function deleteOnboardingField( id: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/admin/onboarding-fields/${encodeURIComponent(id)}`, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function reorderOnboardingFields( fields: Array<{ id: string; sortOrder: number }>, accessToken: string, options?: FetchOptions ): Promise<{ success: boolean }> { return apiFetch<{ success: boolean }>('/api/admin/onboarding-fields/reorder', { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: fields, }) } // --- User onboarding endpoints --- export function getOnboardingStatus( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/onboarding/status', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function submitOnboarding( input: SubmitOnboardingInput, accessToken: string, options?: FetchOptions ): Promise<{ success: boolean }> { return apiFetch<{ success: boolean }>('/api/onboarding/submit', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input.responses, }) } // --- My Reports + Appeals endpoints --- export function getMyReports( accessToken: string, params: PaginationParams = {}, options?: FetchOptions ): Promise { const query = buildQuery({ limit: params.limit, cursor: params.cursor }) return apiFetch(`/api/moderation/my-reports${query}`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function submitAppeal( reportId: number, reason: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch( `/api/moderation/reports/${encodeURIComponent(String(reportId))}/appeal`, { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: { reason }, } ) } // --- User Profile endpoints --- export function getUserProfile( handle: string, communityDid?: string, options?: FetchOptions ): Promise { const query = buildQuery({ communityDid }) return apiFetch(`/api/users/${encodeURIComponent(handle)}${query}`, options) } // --- Community Profile endpoints --- export function getCommunityProfile( communityDid: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch( `/api/communities/${encodeURIComponent(communityDid)}/profile`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` } } ) } export function updateCommunityProfile( communityDid: string, input: UpdateCommunityProfileInput, accessToken: string, options?: FetchOptions ): Promise<{ success: boolean }> { return apiFetch<{ success: boolean }>( `/api/communities/${encodeURIComponent(communityDid)}/profile`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, } ) } export function resetCommunityProfile( communityDid: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/communities/${encodeURIComponent(communityDid)}/profile`, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } // --- Upload endpoints (use FormData, not JSON) --- export async function uploadCommunityAvatar( communityDid: string, file: File, accessToken: string ): Promise { const form = new FormData() form.append('file', file) const url = `${API_URL}/api/communities/${encodeURIComponent(communityDid)}/profile/avatar` const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, body: form, }) if (!response.ok) { await throwApiError(response) } return response.json() as Promise } export async function uploadCommunityBanner( communityDid: string, file: File, accessToken: string ): Promise { const form = new FormData() form.append('file', file) const url = `${API_URL}/api/communities/${encodeURIComponent(communityDid)}/profile/banner` const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, body: form, }) if (!response.ok) { await throwApiError(response) } return response.json() as Promise } // --- Admin design upload endpoints (use FormData, not JSON) --- export async function uploadCommunityLogo( file: File, accessToken: string ): Promise { const form = new FormData() form.append('file', file) const url = `${API_URL}/api/admin/design/logo` const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, body: form, }) if (!response.ok) { await throwApiError(response) } return response.json() as Promise } export async function uploadHeaderLogo(file: File, accessToken: string): Promise { const form = new FormData() form.append('file', file) const url = `${API_URL}/api/admin/design/header-logo` const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, body: form, }) if (!response.ok) { await throwApiError(response) } return response.json() as Promise } export async function uploadCommunityFavicon( file: File, accessToken: string ): Promise { const form = new FormData() form.append('file', file) const url = `${API_URL}/api/admin/design/favicon` const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, body: form, }) if (!response.ok) { await throwApiError(response) } return response.json() as Promise } // --- Sybil Detection endpoints --- export function getSybilClusters( accessToken: string, params: { status?: string } = {}, options?: FetchOptions ): Promise { const query = buildQuery({ status: params.status }) return apiFetch(`/api/admin/sybil-clusters${query}`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function getSybilClusterDetail( id: number, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/admin/sybil-clusters/${id}`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function updateSybilClusterStatus( id: number, status: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/admin/sybil-clusters/${id}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: { status }, }) } export function getTrustSeeds( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/trust-seeds', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function createTrustSeed( input: CreateTrustSeedInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/trust-seeds', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } export function deleteTrustSeed( id: number, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/admin/trust-seeds/${id}`, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function getPdsTrustFactors( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/pds-trust', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function updatePdsTrustFactor( pdsHost: string, trustFactor: number, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/pds-trust', { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: { pdsHost, trustFactor }, }) } export function getTrustGraphStatus( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/trust-graph/status', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function recomputeTrustGraph( accessToken: string, options?: FetchOptions ): Promise<{ message: string }> { return apiFetch<{ message: string }>('/api/admin/trust-graph/recompute', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function getBehavioralFlags( accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/behavioral-flags', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function updateBehavioralFlag( id: number, status: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/admin/behavioral-flags/${id}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: { status }, }) } // --- Setup endpoints --- export function getSetupStatus(options?: FetchOptions): Promise { return apiFetch('/api/setup/status', options) } export function initializeCommunity( input: InitializeCommunityInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/setup/initialize', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } // --- Reaction endpoints --- export interface CreateReactionInput { subjectUri: string subjectCid: string type: string } export interface CreateReactionResponse { uri: string cid: string rkey: string type: string subjectUri: string createdAt: string } export function createReaction( input: CreateReactionInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/reactions', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } export function deleteReaction( uri: string, accessToken: string, options?: FetchOptions ): Promise { const url = `/api/reactions/${encodeURIComponent(uri)}` return apiFetch(url, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function getReactions( subjectUri: string, params: { type?: string; cursor?: string; limit?: number } = {}, options?: FetchOptions ): Promise { const query = buildQuery({ subjectUri, type: params.type, cursor: params.cursor, limit: params.limit, }) return apiFetch(`/api/reactions${query}`, options) } // --- Page endpoints (public) --- export function getPages(options?: FetchOptions): Promise { return apiFetch('/api/pages', options) } export function getPageBySlug(slug: string, options?: FetchOptions): Promise { return apiFetch(`/api/pages/${encodeURIComponent(slug)}`, options) } // --- Admin page endpoints --- export function getAdminPages(accessToken: string, options?: FetchOptions): Promise { return apiFetch('/api/admin/pages', { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function getAdminPage( id: string, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/admin/pages/${encodeURIComponent(id)}`, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } export function createPage( input: CreatePageInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch('/api/admin/pages', { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } export function updatePage( id: string, input: UpdatePageInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/admin/pages/${encodeURIComponent(id)}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } export function deletePage(id: string, accessToken: string, options?: FetchOptions): Promise { return apiFetch(`/api/admin/pages/${encodeURIComponent(id)}`, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } // --- Moderation action endpoints --- export interface PinTopicResponse { uri: string isPinned: boolean pinnedScope: 'category' | 'forum' | null pinnedAt: string | null } export function pinTopic( topicUri: string, params: { scope?: 'category' | 'forum'; reason?: string } = {}, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/moderation/pin/${encodeURIComponent(topicUri)}`, { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: params, }) } export interface LockTopicResponse { uri: string isLocked: boolean } export function lockTopic( topicUri: string, params: { reason?: string } = {}, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/moderation/lock/${encodeURIComponent(topicUri)}`, { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: params, }) } export function deleteTopicMod( topicUri: string, params: { reason: string }, accessToken: string, options?: FetchOptions ): Promise<{ uri: string }> { return apiFetch<{ uri: string }>(`/api/moderation/delete/${encodeURIComponent(topicUri)}`, { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: params, }) } // --- Community Rules endpoints --- export function getCommunityRules( communityDid: string, options?: FetchOptions ): Promise { return apiFetch( `/api/communities/${encodeURIComponent(communityDid)}/rules`, options ) } export function createCommunityRule( communityDid: string, input: CreateRuleInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch(`/api/communities/${encodeURIComponent(communityDid)}/rules`, { ...options, method: 'POST', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, }) } export function updateCommunityRule( communityDid: string, ruleId: number, input: UpdateRuleInput, accessToken: string, options?: FetchOptions ): Promise { return apiFetch( `/api/communities/${encodeURIComponent(communityDid)}/rules/${ruleId}`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, } ) } export function deleteCommunityRule( communityDid: string, ruleId: number, accessToken: string, options?: FetchOptions ): Promise<{ success: boolean }> { return apiFetch<{ success: boolean }>( `/api/communities/${encodeURIComponent(communityDid)}/rules/${ruleId}`, { ...options, method: 'DELETE', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, } ) } export function reorderCommunityRules( communityDid: string, input: ReorderRulesInput, accessToken: string, options?: FetchOptions ): Promise<{ success: boolean }> { return apiFetch<{ success: boolean }>( `/api/communities/${encodeURIComponent(communityDid)}/rules/reorder`, { ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, body: input, } ) } export { ApiError }