/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ /** @jsxRuntime automatic */ /** @jsxImportSource hono/jsx */ import {type ApiError, parseApiResponse} from '@fluxer/admin/src/api/Errors'; import type {JsonValue} from '@fluxer/admin/src/api/JsonTypes'; import type {Session} from '@fluxer/admin/src/types/App'; import type {AdminConfig} from '@fluxer/admin/src/types/Config'; import {buildEndpointUrl, validateOutboundEndpointUrl} from '@fluxer/hono/src/security/OutboundEndpoint'; export type ApiResult = {ok: true; data: T} | {ok: false; error: ApiError}; export interface RequestOptions { method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; path: string; body?: JsonValue | string; queryParams?: Record; auditLogReason?: string; } export class ApiClient { private session: Session; private apiEndpointUrl: URL; constructor(config: AdminConfig, session: Session) { this.session = session; this.apiEndpointUrl = validateOutboundEndpointUrl(config.apiEndpoint, { name: 'admin.apiEndpoint', allowHttp: config.env !== 'production', allowLocalhost: config.env !== 'production', allowPrivateIpLiterals: config.env !== 'production', }); } private buildHeaders(auditLogReason?: string): Record { const headers: Record = { Authorization: `Bearer ${this.session.accessToken}`, 'Content-Type': 'application/json', }; if (auditLogReason) { headers['X-Audit-Log-Reason'] = auditLogReason; } return headers; } private buildUrl(path: string, queryParams?: Record): string { const baseUrl = buildEndpointUrl(this.apiEndpointUrl, path); if (!queryParams) { return baseUrl; } const params = new URLSearchParams(); for (const [key, value] of Object.entries(queryParams)) { if (value !== undefined && value !== null && value !== '') { params.set(key, String(value)); } } const queryString = params.toString(); return queryString ? `${baseUrl}?${queryString}` : baseUrl; } async request(options: RequestOptions): Promise> { try { const url = this.buildUrl(options.path, options.queryParams); const headers = this.buildHeaders(options.auditLogReason); const fetchOptions: RequestInit = { method: options.method, headers, }; if (options.body !== undefined && options.method !== 'GET') { if (typeof options.body === 'string') { fetchOptions.body = options.body; } else { fetchOptions.body = JSON.stringify(options.body); } } const response = await fetch(url, fetchOptions); if (response.status === 204) { return {ok: true, data: undefined as T}; } if (response.ok) { const contentLength = response.headers.get('content-length'); if (contentLength === '0') { return {ok: true, data: undefined as T}; } try { const data = (await response.json()) as T; return {ok: true, data}; } catch { return {ok: true, data: undefined as T}; } } return parseApiResponse(response); } catch (e) { return { ok: false, error: {type: 'networkError', message: (e as Error).message}, }; } } async get( path: string, queryParams?: Record, ): Promise> { return this.request({ method: 'GET', path, ...(queryParams !== undefined ? {queryParams} : {}), }); } async post(path: string, body?: JsonValue | string, auditLogReason?: string): Promise> { return this.request({ method: 'POST', path, ...(body !== undefined ? {body} : {}), ...(auditLogReason !== undefined ? {auditLogReason} : {}), }); } async postVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise> { return this.request({ method: 'POST', path, ...(body !== undefined ? {body} : {}), ...(auditLogReason !== undefined ? {auditLogReason} : {}), }); } async patch(path: string, body?: JsonValue | string, auditLogReason?: string): Promise> { return this.request({ method: 'PATCH', path, ...(body !== undefined ? {body} : {}), ...(auditLogReason !== undefined ? {auditLogReason} : {}), }); } async patchVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise> { return this.request({ method: 'PATCH', path, ...(body !== undefined ? {body} : {}), ...(auditLogReason !== undefined ? {auditLogReason} : {}), }); } async delete(path: string, body?: JsonValue | string, auditLogReason?: string): Promise> { return this.request({ method: 'DELETE', path, ...(body !== undefined ? {body} : {}), ...(auditLogReason !== undefined ? {auditLogReason} : {}), }); } async deleteVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise> { return this.request({ method: 'DELETE', path, ...(body !== undefined ? {body} : {}), ...(auditLogReason !== undefined ? {auditLogReason} : {}), }); } async put(path: string, body?: JsonValue | string, auditLogReason?: string): Promise> { return this.request({ method: 'PUT', path, ...(body !== undefined ? {body} : {}), ...(auditLogReason !== undefined ? {auditLogReason} : {}), }); } async putVoid(path: string, body?: JsonValue | string, auditLogReason?: string): Promise> { return this.request({ method: 'PUT', path, ...(body !== undefined ? {body} : {}), ...(auditLogReason !== undefined ? {auditLogReason} : {}), }); } } export function createApiClient(config: AdminConfig, session: Session): ApiClient { return new ApiClient(config, session); }