Barazo default frontend
barazo.forum
1/**
2 * Type-safe API client for barazo-api.
3 * Server-side: uses fetch directly with the internal API URL.
4 * Client-side: uses fetch with the public API URL.
5 */
6
7import type {
8 AgeDeclarationResponse,
9 AuthorProfile,
10 AuthSession,
11 AuthUser,
12 CategoriesResponse,
13 CategoryTreeNode,
14 CategoryWithTopicCount,
15 CommunityPreferencesResponse,
16 CommunitySettings,
17 CommunityStats,
18 CommunityPreferenceOverride,
19 CreatePageInput,
20 CreateTopicInput,
21 CreateTopicResponse,
22 InitializeCommunityInput,
23 InitializeResponse,
24 Page,
25 PagesResponse,
26 PublicSettings,
27 SetupStatus,
28 Topic,
29 TopicsResponse,
30 UpdateCommunityPreferenceInput,
31 UpdatePageInput,
32 UpdatePreferencesInput,
33 UpdateTopicInput,
34 UserPreferences,
35 Reply,
36 RepliesResponse,
37 CreateReplyInput,
38 CreateReplyResponse,
39 UpdateReplyInput,
40 SearchResponse,
41 NotificationsResponse,
42 PaginationParams,
43 ModerationReportsResponse,
44 FirstPostQueueResponse,
45 ModerationLogResponse,
46 ModerationThresholds,
47 ReportedUsersResponse,
48 AdminUsersResponse,
49 MaturityRating,
50 Plugin,
51 PluginsResponse,
52 RegistrySearchResponse,
53 OnboardingField,
54 AdminOnboardingFieldsResponse,
55 CreateOnboardingFieldInput,
56 UpdateOnboardingFieldInput,
57 OnboardingStatus,
58 SubmitOnboardingInput,
59 MyReport,
60 MyReportsResponse,
61 UserProfile,
62 CommunityProfile,
63 UpdateCommunityProfileInput,
64 UploadResponse,
65 SybilClustersResponse,
66 SybilClusterDetail,
67 SybilCluster,
68 TrustSeedsResponse,
69 TrustSeed,
70 CreateTrustSeedInput,
71 PdsTrustFactorsResponse,
72 PdsTrustFactor,
73 TrustGraphStatus,
74 BehavioralFlagsResponse,
75 BehavioralFlag,
76 ReactionsResponse,
77 CommunityRule,
78 CommunityRulesResponse,
79 CreateRuleInput,
80 UpdateRuleInput,
81 ReorderRulesInput,
82} from './types'
83
84/** Client: relative URLs (empty string). Server: internal Docker network URL. */
85const API_URL =
86 typeof window === 'undefined' ? (process.env.API_INTERNAL_URL ?? 'http://localhost:3000') : ''
87
88interface FetchOptions {
89 headers?: Record<string, string>
90 signal?: AbortSignal
91 method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
92 body?: unknown
93}
94
95class ApiError extends Error {
96 /** The parsed error code from the API response body (e.g., "Onboarding required") */
97 public readonly errorCode: string | undefined
98
99 constructor(
100 public readonly status: number,
101 message: string,
102 errorCode?: string
103 ) {
104 super(message)
105 this.name = 'ApiError'
106 this.errorCode = errorCode
107 }
108}
109
110async function throwApiError(response: Response): Promise<never> {
111 const body = await response.text().catch(() => 'Unknown error')
112 let message = `Request failed (${response.status})`
113 let errorCode: string | undefined
114
115 try {
116 const parsed = JSON.parse(body) as { error?: string }
117 if (parsed.error) {
118 message = parsed.error
119 errorCode = parsed.error
120 }
121 } catch {
122 if (body && body !== 'Unknown error') {
123 message = body
124 }
125 }
126
127 throw new ApiError(response.status, message, errorCode)
128}
129
130async function apiFetch<T>(path: string, options: FetchOptions = {}): Promise<T> {
131 const url = `${API_URL}${path}`
132 const hasBody = options.body !== undefined
133 const response = await fetch(url, {
134 method: options.method ?? 'GET',
135 headers: {
136 ...(hasBody ? { 'Content-Type': 'application/json' } : {}),
137 ...options.headers,
138 },
139 signal: options.signal,
140 ...(hasBody ? { body: JSON.stringify(options.body) } : {}),
141 })
142
143 if (!response.ok) {
144 await throwApiError(response)
145 }
146
147 const contentLength = response.headers.get('content-length')
148 if (response.status === 204 || contentLength === '0') {
149 return undefined as T
150 }
151
152 const text = await response.text()
153 if (!text) {
154 return undefined as T
155 }
156
157 return JSON.parse(text) as T
158}
159
160function buildQuery(params: Record<string, string | number | undefined>): string {
161 const entries = Object.entries(params).filter(
162 (entry): entry is [string, string | number] => entry[1] !== undefined
163 )
164 if (entries.length === 0) return ''
165 return '?' + new URLSearchParams(entries.map(([k, v]) => [k, String(v)])).toString()
166}
167
168// --- Auth endpoints ---
169
170export async function initiateLogin(handle: string): Promise<{ url: string }> {
171 const query = buildQuery({ handle })
172 const result = await apiFetch<{ url: string }>(`/api/auth/login${query}`)
173 if (!result?.url) {
174 throw new ApiError(502, 'Login endpoint did not return a redirect URL')
175 }
176 return result
177}
178
179export async function initiateCrossPostAuth(token: string): Promise<{ url: string }> {
180 const result = await apiFetch<{ url: string }>('/api/auth/crosspost-authorize', {
181 headers: { Authorization: `Bearer ${token}` },
182 })
183 if (!result?.url) {
184 throw new ApiError(502, 'Cross-post auth endpoint did not return a redirect URL')
185 }
186 return result
187}
188
189export function handleCallback(code: string, state: string): Promise<AuthSession> {
190 const query = buildQuery({ code, state })
191 return apiFetch<AuthSession>(`/api/auth/callback${query}`)
192}
193
194export async function refreshSession(): Promise<AuthSession> {
195 const url = `${API_URL}/api/auth/refresh`
196 const response = await fetch(url, {
197 method: 'POST',
198 credentials: 'include',
199 })
200
201 if (!response.ok) {
202 await throwApiError(response)
203 }
204
205 return response.json() as Promise<AuthSession>
206}
207
208export async function logout(accessToken: string): Promise<void> {
209 const url = `${API_URL}/api/auth/session`
210 const response = await fetch(url, {
211 method: 'DELETE',
212 headers: { Authorization: `Bearer ${accessToken}` },
213 credentials: 'include',
214 })
215
216 if (!response.ok && response.status !== 204) {
217 await throwApiError(response)
218 }
219}
220
221export function getCurrentUser(accessToken: string): Promise<AuthUser> {
222 return apiFetch<AuthUser>('/api/auth/me', {
223 headers: { Authorization: `Bearer ${accessToken}` },
224 })
225}
226
227// --- Category endpoints ---
228
229export function getCategories(options?: FetchOptions): Promise<CategoriesResponse> {
230 return apiFetch<CategoriesResponse>('/api/categories', options)
231}
232
233export function getCategoryBySlug(
234 slug: string,
235 options?: FetchOptions
236): Promise<CategoryWithTopicCount> {
237 return apiFetch<CategoryWithTopicCount>(`/api/categories/${encodeURIComponent(slug)}`, options)
238}
239
240// --- Topic endpoints ---
241
242export interface GetTopicsParams extends PaginationParams {
243 category?: string
244 sort?: 'latest' | 'popular'
245}
246
247export function getTopics(
248 params: GetTopicsParams = {},
249 options?: FetchOptions
250): Promise<TopicsResponse> {
251 const query = buildQuery({
252 limit: params.limit,
253 cursor: params.cursor,
254 category: params.category,
255 sort: params.sort,
256 })
257 return apiFetch<TopicsResponse>(`/api/topics${query}`, options)
258}
259
260/** @deprecated Use getTopicByAuthorAndRkey for author-scoped lookup */
261export function getTopicByRkey(rkey: string, options?: FetchOptions): Promise<Topic> {
262 return apiFetch<Topic>(`/api/topics/by-rkey/${encodeURIComponent(rkey)}`, options)
263}
264
265export function getTopicByAuthorAndRkey(
266 handle: string,
267 rkey: string,
268 options?: FetchOptions
269): Promise<Topic> {
270 return apiFetch<Topic>(
271 `/api/topics/by-author-rkey/${encodeURIComponent(handle)}/${encodeURIComponent(rkey)}`,
272 options
273 )
274}
275
276export function createTopic(
277 input: CreateTopicInput,
278 accessToken: string,
279 options?: FetchOptions
280): Promise<CreateTopicResponse> {
281 return apiFetch<CreateTopicResponse>('/api/topics', {
282 ...options,
283 method: 'POST',
284 headers: {
285 ...options?.headers,
286 Authorization: `Bearer ${accessToken}`,
287 },
288 body: input,
289 })
290}
291
292export function updateTopic(
293 rkey: string,
294 input: UpdateTopicInput,
295 accessToken: string,
296 options?: FetchOptions
297): Promise<Topic> {
298 return apiFetch<Topic>(`/api/topics/${encodeURIComponent(rkey)}`, {
299 ...options,
300 method: 'PUT',
301 headers: {
302 ...options?.headers,
303 Authorization: `Bearer ${accessToken}`,
304 },
305 body: input,
306 })
307}
308
309// --- Reply endpoints ---
310
311export function getReplies(
312 topicUri: string,
313 params: PaginationParams & { depth?: number } = {},
314 options?: FetchOptions
315): Promise<RepliesResponse> {
316 const query = buildQuery({
317 limit: params.limit,
318 cursor: params.cursor,
319 depth: params.depth,
320 })
321 return apiFetch<RepliesResponse>(
322 `/api/topics/${encodeURIComponent(topicUri)}/replies${query}`,
323 options
324 )
325}
326
327export function createReply(
328 topicUri: string,
329 input: CreateReplyInput,
330 accessToken: string,
331 options?: FetchOptions
332): Promise<CreateReplyResponse> {
333 return apiFetch<CreateReplyResponse>(`/api/topics/${encodeURIComponent(topicUri)}/replies`, {
334 ...options,
335 method: 'POST',
336 headers: {
337 ...options?.headers,
338 Authorization: `Bearer ${accessToken}`,
339 },
340 body: input,
341 })
342}
343
344export function updateReply(
345 uri: string,
346 input: UpdateReplyInput,
347 accessToken: string,
348 options?: FetchOptions
349): Promise<Reply> {
350 return apiFetch<Reply>(`/api/replies/${encodeURIComponent(uri)}`, {
351 ...options,
352 method: 'PUT',
353 headers: {
354 ...options?.headers,
355 Authorization: `Bearer ${accessToken}`,
356 },
357 body: input,
358 })
359}
360
361export function getReplyByAuthorAndRkey(
362 handle: string,
363 rkey: string,
364 options?: FetchOptions
365): Promise<Reply> {
366 return apiFetch<Reply>(
367 `/api/replies/by-author-rkey/${encodeURIComponent(handle)}/${encodeURIComponent(rkey)}`,
368 options
369 )
370}
371
372export function deleteReply(
373 uri: string,
374 accessToken: string,
375 options?: FetchOptions
376): Promise<void> {
377 return apiFetch<void>(`/api/replies/${encodeURIComponent(uri)}`, {
378 ...options,
379 method: 'DELETE',
380 headers: {
381 ...options?.headers,
382 Authorization: `Bearer ${accessToken}`,
383 },
384 })
385}
386
387// --- Search endpoints ---
388
389export interface SearchParams extends PaginationParams {
390 q: string
391}
392
393export function searchContent(
394 params: SearchParams,
395 options?: FetchOptions
396): Promise<SearchResponse> {
397 const query = buildQuery({
398 q: params.q,
399 limit: params.limit,
400 cursor: params.cursor,
401 })
402 return apiFetch<SearchResponse>(`/api/search${query}`, options)
403}
404
405// --- Notification endpoints ---
406
407export function getNotifications(
408 accessToken: string,
409 params: PaginationParams = {},
410 options?: FetchOptions
411): Promise<NotificationsResponse> {
412 const query = buildQuery({
413 limit: params.limit,
414 cursor: params.cursor,
415 })
416 return apiFetch<NotificationsResponse>(`/api/notifications${query}`, {
417 ...options,
418 headers: {
419 ...options?.headers,
420 Authorization: `Bearer ${accessToken}`,
421 },
422 })
423}
424
425export function markNotificationsRead(
426 accessToken: string,
427 ids: string[],
428 options?: FetchOptions
429): Promise<void> {
430 return apiFetch<void>('/api/notifications/read', {
431 ...options,
432 method: 'PUT',
433 headers: {
434 ...options?.headers,
435 Authorization: `Bearer ${accessToken}`,
436 },
437 body: { ids },
438 })
439}
440
441// --- Community endpoints ---
442
443export function getCommunitySettings(
444 accessToken: string,
445 options?: FetchOptions
446): Promise<CommunitySettings> {
447 return apiFetch<CommunitySettings>('/api/admin/settings', {
448 ...options,
449 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
450 })
451}
452
453export function getPublicSettings(options?: FetchOptions): Promise<PublicSettings> {
454 return apiFetch<PublicSettings>('/api/settings/public', options)
455}
456
457export function getCommunityStats(
458 accessToken: string,
459 options?: FetchOptions
460): Promise<CommunityStats> {
461 return apiFetch<CommunityStats>('/api/admin/stats', {
462 ...options,
463 headers: {
464 ...options?.headers,
465 Authorization: `Bearer ${accessToken}`,
466 },
467 })
468}
469
470// --- Admin category endpoints ---
471
472export function createCategory(
473 input: {
474 name: string
475 slug: string
476 description: string | null
477 parentId: string | null
478 sortOrder: number
479 maturityRating: MaturityRating
480 },
481 accessToken: string,
482 options?: FetchOptions
483): Promise<CategoryTreeNode> {
484 return apiFetch<CategoryTreeNode>('/api/admin/categories', {
485 ...options,
486 method: 'POST',
487 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
488 body: input,
489 })
490}
491
492export function updateCategory(
493 id: string,
494 input: Partial<{
495 name: string
496 slug: string
497 description: string | null
498 parentId: string | null
499 sortOrder: number
500 maturityRating: MaturityRating
501 }>,
502 accessToken: string,
503 options?: FetchOptions
504): Promise<CategoryTreeNode> {
505 return apiFetch<CategoryTreeNode>(`/api/admin/categories/${encodeURIComponent(id)}`, {
506 ...options,
507 method: 'PUT',
508 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
509 body: input,
510 })
511}
512
513export function deleteCategory(
514 id: string,
515 accessToken: string,
516 options?: FetchOptions
517): Promise<void> {
518 return apiFetch<void>(`/api/admin/categories/${encodeURIComponent(id)}`, {
519 ...options,
520 method: 'DELETE',
521 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
522 })
523}
524
525// --- Moderation endpoints ---
526
527export function getModerationReports(
528 accessToken: string,
529 params: PaginationParams = {},
530 options?: FetchOptions
531): Promise<ModerationReportsResponse> {
532 const query = buildQuery({ limit: params.limit, cursor: params.cursor })
533 return apiFetch<ModerationReportsResponse>(`/api/moderation/reports${query}`, {
534 ...options,
535 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
536 })
537}
538
539export function resolveReport(
540 id: string,
541 resolution: string,
542 accessToken: string,
543 options?: FetchOptions
544): Promise<void> {
545 return apiFetch<void>(`/api/moderation/reports/${encodeURIComponent(id)}`, {
546 ...options,
547 method: 'PUT',
548 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
549 body: { resolution },
550 })
551}
552
553export function getFirstPostQueue(
554 accessToken: string,
555 params: PaginationParams = {},
556 options?: FetchOptions
557): Promise<FirstPostQueueResponse> {
558 const query = buildQuery({ limit: params.limit, cursor: params.cursor })
559 return apiFetch<FirstPostQueueResponse>(`/api/moderation/queue${query}`, {
560 ...options,
561 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
562 })
563}
564
565export function resolveFirstPost(
566 id: string,
567 action: 'approved' | 'rejected',
568 accessToken: string,
569 options?: FetchOptions
570): Promise<void> {
571 return apiFetch<void>(`/api/moderation/queue/${encodeURIComponent(id)}`, {
572 ...options,
573 method: 'PUT',
574 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
575 body: { action },
576 })
577}
578
579export function getModerationLog(
580 accessToken: string,
581 params: PaginationParams = {},
582 options?: FetchOptions
583): Promise<ModerationLogResponse> {
584 const query = buildQuery({ limit: params.limit, cursor: params.cursor })
585 return apiFetch<ModerationLogResponse>(`/api/moderation/log${query}`, {
586 ...options,
587 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
588 })
589}
590
591export function getModerationThresholds(
592 accessToken: string,
593 options?: FetchOptions
594): Promise<ModerationThresholds> {
595 return apiFetch<ModerationThresholds>('/api/admin/moderation/thresholds', {
596 ...options,
597 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
598 })
599}
600
601export function updateModerationThresholds(
602 thresholds: Partial<ModerationThresholds>,
603 accessToken: string,
604 options?: FetchOptions
605): Promise<ModerationThresholds> {
606 return apiFetch<ModerationThresholds>('/api/admin/moderation/thresholds', {
607 ...options,
608 method: 'PUT',
609 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
610 body: thresholds,
611 })
612}
613
614export function getReportedUsers(
615 accessToken: string,
616 options?: FetchOptions
617): Promise<ReportedUsersResponse> {
618 return apiFetch<ReportedUsersResponse>('/api/admin/reports/users', {
619 ...options,
620 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
621 })
622}
623
624// --- Admin settings endpoints ---
625
626export function updateCommunitySettings(
627 settings: Partial<CommunitySettings>,
628 accessToken: string,
629 options?: FetchOptions
630): Promise<CommunitySettings> {
631 return apiFetch<CommunitySettings>('/api/admin/settings', {
632 ...options,
633 method: 'PUT',
634 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
635 body: settings,
636 })
637}
638
639// --- Admin user endpoints ---
640
641export function getAdminUsers(
642 accessToken: string,
643 params: PaginationParams = {},
644 options?: FetchOptions
645): Promise<AdminUsersResponse> {
646 const query = buildQuery({ limit: params.limit, cursor: params.cursor })
647 return apiFetch<AdminUsersResponse>(`/api/admin/users${query}`, {
648 ...options,
649 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
650 })
651}
652
653export function banUser(
654 did: string,
655 reason: string,
656 accessToken: string,
657 options?: FetchOptions
658): Promise<void> {
659 return apiFetch<void>('/api/moderation/ban', {
660 ...options,
661 method: 'POST',
662 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
663 body: { did, action: 'ban', reason },
664 })
665}
666
667export function unbanUser(did: string, accessToken: string, options?: FetchOptions): Promise<void> {
668 return apiFetch<void>('/api/moderation/ban', {
669 ...options,
670 method: 'POST',
671 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
672 body: { did, action: 'unban' },
673 })
674}
675
676// --- Plugin endpoints ---
677
678export function getPlugins(accessToken: string, options?: FetchOptions): Promise<PluginsResponse> {
679 return apiFetch<PluginsResponse>('/api/plugins', {
680 ...options,
681 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
682 })
683}
684
685export function togglePlugin(
686 id: string,
687 enabled: boolean,
688 accessToken: string,
689 options?: FetchOptions
690): Promise<void> {
691 return apiFetch<void>(
692 `/api/plugins/${encodeURIComponent(id)}/${enabled ? 'enable' : 'disable'}`,
693 {
694 ...options,
695 method: 'PATCH',
696 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
697 }
698 )
699}
700
701export function updatePluginSettings(
702 id: string,
703 settings: Record<string, boolean | string | number>,
704 accessToken: string,
705 options?: FetchOptions
706): Promise<void> {
707 return apiFetch<void>(`/api/plugins/${encodeURIComponent(id)}/settings`, {
708 ...options,
709 method: 'PATCH',
710 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
711 body: settings,
712 })
713}
714
715export function uninstallPlugin(
716 id: string,
717 accessToken: string,
718 options?: FetchOptions
719): Promise<void> {
720 return apiFetch<void>(`/api/plugins/${encodeURIComponent(id)}`, {
721 ...options,
722 method: 'DELETE',
723 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
724 })
725}
726
727export function installPlugin(
728 packageName: string,
729 version: string | undefined,
730 accessToken: string,
731 options?: FetchOptions
732): Promise<{ plugin: Plugin }> {
733 return apiFetch<{ plugin: Plugin }>('/api/plugins/install', {
734 ...options,
735 method: 'POST',
736 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
737 body: { packageName, ...(version ? { version } : {}) },
738 })
739}
740
741export function searchPluginRegistry(
742 params: { q?: string; category?: string; source?: string },
743 options?: FetchOptions
744): Promise<RegistrySearchResponse> {
745 const searchParams = new URLSearchParams()
746 if (params.q) searchParams.set('q', params.q)
747 if (params.category) searchParams.set('category', params.category)
748 if (params.source) searchParams.set('source', params.source)
749 const query = searchParams.toString()
750 return apiFetch<RegistrySearchResponse>(
751 `/api/plugins/registry/search${query ? `?${query}` : ''}`,
752 {
753 ...options,
754 }
755 )
756}
757
758export function getFeaturedPlugins(options?: FetchOptions): Promise<RegistrySearchResponse> {
759 return apiFetch<RegistrySearchResponse>('/api/plugins/registry/featured', {
760 ...options,
761 })
762}
763
764// --- User Preference endpoints ---
765
766export function getPreferences(
767 accessToken: string,
768 options?: FetchOptions
769): Promise<UserPreferences> {
770 return apiFetch<UserPreferences>('/api/users/me/preferences', {
771 ...options,
772 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
773 })
774}
775
776export function updatePreferences(
777 input: UpdatePreferencesInput,
778 accessToken: string,
779 options?: FetchOptions
780): Promise<UserPreferences> {
781 return apiFetch<UserPreferences>('/api/users/me/preferences', {
782 ...options,
783 method: 'PUT',
784 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
785 body: input,
786 })
787}
788
789export function declareAge(
790 declaredAge: number,
791 accessToken: string,
792 options?: FetchOptions
793): Promise<AgeDeclarationResponse> {
794 return apiFetch<AgeDeclarationResponse>('/api/users/me/age-declaration', {
795 ...options,
796 method: 'POST',
797 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
798 body: { declaredAge },
799 })
800}
801
802export function resolveHandles(
803 handles: string[],
804 accessToken: string,
805 options?: FetchOptions
806): Promise<{ users: AuthorProfile[] }> {
807 const qs = encodeURIComponent(handles.join(','))
808 return apiFetch<{ users: AuthorProfile[] }>(`/api/users/resolve-handles?handles=${qs}`, {
809 ...options,
810 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
811 })
812}
813
814// --- Per-Community Preference endpoints ---
815
816export function getCommunityPreferences(
817 accessToken: string,
818 options?: FetchOptions
819): Promise<CommunityPreferencesResponse> {
820 return apiFetch<CommunityPreferencesResponse>('/api/users/me/preferences/communities', {
821 ...options,
822 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
823 })
824}
825
826export function updateCommunityPreference(
827 communityDid: string,
828 input: UpdateCommunityPreferenceInput,
829 accessToken: string,
830 options?: FetchOptions
831): Promise<CommunityPreferenceOverride> {
832 return apiFetch<CommunityPreferenceOverride>(
833 `/api/users/me/preferences/communities/${encodeURIComponent(communityDid)}`,
834 {
835 ...options,
836 method: 'PUT',
837 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
838 body: input,
839 }
840 )
841}
842
843// --- Block/Mute endpoints ---
844
845export function blockUser(
846 did: string,
847 accessToken: string,
848 options?: FetchOptions
849): Promise<{ success: boolean }> {
850 return apiFetch<{ success: boolean }>(`/api/users/me/block/${encodeURIComponent(did)}`, {
851 ...options,
852 method: 'POST',
853 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
854 })
855}
856
857export function unblockUser(
858 did: string,
859 accessToken: string,
860 options?: FetchOptions
861): Promise<{ success: boolean }> {
862 return apiFetch<{ success: boolean }>(`/api/users/me/block/${encodeURIComponent(did)}`, {
863 ...options,
864 method: 'DELETE',
865 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
866 })
867}
868
869export function muteUser(
870 did: string,
871 accessToken: string,
872 options?: FetchOptions
873): Promise<{ success: boolean }> {
874 return apiFetch<{ success: boolean }>(`/api/users/me/mute/${encodeURIComponent(did)}`, {
875 ...options,
876 method: 'POST',
877 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
878 })
879}
880
881export function unmuteUser(
882 did: string,
883 accessToken: string,
884 options?: FetchOptions
885): Promise<{ success: boolean }> {
886 return apiFetch<{ success: boolean }>(`/api/users/me/mute/${encodeURIComponent(did)}`, {
887 ...options,
888 method: 'DELETE',
889 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
890 })
891}
892
893// --- Admin onboarding field endpoints ---
894
895export function getOnboardingFields(
896 accessToken: string,
897 options?: FetchOptions
898): Promise<AdminOnboardingFieldsResponse> {
899 return apiFetch<AdminOnboardingFieldsResponse>('/api/admin/onboarding-fields', {
900 ...options,
901 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
902 })
903}
904
905export function createOnboardingField(
906 input: CreateOnboardingFieldInput,
907 accessToken: string,
908 options?: FetchOptions
909): Promise<OnboardingField> {
910 return apiFetch<OnboardingField>('/api/admin/onboarding-fields', {
911 ...options,
912 method: 'POST',
913 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
914 body: input,
915 })
916}
917
918export function updateOnboardingField(
919 id: string,
920 input: UpdateOnboardingFieldInput,
921 accessToken: string,
922 options?: FetchOptions
923): Promise<OnboardingField> {
924 return apiFetch<OnboardingField>(`/api/admin/onboarding-fields/${encodeURIComponent(id)}`, {
925 ...options,
926 method: 'PUT',
927 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
928 body: input,
929 })
930}
931
932export function deleteOnboardingField(
933 id: string,
934 accessToken: string,
935 options?: FetchOptions
936): Promise<void> {
937 return apiFetch<void>(`/api/admin/onboarding-fields/${encodeURIComponent(id)}`, {
938 ...options,
939 method: 'DELETE',
940 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
941 })
942}
943
944export function reorderOnboardingFields(
945 fields: Array<{ id: string; sortOrder: number }>,
946 accessToken: string,
947 options?: FetchOptions
948): Promise<{ success: boolean }> {
949 return apiFetch<{ success: boolean }>('/api/admin/onboarding-fields/reorder', {
950 ...options,
951 method: 'PUT',
952 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
953 body: fields,
954 })
955}
956
957// --- User onboarding endpoints ---
958
959export function getOnboardingStatus(
960 accessToken: string,
961 options?: FetchOptions
962): Promise<OnboardingStatus> {
963 return apiFetch<OnboardingStatus>('/api/onboarding/status', {
964 ...options,
965 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
966 })
967}
968
969export function submitOnboarding(
970 input: SubmitOnboardingInput,
971 accessToken: string,
972 options?: FetchOptions
973): Promise<{ success: boolean }> {
974 return apiFetch<{ success: boolean }>('/api/onboarding/submit', {
975 ...options,
976 method: 'POST',
977 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
978 body: input.responses,
979 })
980}
981
982// --- My Reports + Appeals endpoints ---
983
984export function getMyReports(
985 accessToken: string,
986 params: PaginationParams = {},
987 options?: FetchOptions
988): Promise<MyReportsResponse> {
989 const query = buildQuery({ limit: params.limit, cursor: params.cursor })
990 return apiFetch<MyReportsResponse>(`/api/moderation/my-reports${query}`, {
991 ...options,
992 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
993 })
994}
995
996export function submitAppeal(
997 reportId: number,
998 reason: string,
999 accessToken: string,
1000 options?: FetchOptions
1001): Promise<MyReport> {
1002 return apiFetch<MyReport>(
1003 `/api/moderation/reports/${encodeURIComponent(String(reportId))}/appeal`,
1004 {
1005 ...options,
1006 method: 'POST',
1007 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1008 body: { reason },
1009 }
1010 )
1011}
1012
1013// --- User Profile endpoints ---
1014
1015export function getUserProfile(
1016 handle: string,
1017 communityDid?: string,
1018 options?: FetchOptions
1019): Promise<UserProfile> {
1020 const query = buildQuery({ communityDid })
1021 return apiFetch<UserProfile>(`/api/users/${encodeURIComponent(handle)}${query}`, options)
1022}
1023
1024// --- Community Profile endpoints ---
1025
1026export function getCommunityProfile(
1027 communityDid: string,
1028 accessToken: string,
1029 options?: FetchOptions
1030): Promise<CommunityProfile> {
1031 return apiFetch<CommunityProfile>(
1032 `/api/communities/${encodeURIComponent(communityDid)}/profile`,
1033 { ...options, headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` } }
1034 )
1035}
1036
1037export function updateCommunityProfile(
1038 communityDid: string,
1039 input: UpdateCommunityProfileInput,
1040 accessToken: string,
1041 options?: FetchOptions
1042): Promise<{ success: boolean }> {
1043 return apiFetch<{ success: boolean }>(
1044 `/api/communities/${encodeURIComponent(communityDid)}/profile`,
1045 {
1046 ...options,
1047 method: 'PUT',
1048 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1049 body: input,
1050 }
1051 )
1052}
1053
1054export function resetCommunityProfile(
1055 communityDid: string,
1056 accessToken: string,
1057 options?: FetchOptions
1058): Promise<void> {
1059 return apiFetch<void>(`/api/communities/${encodeURIComponent(communityDid)}/profile`, {
1060 ...options,
1061 method: 'DELETE',
1062 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1063 })
1064}
1065
1066// --- Upload endpoints (use FormData, not JSON) ---
1067
1068export async function uploadCommunityAvatar(
1069 communityDid: string,
1070 file: File,
1071 accessToken: string
1072): Promise<UploadResponse> {
1073 const form = new FormData()
1074 form.append('file', file)
1075 const url = `${API_URL}/api/communities/${encodeURIComponent(communityDid)}/profile/avatar`
1076 const response = await fetch(url, {
1077 method: 'POST',
1078 headers: { Authorization: `Bearer ${accessToken}` },
1079 body: form,
1080 })
1081 if (!response.ok) {
1082 await throwApiError(response)
1083 }
1084 return response.json() as Promise<UploadResponse>
1085}
1086
1087export async function uploadCommunityBanner(
1088 communityDid: string,
1089 file: File,
1090 accessToken: string
1091): Promise<UploadResponse> {
1092 const form = new FormData()
1093 form.append('file', file)
1094 const url = `${API_URL}/api/communities/${encodeURIComponent(communityDid)}/profile/banner`
1095 const response = await fetch(url, {
1096 method: 'POST',
1097 headers: { Authorization: `Bearer ${accessToken}` },
1098 body: form,
1099 })
1100 if (!response.ok) {
1101 await throwApiError(response)
1102 }
1103 return response.json() as Promise<UploadResponse>
1104}
1105
1106// --- Admin design upload endpoints (use FormData, not JSON) ---
1107
1108export async function uploadCommunityLogo(
1109 file: File,
1110 accessToken: string
1111): Promise<UploadResponse> {
1112 const form = new FormData()
1113 form.append('file', file)
1114 const url = `${API_URL}/api/admin/design/logo`
1115 const response = await fetch(url, {
1116 method: 'POST',
1117 headers: { Authorization: `Bearer ${accessToken}` },
1118 body: form,
1119 })
1120 if (!response.ok) {
1121 await throwApiError(response)
1122 }
1123 return response.json() as Promise<UploadResponse>
1124}
1125
1126export async function uploadHeaderLogo(file: File, accessToken: string): Promise<UploadResponse> {
1127 const form = new FormData()
1128 form.append('file', file)
1129 const url = `${API_URL}/api/admin/design/header-logo`
1130 const response = await fetch(url, {
1131 method: 'POST',
1132 headers: { Authorization: `Bearer ${accessToken}` },
1133 body: form,
1134 })
1135 if (!response.ok) {
1136 await throwApiError(response)
1137 }
1138 return response.json() as Promise<UploadResponse>
1139}
1140
1141export async function uploadCommunityFavicon(
1142 file: File,
1143 accessToken: string
1144): Promise<UploadResponse> {
1145 const form = new FormData()
1146 form.append('file', file)
1147 const url = `${API_URL}/api/admin/design/favicon`
1148 const response = await fetch(url, {
1149 method: 'POST',
1150 headers: { Authorization: `Bearer ${accessToken}` },
1151 body: form,
1152 })
1153 if (!response.ok) {
1154 await throwApiError(response)
1155 }
1156 return response.json() as Promise<UploadResponse>
1157}
1158
1159// --- Sybil Detection endpoints ---
1160
1161export function getSybilClusters(
1162 accessToken: string,
1163 params: { status?: string } = {},
1164 options?: FetchOptions
1165): Promise<SybilClustersResponse> {
1166 const query = buildQuery({ status: params.status })
1167 return apiFetch<SybilClustersResponse>(`/api/admin/sybil-clusters${query}`, {
1168 ...options,
1169 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1170 })
1171}
1172
1173export function getSybilClusterDetail(
1174 id: number,
1175 accessToken: string,
1176 options?: FetchOptions
1177): Promise<SybilClusterDetail> {
1178 return apiFetch<SybilClusterDetail>(`/api/admin/sybil-clusters/${id}`, {
1179 ...options,
1180 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1181 })
1182}
1183
1184export function updateSybilClusterStatus(
1185 id: number,
1186 status: string,
1187 accessToken: string,
1188 options?: FetchOptions
1189): Promise<SybilCluster> {
1190 return apiFetch<SybilCluster>(`/api/admin/sybil-clusters/${id}`, {
1191 ...options,
1192 method: 'PUT',
1193 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1194 body: { status },
1195 })
1196}
1197
1198export function getTrustSeeds(
1199 accessToken: string,
1200 options?: FetchOptions
1201): Promise<TrustSeedsResponse> {
1202 return apiFetch<TrustSeedsResponse>('/api/admin/trust-seeds', {
1203 ...options,
1204 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1205 })
1206}
1207
1208export function createTrustSeed(
1209 input: CreateTrustSeedInput,
1210 accessToken: string,
1211 options?: FetchOptions
1212): Promise<TrustSeed> {
1213 return apiFetch<TrustSeed>('/api/admin/trust-seeds', {
1214 ...options,
1215 method: 'POST',
1216 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1217 body: input,
1218 })
1219}
1220
1221export function deleteTrustSeed(
1222 id: number,
1223 accessToken: string,
1224 options?: FetchOptions
1225): Promise<void> {
1226 return apiFetch<void>(`/api/admin/trust-seeds/${id}`, {
1227 ...options,
1228 method: 'DELETE',
1229 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1230 })
1231}
1232
1233export function getPdsTrustFactors(
1234 accessToken: string,
1235 options?: FetchOptions
1236): Promise<PdsTrustFactorsResponse> {
1237 return apiFetch<PdsTrustFactorsResponse>('/api/admin/pds-trust', {
1238 ...options,
1239 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1240 })
1241}
1242
1243export function updatePdsTrustFactor(
1244 pdsHost: string,
1245 trustFactor: number,
1246 accessToken: string,
1247 options?: FetchOptions
1248): Promise<PdsTrustFactor> {
1249 return apiFetch<PdsTrustFactor>('/api/admin/pds-trust', {
1250 ...options,
1251 method: 'PUT',
1252 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1253 body: { pdsHost, trustFactor },
1254 })
1255}
1256
1257export function getTrustGraphStatus(
1258 accessToken: string,
1259 options?: FetchOptions
1260): Promise<TrustGraphStatus> {
1261 return apiFetch<TrustGraphStatus>('/api/admin/trust-graph/status', {
1262 ...options,
1263 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1264 })
1265}
1266
1267export function recomputeTrustGraph(
1268 accessToken: string,
1269 options?: FetchOptions
1270): Promise<{ message: string }> {
1271 return apiFetch<{ message: string }>('/api/admin/trust-graph/recompute', {
1272 ...options,
1273 method: 'POST',
1274 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1275 })
1276}
1277
1278export function getBehavioralFlags(
1279 accessToken: string,
1280 options?: FetchOptions
1281): Promise<BehavioralFlagsResponse> {
1282 return apiFetch<BehavioralFlagsResponse>('/api/admin/behavioral-flags', {
1283 ...options,
1284 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1285 })
1286}
1287
1288export function updateBehavioralFlag(
1289 id: number,
1290 status: string,
1291 accessToken: string,
1292 options?: FetchOptions
1293): Promise<BehavioralFlag> {
1294 return apiFetch<BehavioralFlag>(`/api/admin/behavioral-flags/${id}`, {
1295 ...options,
1296 method: 'PUT',
1297 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1298 body: { status },
1299 })
1300}
1301
1302// --- Setup endpoints ---
1303
1304export function getSetupStatus(options?: FetchOptions): Promise<SetupStatus> {
1305 return apiFetch<SetupStatus>('/api/setup/status', options)
1306}
1307
1308export function initializeCommunity(
1309 input: InitializeCommunityInput,
1310 accessToken: string,
1311 options?: FetchOptions
1312): Promise<InitializeResponse> {
1313 return apiFetch<InitializeResponse>('/api/setup/initialize', {
1314 ...options,
1315 method: 'POST',
1316 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1317 body: input,
1318 })
1319}
1320
1321// --- Reaction endpoints ---
1322
1323export interface CreateReactionInput {
1324 subjectUri: string
1325 subjectCid: string
1326 type: string
1327}
1328
1329export interface CreateReactionResponse {
1330 uri: string
1331 cid: string
1332 rkey: string
1333 type: string
1334 subjectUri: string
1335 createdAt: string
1336}
1337
1338export function createReaction(
1339 input: CreateReactionInput,
1340 accessToken: string,
1341 options?: FetchOptions
1342): Promise<CreateReactionResponse> {
1343 return apiFetch<CreateReactionResponse>('/api/reactions', {
1344 ...options,
1345 method: 'POST',
1346 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1347 body: input,
1348 })
1349}
1350
1351export function deleteReaction(
1352 uri: string,
1353 accessToken: string,
1354 options?: FetchOptions
1355): Promise<void> {
1356 const url = `/api/reactions/${encodeURIComponent(uri)}`
1357 return apiFetch<void>(url, {
1358 ...options,
1359 method: 'DELETE',
1360 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1361 })
1362}
1363
1364export function getReactions(
1365 subjectUri: string,
1366 params: { type?: string; cursor?: string; limit?: number } = {},
1367 options?: FetchOptions
1368): Promise<ReactionsResponse> {
1369 const query = buildQuery({
1370 subjectUri,
1371 type: params.type,
1372 cursor: params.cursor,
1373 limit: params.limit,
1374 })
1375 return apiFetch<ReactionsResponse>(`/api/reactions${query}`, options)
1376}
1377
1378// --- Page endpoints (public) ---
1379
1380export function getPages(options?: FetchOptions): Promise<PagesResponse> {
1381 return apiFetch<PagesResponse>('/api/pages', options)
1382}
1383
1384export function getPageBySlug(slug: string, options?: FetchOptions): Promise<Page> {
1385 return apiFetch<Page>(`/api/pages/${encodeURIComponent(slug)}`, options)
1386}
1387
1388// --- Admin page endpoints ---
1389
1390export function getAdminPages(accessToken: string, options?: FetchOptions): Promise<PagesResponse> {
1391 return apiFetch<PagesResponse>('/api/admin/pages', {
1392 ...options,
1393 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1394 })
1395}
1396
1397export function getAdminPage(
1398 id: string,
1399 accessToken: string,
1400 options?: FetchOptions
1401): Promise<Page> {
1402 return apiFetch<Page>(`/api/admin/pages/${encodeURIComponent(id)}`, {
1403 ...options,
1404 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1405 })
1406}
1407
1408export function createPage(
1409 input: CreatePageInput,
1410 accessToken: string,
1411 options?: FetchOptions
1412): Promise<Page> {
1413 return apiFetch<Page>('/api/admin/pages', {
1414 ...options,
1415 method: 'POST',
1416 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1417 body: input,
1418 })
1419}
1420
1421export function updatePage(
1422 id: string,
1423 input: UpdatePageInput,
1424 accessToken: string,
1425 options?: FetchOptions
1426): Promise<Page> {
1427 return apiFetch<Page>(`/api/admin/pages/${encodeURIComponent(id)}`, {
1428 ...options,
1429 method: 'PUT',
1430 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1431 body: input,
1432 })
1433}
1434
1435export function deletePage(id: string, accessToken: string, options?: FetchOptions): Promise<void> {
1436 return apiFetch<void>(`/api/admin/pages/${encodeURIComponent(id)}`, {
1437 ...options,
1438 method: 'DELETE',
1439 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1440 })
1441}
1442
1443// --- Moderation action endpoints ---
1444
1445export interface PinTopicResponse {
1446 uri: string
1447 isPinned: boolean
1448 pinnedScope: 'category' | 'forum' | null
1449 pinnedAt: string | null
1450}
1451
1452export function pinTopic(
1453 topicUri: string,
1454 params: { scope?: 'category' | 'forum'; reason?: string } = {},
1455 accessToken: string,
1456 options?: FetchOptions
1457): Promise<PinTopicResponse> {
1458 return apiFetch<PinTopicResponse>(`/api/moderation/pin/${encodeURIComponent(topicUri)}`, {
1459 ...options,
1460 method: 'POST',
1461 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1462 body: params,
1463 })
1464}
1465
1466export interface LockTopicResponse {
1467 uri: string
1468 isLocked: boolean
1469}
1470
1471export function lockTopic(
1472 topicUri: string,
1473 params: { reason?: string } = {},
1474 accessToken: string,
1475 options?: FetchOptions
1476): Promise<LockTopicResponse> {
1477 return apiFetch<LockTopicResponse>(`/api/moderation/lock/${encodeURIComponent(topicUri)}`, {
1478 ...options,
1479 method: 'POST',
1480 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1481 body: params,
1482 })
1483}
1484
1485export function deleteTopicMod(
1486 topicUri: string,
1487 params: { reason: string },
1488 accessToken: string,
1489 options?: FetchOptions
1490): Promise<{ uri: string }> {
1491 return apiFetch<{ uri: string }>(`/api/moderation/delete/${encodeURIComponent(topicUri)}`, {
1492 ...options,
1493 method: 'POST',
1494 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1495 body: params,
1496 })
1497}
1498
1499// --- Community Rules endpoints ---
1500
1501export function getCommunityRules(
1502 communityDid: string,
1503 options?: FetchOptions
1504): Promise<CommunityRulesResponse> {
1505 return apiFetch<CommunityRulesResponse>(
1506 `/api/communities/${encodeURIComponent(communityDid)}/rules`,
1507 options
1508 )
1509}
1510
1511export function createCommunityRule(
1512 communityDid: string,
1513 input: CreateRuleInput,
1514 accessToken: string,
1515 options?: FetchOptions
1516): Promise<CommunityRule> {
1517 return apiFetch<CommunityRule>(`/api/communities/${encodeURIComponent(communityDid)}/rules`, {
1518 ...options,
1519 method: 'POST',
1520 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1521 body: input,
1522 })
1523}
1524
1525export function updateCommunityRule(
1526 communityDid: string,
1527 ruleId: number,
1528 input: UpdateRuleInput,
1529 accessToken: string,
1530 options?: FetchOptions
1531): Promise<CommunityRule> {
1532 return apiFetch<CommunityRule>(
1533 `/api/communities/${encodeURIComponent(communityDid)}/rules/${ruleId}`,
1534 {
1535 ...options,
1536 method: 'PUT',
1537 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1538 body: input,
1539 }
1540 )
1541}
1542
1543export function deleteCommunityRule(
1544 communityDid: string,
1545 ruleId: number,
1546 accessToken: string,
1547 options?: FetchOptions
1548): Promise<{ success: boolean }> {
1549 return apiFetch<{ success: boolean }>(
1550 `/api/communities/${encodeURIComponent(communityDid)}/rules/${ruleId}`,
1551 {
1552 ...options,
1553 method: 'DELETE',
1554 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1555 }
1556 )
1557}
1558
1559export function reorderCommunityRules(
1560 communityDid: string,
1561 input: ReorderRulesInput,
1562 accessToken: string,
1563 options?: FetchOptions
1564): Promise<{ success: boolean }> {
1565 return apiFetch<{ success: boolean }>(
1566 `/api/communities/${encodeURIComponent(communityDid)}/rules/reorder`,
1567 {
1568 ...options,
1569 method: 'PUT',
1570 headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` },
1571 body: input,
1572 }
1573 )
1574}
1575
1576export { ApiError }