import { generateOpenApiDocument } from "trpc-to-openapi"; import { z } from "zod/v4"; import { API_KEY_HEADER_NAME } from "@homarr/auth/api-key"; import { appRouter } from "./router/app"; import { boardRouter } from "./router/board"; import { ensureRootCertificateProcedure, getCertificateProcedure } from "./router/certificates/certificate-router"; import { integrationRequestProcedure } from "./router/integration/integration-request"; import { integrationTestStoredConnectionProcedure } from "./router/integration/integration-test-stored-connection"; import { infoRouter } from "./router/info"; import { inviteRouter } from "./router/invite"; import { serverSettingsRouter } from "./router/serverSettings"; import { userRouter } from "./router/user"; import { createTRPCRouter } from "./trpc"; export { normalizeRecursiveJsonSchemasForScalar } from "./open-api-scalar"; export const openApiRouter = createTRPCRouter({ appRouter, boardRouter, infoRouter, inviteRouter, serverSettingsRouter, userRouter, certificates: createTRPCRouter({ getCertificate: getCertificateProcedure, ensureRootCertificate: ensureRootCertificateProcedure, }), integration: createTRPCRouter({ request: integrationRequestProcedure, testConnection: integrationTestStoredConnectionProcedure, }), }); export const openApiDocument = (base: string) => { const document = generateOpenApiDocument(openApiRouter, { title: "Homarr API documentation", version: "1.1.0", baseUrl: base, docsUrl: "https://homarr.dev", securitySchemes: { apikey: { type: "apiKey", name: API_KEY_HEADER_NAME, description: "API key which can be obtained in the Homarr administration dashboard", in: "header", }, }, }); // These reads support anonymous public resources and API-key-scoped private resources. // The generator's boolean `protect` cannot express optional authentication. for (const path of ["/api/boards", "/api/apps/{id}"]) { const operation = document.paths?.[path]?.get; if (operation) operation.security = [{}, { apikey: [] }]; } // Exactly one selector is valid; generated samples otherwise fill all three. const integrationBody = document.paths?.["/api/integrations/request"]?.post?.requestBody; if (integrationBody && "content" in integrationBody) { const json = integrationBody.content["application/json"]; if (json) json.example = { integrationKind: "sonarr", method: "GET", path: "/api/v3/system/status" }; } // The adapter omits root refinements when generating request bodies. const preferenceBody = document.paths?.["/api/users/preferences"]?.patch?.requestBody; if (preferenceBody && "content" in preferenceBody) { const json = preferenceBody.content["application/json"]; if (json) { const schema = z.toJSONSchema(userRouter["_def"].record.updatePreferences["_def"].inputs[0] as z.ZodType, { io: "input", }); delete schema.$schema; json.schema = schema as typeof json.schema; json.example = { colorScheme: "light" }; } } document.tags = ( [ ["boards", "Build dashboards"], ["apps", "Manage apps"], ["users", "Manage users and preferences"], ["settings", "Configure the instance"], ["integrations", "Connect integrations"], ["invites", "Manage invitations"], ["certificates", "Manage trusted certificates"], ["info", "Inspect Homarr"], ] as const ).map(([name, title]) => ({ name, "x-displayName": title })); return document; };