From 6f0dbae121ba76b0c2fc0841459bf03fc9276979 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Fri, 19 Dec 2025 16:37:21 +0100 Subject: [PATCH] refactor(http): move to core package (#4690) --- apps/nextjs/package.json | 1 - apps/tasks/src/overrides.ts | 5 +- packages/api/package.json | 1 - packages/api/src/router/location.ts | 4 +- packages/auth/package.json | 1 - packages/auth/providers/oidc/oidc-provider.ts | 2 +- packages/certificates/eslint.config.js | 9 --- packages/certificates/package.json | 37 ---------- packages/certificates/tsconfig.json | 8 --- .../http/handlers/http-error-handler.ts | 4 +- .../parse/handlers/parse-error-handler.ts | 4 +- packages/common/src/fetch-agent.ts | 41 ----------- packages/common/src/fetch-with-timeout.ts | 16 ----- packages/common/src/index.ts | 1 - packages/common/src/server.ts | 1 - packages/core/package.json | 4 +- .../certificates/hostnames/index.ts | 4 ++ .../src/infrastructure/http/http-agent.ts | 72 +++++++++++++++++++ .../core/src/infrastructure/http/index.ts | 8 +++ .../src/infrastructure/http/request.ts} | 28 ++++++-- .../core/src/infrastructure/http/timeout.ts | 19 +++++ .../core/src/infrastructure/logs/index.ts | 10 ++- .../infrastructure/http/http-agent.spec.ts} | 45 +++++------- .../src/test/infrastructure/logs/index.ts | 49 +++++++++++++ .../repositories/github.icon-repository.ts | 4 +- .../repositories/jsdelivr.icon-repository.ts | 4 +- packages/image-proxy/package.json | 1 - packages/image-proxy/src/index.ts | 2 +- packages/integrations/package.json | 1 - .../adguard-home/adguard-home-integration.ts | 2 +- packages/integrations/src/base/integration.ts | 2 +- .../test-connection-service.ts | 2 +- .../src/codeberg/codeberg-integration.ts | 2 +- .../src/dashdot/dashdot-integration.ts | 2 +- .../src/docker-hub/docker-hub-integration.ts | 2 +- .../aria2/aria2-integration.ts | 2 +- .../deluge/deluge-integration.ts | 2 +- .../nzbget/nzbget-integration.ts | 2 +- .../qbittorrent/qbittorrent-integration.ts | 2 +- .../sabnzbd/sabnzbd-integration.ts | 2 +- .../transmission/transmission-integration.ts | 2 +- .../integrations/src/emby/emby-integration.ts | 2 +- .../github-container-registry-integration.ts | 2 +- .../src/github/github-integration.ts | 2 +- .../src/gitlab/gitlab-integration.ts | 2 +- .../homeassistant-integration.ts | 2 +- .../integrations/src/ical/ical-integration.ts | 2 +- .../src/jellyfin/jellyfin-integration.ts | 2 +- .../linuxserverio-integration.ts | 2 +- .../lidarr/lidarr-integration.ts | 2 +- .../radarr/radarr-integration.ts | 2 +- .../readarr/readarr-integration.ts | 2 +- .../sonarr/sonarr-integration.ts | 2 +- .../media-transcoding/tdarr-integration.ts | 2 +- .../src/nextcloud/nextcloud.integration.ts | 2 +- .../integrations/src/npm/npm-integration.ts | 2 +- .../integrations/src/ntfy/ntfy-integration.ts | 2 +- .../openmediavault-integration.ts | 2 +- .../src/opnsense/opnsense-integration.ts | 2 +- .../src/overseerr/overseerr-integration.ts | 2 +- .../pi-hole/pi-hole-integration-factory.ts | 2 +- .../src/pi-hole/v5/pi-hole-integration-v5.ts | 2 +- .../src/pi-hole/v6/pi-hole-integration-v6.ts | 2 +- .../integrations/src/plex/plex-integration.ts | 2 +- .../src/prowlarr/prowlarr-integration.ts | 2 +- .../src/proxmox/proxmox-integration.ts | 2 +- .../integrations/src/quay/quay-integration.ts | 2 +- .../unifi-controller-integration.ts | 2 +- packages/ping/package.json | 1 - packages/ping/src/index.ts | 36 ++++------ .../src/minecraft-server-status.ts | 4 +- packages/request-handler/src/stock-price.ts | 4 +- .../request-handler/src/update-checker.ts | 4 +- packages/request-handler/src/weather.ts | 4 +- pnpm-lock.yaml | 49 ------------- 75 files changed, 280 insertions(+), 286 deletions(-) delete mode 100644 packages/certificates/eslint.config.js delete mode 100644 packages/certificates/package.json delete mode 100644 packages/certificates/tsconfig.json delete mode 100644 packages/common/src/fetch-agent.ts delete mode 100644 packages/common/src/fetch-with-timeout.ts create mode 100644 packages/core/src/infrastructure/http/http-agent.ts create mode 100644 packages/core/src/infrastructure/http/index.ts rename packages/{certificates/src/server.ts => core/src/infrastructure/http/request.ts} (74%) create mode 100644 packages/core/src/infrastructure/http/timeout.ts rename packages/{common/src/test/fetch-agent.spec.ts => core/src/test/infrastructure/http/http-agent.spec.ts} (66%) create mode 100644 packages/core/src/test/infrastructure/logs/index.ts diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index ca8bd011a..f4a7738e4 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -23,7 +23,6 @@ "@homarr/api": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0", "@homarr/boards": "workspace:^0.1.0", - "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", "@homarr/cron-job-status": "workspace:^0.1.0", diff --git a/apps/tasks/src/overrides.ts b/apps/tasks/src/overrides.ts index f05432c02..684531f81 100644 --- a/apps/tasks/src/overrides.ts +++ b/apps/tasks/src/overrides.ts @@ -1,6 +1,5 @@ import { setGlobalDispatcher } from "undici"; -import { LoggingAgent } from "@homarr/common/server"; +import { UndiciHttpAgent } from "@homarr/core/infrastructure/http"; -const agent = new LoggingAgent(); -setGlobalDispatcher(agent); +setGlobalDispatcher(new UndiciHttpAgent()); diff --git a/packages/api/package.json b/packages/api/package.json index 1d2c3de29..cece7278f 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -22,7 +22,6 @@ "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/auth": "workspace:^0.1.0", - "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", "@homarr/cron-job-api": "workspace:^0.1.0", diff --git a/packages/api/src/router/location.ts b/packages/api/src/router/location.ts index 7379078ed..72779d2e7 100644 --- a/packages/api/src/router/location.ts +++ b/packages/api/src/router/location.ts @@ -1,6 +1,6 @@ import { z } from "zod/v4"; -import { fetchWithTimeout } from "@homarr/common"; +import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { createTRPCRouter, publicProcedure } from "../trpc"; @@ -36,7 +36,7 @@ export const locationRouter = createTRPCRouter({ .input(locationSearchCityInput) .output(locationSearchCityOutput) .query(async ({ input }) => { - const res = await fetchWithTimeout(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`); + const res = await fetchWithTimeoutAsync(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`); return (await res.json()) as z.infer; }), }); diff --git a/packages/auth/package.json b/packages/auth/package.json index f3e3a3235..12049d28b 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -25,7 +25,6 @@ "dependencies": { "@auth/core": "^0.41.1", "@auth/drizzle-adapter": "^1.11.1", - "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", diff --git a/packages/auth/providers/oidc/oidc-provider.ts b/packages/auth/providers/oidc/oidc-provider.ts index 7441b8f60..c706d978f 100644 --- a/packages/auth/providers/oidc/oidc-provider.ts +++ b/packages/auth/providers/oidc/oidc-provider.ts @@ -3,7 +3,7 @@ import type { OIDCConfig } from "@auth/core/providers"; import type { Profile } from "@auth/core/types"; import { customFetch } from "next-auth"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { env } from "../../env"; import { createRedirectUri } from "../../redirect"; diff --git a/packages/certificates/eslint.config.js b/packages/certificates/eslint.config.js deleted file mode 100644 index 5b19b6f8a..000000000 --- a/packages/certificates/eslint.config.js +++ /dev/null @@ -1,9 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [ - { - ignores: [], - }, - ...baseConfig, -]; diff --git a/packages/certificates/package.json b/packages/certificates/package.json deleted file mode 100644 index b48ade91d..000000000 --- a/packages/certificates/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@homarr/certificates", - "version": "0.1.0", - "private": true, - "license": "Apache-2.0", - "type": "module", - "exports": { - "./server": "./src/server.ts" - }, - "typesVersions": { - "*": { - "*": [ - "src/*" - ] - } - }, - "scripts": { - "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", - "typecheck": "tsc --noEmit" - }, - "prettier": "@homarr/prettier-config", - "dependencies": { - "@homarr/common": "workspace:^0.1.0", - "@homarr/core": "workspace:^0.1.0", - "@homarr/db": "workspace:^0.1.0", - "undici": "7.16.0" - }, - "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", - "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.39.2", - "typescript": "^5.9.3" - } -} diff --git a/packages/certificates/tsconfig.json b/packages/certificates/tsconfig.json deleted file mode 100644 index cbe8483d9..000000000 --- a/packages/certificates/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "@homarr/tsconfig/base.json", - "compilerOptions": { - "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" - }, - "include": ["*.ts", "src"], - "exclude": ["node_modules"] -} diff --git a/packages/common/src/errors/http/handlers/http-error-handler.ts b/packages/common/src/errors/http/handlers/http-error-handler.ts index 2773b67cb..4a661199f 100644 --- a/packages/common/src/errors/http/handlers/http-error-handler.ts +++ b/packages/common/src/errors/http/handlers/http-error-handler.ts @@ -1,11 +1,11 @@ -import type { Logger } from "@homarr/core/infrastructure/logs"; +import type { ILogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { AnyRequestError } from "../request-error"; import type { ResponseError } from "../response-error"; export abstract class HttpErrorHandler { - protected logger: Logger; + protected logger: ILogger; constructor(type: string) { this.logger = createLogger({ module: "httpErrorHandler", type }); diff --git a/packages/common/src/errors/parse/handlers/parse-error-handler.ts b/packages/common/src/errors/parse/handlers/parse-error-handler.ts index 6231362f5..428eb36c0 100644 --- a/packages/common/src/errors/parse/handlers/parse-error-handler.ts +++ b/packages/common/src/errors/parse/handlers/parse-error-handler.ts @@ -1,10 +1,10 @@ -import type { Logger } from "@homarr/core/infrastructure/logs"; +import type { ILogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { ParseError } from "../parse-error"; export abstract class ParseErrorHandler { - protected logger: Logger; + protected logger: ILogger; constructor(type: string) { this.logger = createLogger({ module: "parseErrorHandler", type }); } diff --git a/packages/common/src/fetch-agent.ts b/packages/common/src/fetch-agent.ts deleted file mode 100644 index d256bd9df..000000000 --- a/packages/common/src/fetch-agent.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Dispatcher } from "undici"; -import { Agent } from "undici"; - -import { createLogger } from "@homarr/core/infrastructure/logs"; - -// The below import statement initializes dns-caching -import "@homarr/core/infrastructure/dns/init"; - -const logger = createLogger({ module: "fetchAgent" }); - -export class LoggingAgent extends Agent { - constructor(...props: ConstructorParameters) { - super(...props); - } - - dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean { - const path = options.path - .split("/") - .map((segment) => (segment.length >= 32 && !segment.startsWith("?") ? "REDACTED" : segment)) - .join("/"); - const url = new URL(`${options.origin as string}${path}`); - - // The below code should prevent sensitive data from being logged as - // some integrations use query parameters for auth - url.searchParams.forEach((value, key) => { - if (value === "") return; // Skip empty values - if (/^-?\d{1,12}$/.test(value)) return; // Skip small numbers - if (value === "true" || value === "false") return; // Skip boolean values - if (/^[a-zA-Z]{1,12}$/.test(value)) return; // Skip short strings - if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return; // Skip dates - if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return; // Skip date times - - url.searchParams.set(key, "REDACTED"); - }); - - logger.debug( - `Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers ?? {}).length} headers)`, - ); - return super.dispatch(options, handler); - } -} diff --git a/packages/common/src/fetch-with-timeout.ts b/packages/common/src/fetch-with-timeout.ts deleted file mode 100644 index 48353415f..000000000 --- a/packages/common/src/fetch-with-timeout.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Same as fetch, but with a timeout of 10 seconds. - * https://stackoverflow.com/questions/46946380/fetch-api-request-timeout - * @param param0 fetch arguments - * @returns fetch response - */ -export const fetchWithTimeout = (...[url, requestInit]: Parameters) => { - const controller = new AbortController(); - - // 10 seconds timeout: - const timeoutId = setTimeout(() => controller.abort(), 10000); - - return fetch(url, { signal: controller.signal, ...requestInit }).finally(() => { - clearTimeout(timeoutId); - }); -}; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index f7d667155..5f82ea6a3 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -9,7 +9,6 @@ export * from "./id"; export * from "./url"; export * from "./number"; export * from "./error"; -export * from "./fetch-with-timeout"; export * from "./theme"; export * from "./function"; export * from "./id"; diff --git a/packages/common/src/server.ts b/packages/common/src/server.ts index 5d0a7de4f..fd23f7404 100644 --- a/packages/common/src/server.ts +++ b/packages/common/src/server.ts @@ -1,5 +1,4 @@ export * from "./security"; export * from "./encryption"; export * from "./user-agent"; -export * from "./fetch-agent"; export * from "./errors"; diff --git a/packages/core/package.json b/packages/core/package.json index 1a61fa398..6fe412842 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -18,7 +18,9 @@ "./infrastructure/certificates/hostnames/db/sqlite": "./src/infrastructure/certificates/hostnames/db/sqlite.ts", "./infrastructure/certificates/hostnames/db/mysql": "./src/infrastructure/certificates/hostnames/db/mysql.ts", "./infrastructure/certificates/hostnames/db/postgresql": "./src/infrastructure/certificates/hostnames/db/postgresql.ts", - "./infrastructure/dns/init": "./src/infrastructure/dns/init.ts" + "./infrastructure/dns/init": "./src/infrastructure/dns/init.ts", + "./infrastructure/http": "./src/infrastructure/http/index.ts", + "./infrastructure/http/timeout": "./src/infrastructure/http/timeout.ts" }, "typesVersions": { "*": { diff --git a/packages/core/src/infrastructure/certificates/hostnames/index.ts b/packages/core/src/infrastructure/certificates/hostnames/index.ts index 46cc86b7f..afcbf11dd 100644 --- a/packages/core/src/infrastructure/certificates/hostnames/index.ts +++ b/packages/core/src/infrastructure/certificates/hostnames/index.ts @@ -1,3 +1,5 @@ +import type { InferSelectModel } from "drizzle-orm"; + import { createDb } from "../../db"; import { schema } from "./db/schema"; @@ -6,3 +8,5 @@ const db = createDb(schema); export const getTrustedCertificateHostnamesAsync = async () => { return await db.query.trustedCertificateHostnames.findMany(); }; + +export type TrustedCertificateHostname = InferSelectModel; diff --git a/packages/core/src/infrastructure/http/http-agent.ts b/packages/core/src/infrastructure/http/http-agent.ts new file mode 100644 index 000000000..5b2368215 --- /dev/null +++ b/packages/core/src/infrastructure/http/http-agent.ts @@ -0,0 +1,72 @@ +import type { Dispatcher } from "undici"; +import { Agent } from "undici"; + +import type { ILogger } from "@homarr/core/infrastructure/logs"; +import { createLogger } from "@homarr/core/infrastructure/logs"; + +// The below import statement initializes dns-caching +import "@homarr/core/infrastructure/dns/init"; + +interface HttpAgentOptions extends Agent.Options { + logger?: ILogger; +} + +export class UndiciHttpAgent extends Agent { + private logger: ILogger; + + constructor(props?: HttpAgentOptions) { + super(props); + + this.logger = props?.logger ?? createLogger({ module: "httpAgent" }); + } + + dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean { + this.logRequestDispatch(options); + return super.dispatch(options, handler); + } + + private logRequestDispatch(options: Dispatcher.DispatchOptions) { + const path = this.redactPathParams(options.path); + let url = new URL(`${options.origin as string}${path}`); + url = this.redactSearchParams(url); + + this.logger.debug( + `Dispatching request ${url.toString().replaceAll("=&", "&")} (${Object.keys(options.headers ?? {}).length} headers)`, + ); + } + + /** + * Redact path parameters that are longer than 32 characters + * This is to prevent sensitive data from being logged + * @param path path of the request + * @returns redacted path + */ + private redactPathParams(path: string): string { + return path + .split("/") + .map((segment) => (segment.length >= 32 && !segment.startsWith("?") ? "REDACTED" : segment)) + .join("/"); + } + + /** + * Redact sensitive search parameters from the URL. + * It allows certain patterns to remain unredacted. + * Like small numbers, booleans, short strings, dates, and date-times. + * Some integrations use query parameters for auth. + * @param url URL object of the request + * @returns redacted URL object + */ + private redactSearchParams(url: URL): URL { + url.searchParams.forEach((value, key) => { + if (value === "") return; // Skip empty values + if (/^-?\d{1,12}$/.test(value)) return; // Skip small numbers + if (value === "true" || value === "false") return; // Skip boolean values + if (/^[a-zA-Z]{1,12}$/.test(value)) return; // Skip short strings + if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return; // Skip dates + if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)) return; // Skip date times + + url.searchParams.set(key, "REDACTED"); + }); + return url; + } +} diff --git a/packages/core/src/infrastructure/http/index.ts b/packages/core/src/infrastructure/http/index.ts new file mode 100644 index 000000000..572388f2a --- /dev/null +++ b/packages/core/src/infrastructure/http/index.ts @@ -0,0 +1,8 @@ +export { UndiciHttpAgent } from "./http-agent"; +export { + createAxiosCertificateInstanceAsync, + createCertificateAgentAsync, + createCustomCheckServerIdentity, + createHttpsAgentAsync, + fetchWithTrustedCertificatesAsync, +} from "./request"; diff --git a/packages/certificates/src/server.ts b/packages/core/src/infrastructure/http/request.ts similarity index 74% rename from packages/certificates/src/server.ts rename to packages/core/src/infrastructure/http/request.ts index 81d7210b0..b8dcf962e 100644 --- a/packages/certificates/src/server.ts +++ b/packages/core/src/infrastructure/http/request.ts @@ -5,16 +5,17 @@ import axios from "axios"; import type { RequestInfo, RequestInit, Response } from "undici"; import { fetch } from "undici"; -import { LoggingAgent } from "@homarr/common/server"; import { getAllTrustedCertificatesAsync, getTrustedCertificateHostnamesAsync, } from "@homarr/core/infrastructure/certificates"; -import type { InferSelectModel } from "@homarr/db"; -import type { trustedCertificateHostnames } from "@homarr/db/schema"; +import { UndiciHttpAgent } from "@homarr/core/infrastructure/http"; + +import type { TrustedCertificateHostname } from "../certificates/hostnames"; +import { withTimeoutAsync } from "./timeout"; export const createCustomCheckServerIdentity = ( - trustedHostnames: InferSelectModel[], + trustedHostnames: TrustedCertificateHostname[], ): typeof checkServerIdentity => { return (hostname, peerCertificate) => { const matchingTrustedHostnames = trustedHostnames.filter( @@ -32,7 +33,7 @@ export const createCertificateAgentAsync = async (override?: { ca: string | string[]; checkServerIdentity: typeof checkServerIdentity; }) => { - return new LoggingAgent({ + return new UndiciHttpAgent({ connect: override ?? { ca: await getAllTrustedCertificatesAsync(), checkServerIdentity: createCustomCheckServerIdentity(await getTrustedCertificateHostnamesAsync()), @@ -57,8 +58,23 @@ export const createAxiosCertificateInstanceAsync = async ( }); }; -export const fetchWithTrustedCertificatesAsync = async (url: RequestInfo, options?: RequestInit): Promise => { +export const fetchWithTrustedCertificatesAsync = async ( + url: RequestInfo, + options?: RequestInit & { timeout?: number }, +): Promise => { const agent = await createCertificateAgentAsync(undefined); + if (options?.timeout) { + return await withTimeoutAsync( + async (signal) => + fetch(url, { + ...options, + signal, + dispatcher: agent, + }), + options.timeout, + ); + } + return fetch(url, { ...options, dispatcher: agent, diff --git a/packages/core/src/infrastructure/http/timeout.ts b/packages/core/src/infrastructure/http/timeout.ts new file mode 100644 index 000000000..e56bd5e56 --- /dev/null +++ b/packages/core/src/infrastructure/http/timeout.ts @@ -0,0 +1,19 @@ +import type { Response as UndiciResponse } from "undici"; + +// https://stackoverflow.com/questions/46946380/fetch-api-request-timeout +export const withTimeoutAsync = async ( + callback: (signal: AbortSignal) => Promise, + timeout = 10000, +) => { + const controller = new AbortController(); + + const timeoutId = setTimeout(() => controller.abort(), timeout); + + return await callback(controller.signal).finally(() => { + clearTimeout(timeoutId); + }); +}; + +export const fetchWithTimeoutAsync = async (...[url, requestInit]: Parameters) => { + return await withTimeoutAsync((signal) => fetch(url, { ...requestInit, signal })); +}; diff --git a/packages/core/src/infrastructure/logs/index.ts b/packages/core/src/infrastructure/logs/index.ts index 41788262f..df70b5578 100644 --- a/packages/core/src/infrastructure/logs/index.ts +++ b/packages/core/src/infrastructure/logs/index.ts @@ -15,4 +15,12 @@ interface DefaultMetadata { } export const createLogger = (metadata: DefaultMetadata & Record) => logger.child(metadata); -export type Logger = winston.Logger; + +type LogMethod = ((message: string, metadata?: Record) => void) | ((error: unknown) => void); + +export interface ILogger { + debug: LogMethod; + info: LogMethod; + warn: LogMethod; + error: LogMethod; +} diff --git a/packages/common/src/test/fetch-agent.spec.ts b/packages/core/src/test/infrastructure/http/http-agent.spec.ts similarity index 66% rename from packages/common/src/test/fetch-agent.spec.ts rename to packages/core/src/test/infrastructure/http/http-agent.spec.ts index 883632f3b..52d6b09e8 100644 --- a/packages/common/src/test/fetch-agent.spec.ts +++ b/packages/core/src/test/infrastructure/http/http-agent.spec.ts @@ -1,9 +1,9 @@ import type { Dispatcher } from "undici"; import { describe, expect, test, vi } from "vitest"; -import * as logs from "@homarr/core/infrastructure/logs"; +import { UndiciHttpAgent } from "@homarr/core/infrastructure/http"; -import { LoggingAgent } from "../fetch-agent"; +import { TestLogger } from "../logs"; vi.mock("undici", () => { return { @@ -16,37 +16,28 @@ vi.mock("undici", () => { }; }); -vi.mock("@homarr/core/infrastructure/logs", async () => { - const actual: typeof logs = await vi.importActual("@homarr/core/infrastructure/logs"); - return { - ...actual, - createLogger: vi.fn().mockReturnValue({ - debug: vi.fn(), - }), - }; -}); - const REDACTED = "REDACTED"; -const loggerMock = logs.createLogger({ module: "test" }); - -describe("LoggingAgent should log all requests", () => { +describe("UndiciHttpAgent should log all requests", () => { test("should log all requests", () => { // Arrange - const debugSpy = vi.spyOn(loggerMock, "debug"); - const agent = new LoggingAgent(); + const logger = new TestLogger(); + const agent = new UndiciHttpAgent({ logger }); // Act agent.dispatch({ origin: "https://homarr.dev", path: "/", method: "GET" }, {}); // Assert - expect(debugSpy).toHaveBeenCalledWith("Dispatching request https://homarr.dev/ (0 headers)"); + expect(logger.messages).toContainEqual({ + level: "debug", + message: "Dispatching request https://homarr.dev/ (0 headers)", + }); }); test("should show amount of headers", () => { // Arrange - const debugSpy = vi.spyOn(loggerMock, "debug"); - const agent = new LoggingAgent(); + const logger = new TestLogger(); + const agent = new UndiciHttpAgent({ logger }); // Act agent.dispatch( @@ -63,7 +54,7 @@ describe("LoggingAgent should log all requests", () => { ); // Assert - expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining("(2 headers)")); + expect(logger.messages.at(-1)?.message).toContain("(2 headers)"); }); test.each([ @@ -81,14 +72,14 @@ describe("LoggingAgent should log all requests", () => { [`/${"a".repeat(32)}/?param=123`, `/${REDACTED}/?param=123`], ])("should redact sensitive data in url https://homarr.dev%s", (path, expected) => { // Arrange - const debugSpy = vi.spyOn(loggerMock, "debug"); - const agent = new LoggingAgent(); + const logger = new TestLogger(); + const agent = new UndiciHttpAgent({ logger }); // Act agent.dispatch({ origin: "https://homarr.dev", path, method: "GET" }, {}); // Assert - expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${expected} `)); + expect(logger.messages.at(-1)?.message).toContain(` https://homarr.dev${expected} `); }); test.each([ ["empty", "/?empty"], @@ -100,13 +91,13 @@ describe("LoggingAgent should log all requests", () => { ["date times", "/?datetime=2022-01-01T00:00:00.000Z"], ])("should not redact values that are %s", (_reason, path) => { // Arrange - const debugSpy = vi.spyOn(loggerMock, "debug"); - const agent = new LoggingAgent(); + const logger = new TestLogger(); + const agent = new UndiciHttpAgent({ logger }); // Act agent.dispatch({ origin: "https://homarr.dev", path, method: "GET" }, {}); // Assert - expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining(` https://homarr.dev${path} `)); + expect(logger.messages.at(-1)?.message).toContain(` https://homarr.dev${path} `); }); }); diff --git a/packages/core/src/test/infrastructure/logs/index.ts b/packages/core/src/test/infrastructure/logs/index.ts new file mode 100644 index 000000000..1e2c6c282 --- /dev/null +++ b/packages/core/src/test/infrastructure/logs/index.ts @@ -0,0 +1,49 @@ +import type { ILogger } from "@homarr/core/infrastructure/logs"; +import type { LogLevel } from "@homarr/core/infrastructure/logs/constants"; + +interface LogMessage { + level: LogLevel; + message: string; + meta?: Record; +} + +interface LogError { + level: LogLevel; + error: unknown; +} + +type LogEntry = LogMessage | LogError; + +export class TestLogger implements ILogger { + public entries: LogEntry[] = []; + public get messages(): LogMessage[] { + return this.entries.filter((entry) => "message" in entry); + } + public get errors(): LogError[] { + return this.entries.filter((entry) => "error" in entry); + } + + private log(level: LogLevel, param1: unknown, param2?: Record): void { + if (typeof param1 === "string") { + this.entries.push({ level, message: param1, meta: param2 }); + } else { + this.entries.push({ level, error: param1 }); + } + } + + debug(param1: unknown, param2?: Record): void { + this.log("debug", param1, param2); + } + + info(param1: unknown, param2?: Record): void { + this.log("info", param1, param2); + } + + warn(param1: unknown, param2?: Record): void { + this.log("warn", param1, param2); + } + + error(param1: unknown, param2?: Record): void { + this.log("error", param1, param2); + } +} diff --git a/packages/icons/src/repositories/github.icon-repository.ts b/packages/icons/src/repositories/github.icon-repository.ts index 2152a450b..8f6e55dc8 100644 --- a/packages/icons/src/repositories/github.icon-repository.ts +++ b/packages/icons/src/repositories/github.icon-repository.ts @@ -1,6 +1,6 @@ import { parse } from "path"; -import { fetchWithTimeout } from "@homarr/common"; +import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { RepositoryIconGroup } from "../types/repository-icon-group"; @@ -23,7 +23,7 @@ export class GitHubIconRepository extends IconRepository { throw new Error("Repository URLs are required for this repository"); } - const response = await fetchWithTimeout(this.repositoryIndexingUrl); + const response = await fetchWithTimeoutAsync(this.repositoryIndexingUrl); const listOfFiles = (await response.json()) as GitHubApiResponse; return { diff --git a/packages/icons/src/repositories/jsdelivr.icon-repository.ts b/packages/icons/src/repositories/jsdelivr.icon-repository.ts index 2b9bd944b..acff85923 100644 --- a/packages/icons/src/repositories/jsdelivr.icon-repository.ts +++ b/packages/icons/src/repositories/jsdelivr.icon-repository.ts @@ -1,6 +1,6 @@ import { parse } from "path"; -import { fetchWithTimeout } from "@homarr/common"; +import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { RepositoryIconGroup } from "../types/repository-icon-group"; @@ -19,7 +19,7 @@ export class JsdelivrIconRepository extends IconRepository { } protected async getAllIconsInternalAsync(): Promise { - const response = await fetchWithTimeout(this.repositoryIndexingUrl); + const response = await fetchWithTimeoutAsync(this.repositoryIndexingUrl); const listOfFiles = (await response.json()) as JsdelivrApiResponse; return { diff --git a/packages/image-proxy/package.json b/packages/image-proxy/package.json index bb742630e..f6ea7ccf6 100644 --- a/packages/image-proxy/package.json +++ b/packages/image-proxy/package.json @@ -22,7 +22,6 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { - "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", diff --git a/packages/image-proxy/src/index.ts b/packages/image-proxy/src/index.ts index db3fc1a5b..736a64dcb 100644 --- a/packages/image-proxy/src/index.ts +++ b/packages/image-proxy/src/index.ts @@ -1,8 +1,8 @@ import bcrypt from "bcrypt"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { createId } from "@homarr/common"; import { decryptSecret, encryptSecret } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { createGetSetChannel } from "@homarr/redis"; diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 46b2a775b..71bada623 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -29,7 +29,6 @@ "@ctrl/qbittorrent": "^9.11.0", "@ctrl/transmission": "^7.4.0", "@gitbeaker/rest": "^43.8.0", - "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", diff --git a/packages/integrations/src/adguard-home/adguard-home-integration.ts b/packages/integrations/src/adguard-home/adguard-home-integration.ts index 227ffdbdb..6292617bb 100644 --- a/packages/integrations/src/adguard-home/adguard-home-integration.ts +++ b/packages/integrations/src/adguard-home/adguard-home-integration.ts @@ -1,5 +1,5 @@ -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ParseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; diff --git a/packages/integrations/src/base/integration.ts b/packages/integrations/src/base/integration.ts index 9b07a7f6d..ec6c7e4b5 100644 --- a/packages/integrations/src/base/integration.ts +++ b/packages/integrations/src/base/integration.ts @@ -3,8 +3,8 @@ import type { AxiosInstance } from "axios"; import type { Dispatcher } from "undici"; import { fetch as undiciFetch } from "undici"; -import { createAxiosCertificateInstanceAsync, createCertificateAgentAsync } from "@homarr/certificates/server"; import { removeTrailingSlash } from "@homarr/common"; +import { createAxiosCertificateInstanceAsync, createCertificateAgentAsync } from "@homarr/core/infrastructure/http"; import type { IntegrationSecretKind } from "@homarr/definitions"; import { HandleIntegrationErrors } from "./errors/decorator"; diff --git a/packages/integrations/src/base/test-connection/test-connection-service.ts b/packages/integrations/src/base/test-connection/test-connection-service.ts index 0a1a7e88b..4874d706d 100644 --- a/packages/integrations/src/base/test-connection/test-connection-service.ts +++ b/packages/integrations/src/base/test-connection/test-connection-service.ts @@ -1,12 +1,12 @@ import type { X509Certificate } from "node:crypto"; import tls from "node:tls"; -import { createCustomCheckServerIdentity } from "@homarr/certificates/server"; import { getPortFromUrl } from "@homarr/common"; import { getAllTrustedCertificatesAsync, getTrustedCertificateHostnamesAsync, } from "@homarr/core/infrastructure/certificates"; +import { createCustomCheckServerIdentity } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationRequestErrorOfType } from "../errors/http/integration-request-error"; diff --git a/packages/integrations/src/codeberg/codeberg-integration.ts b/packages/integrations/src/codeberg/codeberg-integration.ts index c56aaae37..2396a81e0 100644 --- a/packages/integrations/src/codeberg/codeberg-integration.ts +++ b/packages/integrations/src/codeberg/codeberg-integration.ts @@ -1,6 +1,6 @@ import type { RequestInit, Response } from "undici"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../base/integration"; diff --git a/packages/integrations/src/dashdot/dashdot-integration.ts b/packages/integrations/src/dashdot/dashdot-integration.ts index bf58feb40..2efb47c16 100644 --- a/packages/integrations/src/dashdot/dashdot-integration.ts +++ b/packages/integrations/src/dashdot/dashdot-integration.ts @@ -5,7 +5,7 @@ import "@homarr/redis"; import dayjs from "dayjs"; import { z } from "zod/v4"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createChannelEventHistoryOld } from "../../../redis/src/lib/channel"; import type { IntegrationTestingInput } from "../base/integration"; diff --git a/packages/integrations/src/docker-hub/docker-hub-integration.ts b/packages/integrations/src/docker-hub/docker-hub-integration.ts index 3778e69b9..b9e9ea09c 100644 --- a/packages/integrations/src/docker-hub/docker-hub-integration.ts +++ b/packages/integrations/src/docker-hub/docker-hub-integration.ts @@ -1,7 +1,7 @@ import type { fetch, RequestInit, Response } from "undici"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationInput, IntegrationTestingInput } from "../base/integration"; diff --git a/packages/integrations/src/download-client/aria2/aria2-integration.ts b/packages/integrations/src/download-client/aria2/aria2-integration.ts index 8c8482a94..447e33742 100644 --- a/packages/integrations/src/download-client/aria2/aria2-integration.ts +++ b/packages/integrations/src/download-client/aria2/aria2-integration.ts @@ -1,8 +1,8 @@ import path from "path"; import type { fetch as undiciFetch } from "undici"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; diff --git a/packages/integrations/src/download-client/deluge/deluge-integration.ts b/packages/integrations/src/download-client/deluge/deluge-integration.ts index a3310777a..0ed32127b 100644 --- a/packages/integrations/src/download-client/deluge/deluge-integration.ts +++ b/packages/integrations/src/download-client/deluge/deluge-integration.ts @@ -2,7 +2,7 @@ import { Deluge } from "@ctrl/deluge"; import dayjs from "dayjs"; import type { Dispatcher } from "undici"; -import { createCertificateAgentAsync } from "@homarr/certificates/server"; +import { createCertificateAgentAsync } from "@homarr/core/infrastructure/http"; import { HandleIntegrationErrors } from "../../base/errors/decorator"; import { integrationOFetchHttpErrorHandler } from "../../base/errors/http"; diff --git a/packages/integrations/src/download-client/nzbget/nzbget-integration.ts b/packages/integrations/src/download-client/nzbget/nzbget-integration.ts index 3b799f835..1d5c66429 100644 --- a/packages/integrations/src/download-client/nzbget/nzbget-integration.ts +++ b/packages/integrations/src/download-client/nzbget/nzbget-integration.ts @@ -1,8 +1,8 @@ import dayjs from "dayjs"; import type { fetch as undiciFetch } from "undici"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; diff --git a/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts b/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts index 1634f39a0..1076bca5e 100644 --- a/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts +++ b/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts @@ -2,7 +2,7 @@ import { QBittorrent } from "@ctrl/qbittorrent"; import dayjs from "dayjs"; import type { Dispatcher } from "undici"; -import { createCertificateAgentAsync } from "@homarr/certificates/server"; +import { createCertificateAgentAsync } from "@homarr/core/infrastructure/http"; import { HandleIntegrationErrors } from "../../base/errors/decorator"; import { integrationOFetchHttpErrorHandler } from "../../base/errors/http"; diff --git a/packages/integrations/src/download-client/sabnzbd/sabnzbd-integration.ts b/packages/integrations/src/download-client/sabnzbd/sabnzbd-integration.ts index 42abc58b1..69352ca02 100644 --- a/packages/integrations/src/download-client/sabnzbd/sabnzbd-integration.ts +++ b/packages/integrations/src/download-client/sabnzbd/sabnzbd-integration.ts @@ -2,8 +2,8 @@ import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import type { fetch as undiciFetch } from "undici"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; diff --git a/packages/integrations/src/download-client/transmission/transmission-integration.ts b/packages/integrations/src/download-client/transmission/transmission-integration.ts index 7ce4d561e..7b579b1bc 100644 --- a/packages/integrations/src/download-client/transmission/transmission-integration.ts +++ b/packages/integrations/src/download-client/transmission/transmission-integration.ts @@ -2,7 +2,7 @@ import { Transmission } from "@ctrl/transmission"; import dayjs from "dayjs"; import type { Dispatcher } from "undici"; -import { createCertificateAgentAsync } from "@homarr/certificates/server"; +import { createCertificateAgentAsync } from "@homarr/core/infrastructure/http"; import { HandleIntegrationErrors } from "../../base/errors/decorator"; import { integrationOFetchHttpErrorHandler } from "../../base/errors/http"; diff --git a/packages/integrations/src/emby/emby-integration.ts b/packages/integrations/src/emby/emby-integration.ts index ccfc43222..132a0ac8b 100644 --- a/packages/integrations/src/emby/emby-integration.ts +++ b/packages/integrations/src/emby/emby-integration.ts @@ -1,8 +1,8 @@ import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models"; import { z } from "zod/v4"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; diff --git a/packages/integrations/src/github-container-registry/github-container-registry-integration.ts b/packages/integrations/src/github-container-registry/github-container-registry-integration.ts index 777b67287..1400551a8 100644 --- a/packages/integrations/src/github-container-registry/github-container-registry-integration.ts +++ b/packages/integrations/src/github-container-registry/github-container-registry-integration.ts @@ -2,7 +2,7 @@ import { createAppAuth } from "@octokit/auth-app"; import { Octokit, RequestError } from "octokit"; import type { fetch } from "undici"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { HandleIntegrationErrors } from "../base/errors/decorator"; diff --git a/packages/integrations/src/github/github-integration.ts b/packages/integrations/src/github/github-integration.ts index 3c3825d2b..01548ecc6 100644 --- a/packages/integrations/src/github/github-integration.ts +++ b/packages/integrations/src/github/github-integration.ts @@ -2,7 +2,7 @@ import { createAppAuth } from "@octokit/auth-app"; import { Octokit, RequestError as OctokitRequestError } from "octokit"; import type { fetch } from "undici"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { HandleIntegrationErrors } from "../base/errors/decorator"; diff --git a/packages/integrations/src/gitlab/gitlab-integration.ts b/packages/integrations/src/gitlab/gitlab-integration.ts index 2c9ecf08d..fd1ebc926 100644 --- a/packages/integrations/src/gitlab/gitlab-integration.ts +++ b/packages/integrations/src/gitlab/gitlab-integration.ts @@ -3,7 +3,7 @@ import { createRequesterFn, defaultOptionsHandler } from "@gitbeaker/requester-u import type { FormattedResponse, RequestOptions, ResourceOptions } from "@gitbeaker/requester-utils"; import { Gitlab } from "@gitbeaker/rest"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../base/integration"; diff --git a/packages/integrations/src/homeassistant/homeassistant-integration.ts b/packages/integrations/src/homeassistant/homeassistant-integration.ts index 096dfc49a..6da82df46 100644 --- a/packages/integrations/src/homeassistant/homeassistant-integration.ts +++ b/packages/integrations/src/homeassistant/homeassistant-integration.ts @@ -1,7 +1,7 @@ import z from "zod"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; diff --git a/packages/integrations/src/ical/ical-integration.ts b/packages/integrations/src/ical/ical-integration.ts index 8280d4303..0e725bccb 100644 --- a/packages/integrations/src/ical/ical-integration.ts +++ b/packages/integrations/src/ical/ical-integration.ts @@ -1,6 +1,6 @@ import ICAL from "ical.js"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; diff --git a/packages/integrations/src/jellyfin/jellyfin-integration.ts b/packages/integrations/src/jellyfin/jellyfin-integration.ts index 34fee79b6..b2179987c 100644 --- a/packages/integrations/src/jellyfin/jellyfin-integration.ts +++ b/packages/integrations/src/jellyfin/jellyfin-integration.ts @@ -6,7 +6,7 @@ import { getUserApi } from "@jellyfin/sdk/lib/utils/api/user-api"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api/user-library-api"; import type { AxiosInstance } from "axios"; -import { createAxiosCertificateInstanceAsync } from "@homarr/certificates/server"; +import { createAxiosCertificateInstanceAsync } from "@homarr/core/infrastructure/http"; import { HandleIntegrationErrors } from "../base/errors/decorator"; import { integrationAxiosHttpErrorHandler } from "../base/errors/http"; diff --git a/packages/integrations/src/linuxserverio/linuxserverio-integration.ts b/packages/integrations/src/linuxserverio/linuxserverio-integration.ts index ebe3aa4a5..3178f2c4b 100644 --- a/packages/integrations/src/linuxserverio/linuxserverio-integration.ts +++ b/packages/integrations/src/linuxserverio/linuxserverio-integration.ts @@ -1,4 +1,4 @@ -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../base/integration"; diff --git a/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts b/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts index d8ca04b15..0bac3ec30 100644 --- a/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts +++ b/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts @@ -1,6 +1,6 @@ import { z } from "zod/v4"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { Integration } from "../../base/integration"; diff --git a/packages/integrations/src/media-organizer/radarr/radarr-integration.ts b/packages/integrations/src/media-organizer/radarr/radarr-integration.ts index ee42dbebb..b557f7b77 100644 --- a/packages/integrations/src/media-organizer/radarr/radarr-integration.ts +++ b/packages/integrations/src/media-organizer/radarr/radarr-integration.ts @@ -1,6 +1,6 @@ import { z } from "zod/v4"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../../base/integration"; diff --git a/packages/integrations/src/media-organizer/readarr/readarr-integration.ts b/packages/integrations/src/media-organizer/readarr/readarr-integration.ts index 1390fb560..aa036626c 100644 --- a/packages/integrations/src/media-organizer/readarr/readarr-integration.ts +++ b/packages/integrations/src/media-organizer/readarr/readarr-integration.ts @@ -1,6 +1,6 @@ import { z } from "zod/v4"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { Integration } from "../../base/integration"; diff --git a/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts b/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts index d35b54365..17826e2de 100644 --- a/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts +++ b/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts @@ -1,6 +1,6 @@ import { z } from "zod/v4"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { Integration } from "../../base/integration"; diff --git a/packages/integrations/src/media-transcoding/tdarr-integration.ts b/packages/integrations/src/media-transcoding/tdarr-integration.ts index fc3208f5f..5a16d9ff3 100644 --- a/packages/integrations/src/media-transcoding/tdarr-integration.ts +++ b/packages/integrations/src/media-transcoding/tdarr-integration.ts @@ -1,4 +1,4 @@ -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; diff --git a/packages/integrations/src/nextcloud/nextcloud.integration.ts b/packages/integrations/src/nextcloud/nextcloud.integration.ts index 640c86273..aa57f480f 100644 --- a/packages/integrations/src/nextcloud/nextcloud.integration.ts +++ b/packages/integrations/src/nextcloud/nextcloud.integration.ts @@ -6,7 +6,7 @@ import type { RequestInit as NodeFetchRequestInit } from "node-fetch"; import * as ical from "node-ical"; import { DAVClient } from "tsdav"; -import { createHttpsAgentAsync } from "@homarr/certificates/server"; +import { createHttpsAgentAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { HandleIntegrationErrors } from "../base/errors/decorator"; diff --git a/packages/integrations/src/npm/npm-integration.ts b/packages/integrations/src/npm/npm-integration.ts index 13a307467..b0c8c050b 100644 --- a/packages/integrations/src/npm/npm-integration.ts +++ b/packages/integrations/src/npm/npm-integration.ts @@ -1,4 +1,4 @@ -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; diff --git a/packages/integrations/src/ntfy/ntfy-integration.ts b/packages/integrations/src/ntfy/ntfy-integration.ts index 5c6ddb67b..0fa437fec 100644 --- a/packages/integrations/src/ntfy/ntfy-integration.ts +++ b/packages/integrations/src/ntfy/ntfy-integration.ts @@ -1,5 +1,5 @@ -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { Integration } from "../base/integration"; import type { IntegrationTestingInput } from "../base/integration"; diff --git a/packages/integrations/src/openmediavault/openmediavault-integration.ts b/packages/integrations/src/openmediavault/openmediavault-integration.ts index ee45a996d..128e68a43 100644 --- a/packages/integrations/src/openmediavault/openmediavault-integration.ts +++ b/packages/integrations/src/openmediavault/openmediavault-integration.ts @@ -1,7 +1,7 @@ import type { Headers, HeadersInit, fetch as undiciFetch, Response as UndiciResponse } from "undici"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationInput, IntegrationTestingInput } from "../base/integration"; diff --git a/packages/integrations/src/opnsense/opnsense-integration.ts b/packages/integrations/src/opnsense/opnsense-integration.ts index 0c1841d55..8ab0e968d 100644 --- a/packages/integrations/src/opnsense/opnsense-integration.ts +++ b/packages/integrations/src/opnsense/opnsense-integration.ts @@ -1,5 +1,5 @@ -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ParseError, ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createChannelEventHistoryOld } from "../../../redis/src/lib/channel"; import type { IntegrationTestingInput } from "../base/integration"; diff --git a/packages/integrations/src/overseerr/overseerr-integration.ts b/packages/integrations/src/overseerr/overseerr-integration.ts index 88846ff49..042f2faab 100644 --- a/packages/integrations/src/overseerr/overseerr-integration.ts +++ b/packages/integrations/src/overseerr/overseerr-integration.ts @@ -1,6 +1,6 @@ import { z } from "zod/v4"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; diff --git a/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts b/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts index 0d542b540..346b40642 100644 --- a/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts +++ b/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts @@ -1,5 +1,5 @@ -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { removeTrailingSlash } from "@homarr/common"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import type { IntegrationInput } from "../base/integration"; import { PiHoleIntegrationV5 } from "./v5/pi-hole-integration-v5"; diff --git a/packages/integrations/src/pi-hole/v5/pi-hole-integration-v5.ts b/packages/integrations/src/pi-hole/v5/pi-hole-integration-v5.ts index 3ffc2e248..f0257615b 100644 --- a/packages/integrations/src/pi-hole/v5/pi-hole-integration-v5.ts +++ b/packages/integrations/src/pi-hole/v5/pi-hole-integration-v5.ts @@ -1,5 +1,5 @@ -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import type { IntegrationTestingInput } from "../../base/integration"; import { Integration } from "../../base/integration"; diff --git a/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts b/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts index b4a472e16..fd54c0915 100644 --- a/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts +++ b/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts @@ -1,8 +1,8 @@ import type { fetch as undiciFetch, Response as UndiciResponse } from "undici"; import type { z } from "zod/v4"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationInput, IntegrationTestingInput } from "../../base/integration"; diff --git a/packages/integrations/src/plex/plex-integration.ts b/packages/integrations/src/plex/plex-integration.ts index f8ca3367b..9c2cb8a4f 100644 --- a/packages/integrations/src/plex/plex-integration.ts +++ b/packages/integrations/src/plex/plex-integration.ts @@ -1,8 +1,8 @@ import { parseStringPromise } from "xml2js"; import { z } from "zod/v4"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ParseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ImageProxy } from "@homarr/image-proxy"; diff --git a/packages/integrations/src/prowlarr/prowlarr-integration.ts b/packages/integrations/src/prowlarr/prowlarr-integration.ts index 22b3bbab2..db6e35f06 100644 --- a/packages/integrations/src/prowlarr/prowlarr-integration.ts +++ b/packages/integrations/src/prowlarr/prowlarr-integration.ts @@ -1,6 +1,6 @@ import { z } from "zod/v4"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; diff --git a/packages/integrations/src/proxmox/proxmox-integration.ts b/packages/integrations/src/proxmox/proxmox-integration.ts index 164ca377e..433162186 100644 --- a/packages/integrations/src/proxmox/proxmox-integration.ts +++ b/packages/integrations/src/proxmox/proxmox-integration.ts @@ -1,7 +1,7 @@ import type { Proxmox } from "proxmox-api"; import proxmoxApi from "proxmox-api"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { HandleIntegrationErrors } from "../base/errors/decorator"; diff --git a/packages/integrations/src/quay/quay-integration.ts b/packages/integrations/src/quay/quay-integration.ts index ac150e53a..21cbf797e 100644 --- a/packages/integrations/src/quay/quay-integration.ts +++ b/packages/integrations/src/quay/quay-integration.ts @@ -1,6 +1,6 @@ import type { RequestInit, Response } from "undici"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { IntegrationTestingInput } from "../base/integration"; diff --git a/packages/integrations/src/unifi-controller/unifi-controller-integration.ts b/packages/integrations/src/unifi-controller/unifi-controller-integration.ts index b4a688e63..bee66c956 100644 --- a/packages/integrations/src/unifi-controller/unifi-controller-integration.ts +++ b/packages/integrations/src/unifi-controller/unifi-controller-integration.ts @@ -2,12 +2,12 @@ import type tls from "node:tls"; import axios from "axios"; import { HttpCookieAgent, HttpsCookieAgent } from "http-cookie-agent/http"; -import { createCustomCheckServerIdentity } from "@homarr/certificates/server"; import { getPortFromUrl } from "@homarr/common"; import { getAllTrustedCertificatesAsync, getTrustedCertificateHostnamesAsync, } from "@homarr/core/infrastructure/certificates"; +import { createCustomCheckServerIdentity } from "@homarr/core/infrastructure/http"; import type { SiteStats } from "@homarr/node-unifi"; import Unifi from "@homarr/node-unifi"; diff --git a/packages/ping/package.json b/packages/ping/package.json index 873db96ae..c73010fdc 100644 --- a/packages/ping/package.json +++ b/packages/ping/package.json @@ -22,7 +22,6 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { - "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0" }, diff --git a/packages/ping/src/index.ts b/packages/ping/src/index.ts index 9e581beee..124acb825 100644 --- a/packages/ping/src/index.ts +++ b/packages/ping/src/index.ts @@ -1,7 +1,8 @@ import { fetch } from "undici"; import { extractErrorMessage } from "@homarr/common"; -import { LoggingAgent } from "@homarr/common/server"; +import { UndiciHttpAgent } from "@homarr/core/infrastructure/http"; +import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; @@ -9,28 +10,21 @@ const logger = createLogger({ module: "ping" }); export const sendPingRequestAsync = async (url: string) => { try { - const controller = new AbortController(); - - // 10 seconds timeout: - const timeoutId = setTimeout(() => controller.abort(), 10000); const start = performance.now(); - - return await fetch(url, { - dispatcher: new LoggingAgent({ - connect: { - rejectUnauthorized: false, // Ping should always work, even with untrusted certificates - }, - }), - signal: controller.signal, - }) - .finally(() => { - clearTimeout(timeoutId); - }) - .then((response) => { - const end = performance.now(); - const durationMs = end - start; - return { statusCode: response.status, durationMs }; + return await withTimeoutAsync(async (signal) => { + return await fetch(url, { + dispatcher: new UndiciHttpAgent({ + connect: { + rejectUnauthorized: false, // Ping should always work, even with untrusted certificates + }, + }), + signal, }); + }).then((response) => { + const end = performance.now(); + const durationMs = end - start; + return { statusCode: response.status, durationMs }; + }); } catch (error) { logger.error(new ErrorWithMetadata("Failed to send ping request", { url }, { cause: error })); return { diff --git a/packages/request-handler/src/minecraft-server-status.ts b/packages/request-handler/src/minecraft-server-status.ts index 4cff54ed8..f6c813193 100644 --- a/packages/request-handler/src/minecraft-server-status.ts +++ b/packages/request-handler/src/minecraft-server-status.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import { z } from "zod/v4"; -import { fetchWithTimeout } from "@homarr/common"; +import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; @@ -11,7 +11,7 @@ export const minecraftServerStatusRequestHandler = createCachedWidgetRequestHand async requestAsync(input: { domain: string; isBedrockServer: boolean }) { const path = `${input.isBedrockServer ? "/bedrock" : ""}/3/${input.domain}`; - const response = await fetchWithTimeout(`https://api.mcsrvstat.us${path}`); + const response = await fetchWithTimeoutAsync(`https://api.mcsrvstat.us${path}`); return responseSchema.parse(await response.json()); }, cacheDuration: dayjs.duration(5, "minutes"), diff --git a/packages/request-handler/src/stock-price.ts b/packages/request-handler/src/stock-price.ts index 0df74abfb..7b724c43a 100644 --- a/packages/request-handler/src/stock-price.ts +++ b/packages/request-handler/src/stock-price.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import { z } from "zod/v4"; -import { fetchWithTimeout } from "@homarr/common"; +import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; @@ -9,7 +9,7 @@ export const fetchStockPriceHandler = createCachedWidgetRequestHandler({ queryKey: "fetchStockPriceResult", widgetKind: "stockPrice", async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) { - const response = await fetchWithTimeout( + const response = await fetchWithTimeoutAsync( `https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`, ); const data = dataSchema.parse(await response.json()); diff --git a/packages/request-handler/src/update-checker.ts b/packages/request-handler/src/update-checker.ts index b47d0894e..b2dca3b1e 100644 --- a/packages/request-handler/src/update-checker.ts +++ b/packages/request-handler/src/update-checker.ts @@ -2,8 +2,8 @@ import dayjs from "dayjs"; import { Octokit } from "octokit"; import { compareSemVer, isValidSemVer } from "semver-parser"; -import { fetchWithTimeout } from "@homarr/common"; import { env } from "@homarr/common/env"; +import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { createLogger } from "@homarr/core/infrastructure/logs"; import { createChannelWithLatestAndEvents } from "@homarr/redis"; import { createCachedRequestHandler } from "@homarr/request-handler/lib/cached-request-handler"; @@ -23,7 +23,7 @@ export const updateCheckerRequestHandler = createCachedRequestHandler({ const octokit = new Octokit({ request: { - fetch: fetchWithTimeout, + fetch: fetchWithTimeoutAsync, }, }); const releases = await octokit.rest.repos.listReleases({ diff --git a/packages/request-handler/src/weather.ts b/packages/request-handler/src/weather.ts index faa97846c..fdf1af8f2 100644 --- a/packages/request-handler/src/weather.ts +++ b/packages/request-handler/src/weather.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import { z } from "zod"; -import { fetchWithTimeout } from "@homarr/common"; +import { fetchWithTimeoutAsync } from "@homarr/core/infrastructure/http/timeout"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; @@ -9,7 +9,7 @@ export const weatherRequestHandler = createCachedWidgetRequestHandler({ queryKey: "weatherAtLocation", widgetKind: "weather", async requestAsync(input: { latitude: number; longitude: number }) { - const res = await fetchWithTimeout( + const res = await fetchWithTimeoutAsync( `https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max¤t_weather=true&timezone=auto`, ); const json: unknown = await res.json(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edf0096d0..502582e78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,9 +133,6 @@ importers: '@homarr/boards': specifier: workspace:^0.1.0 version: link:../../packages/boards - '@homarr/certificates': - specifier: workspace:^0.1.0 - version: link:../../packages/certificates '@homarr/common': specifier: workspace:^0.1.0 version: link:../../packages/common @@ -575,9 +572,6 @@ importers: '@homarr/auth': specifier: workspace:^0.1.0 version: link:../auth - '@homarr/certificates': - specifier: workspace:^0.1.0 - version: link:../certificates '@homarr/common': specifier: workspace:^0.1.0 version: link:../common @@ -699,9 +693,6 @@ importers: '@auth/drizzle-adapter': specifier: ^1.11.1 version: 1.11.1 - '@homarr/certificates': - specifier: workspace:^0.1.0 - version: link:../certificates '@homarr/common': specifier: workspace:^0.1.0 version: link:../common @@ -795,37 +786,6 @@ importers: specifier: ^5.9.3 version: 5.9.3 - packages/certificates: - dependencies: - '@homarr/common': - specifier: workspace:^0.1.0 - version: link:../common - '@homarr/core': - specifier: workspace:^0.1.0 - version: link:../core - '@homarr/db': - specifier: workspace:^0.1.0 - version: link:../db - undici: - specifier: 7.16.0 - version: 7.16.0 - devDependencies: - '@homarr/eslint-config': - specifier: workspace:^0.2.0 - version: link:../../tooling/eslint - '@homarr/prettier-config': - specifier: workspace:^0.1.0 - version: link:../../tooling/prettier - '@homarr/tsconfig': - specifier: workspace:^0.1.0 - version: link:../../tooling/typescript - eslint: - specifier: ^9.39.2 - version: 9.39.2 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - packages/cli: dependencies: '@drizzle-team/brocli': @@ -1420,9 +1380,6 @@ importers: packages/image-proxy: dependencies: - '@homarr/certificates': - specifier: workspace:^0.1.0 - version: link:../certificates '@homarr/common': specifier: workspace:^0.1.0 version: link:../common @@ -1469,9 +1426,6 @@ importers: '@gitbeaker/rest': specifier: ^43.8.0 version: 43.8.0 - '@homarr/certificates': - specifier: workspace:^0.1.0 - version: link:../certificates '@homarr/common': specifier: workspace:^0.1.0 version: link:../common @@ -1799,9 +1753,6 @@ importers: packages/ping: dependencies: - '@homarr/certificates': - specifier: workspace:^0.1.0 - version: link:../certificates '@homarr/common': specifier: workspace:^0.1.0 version: link:../common