refactor(http): move to core package (#4690)

This commit is contained in:
Meier Lukas
2025-12-19 16:37:21 +01:00
committed by GitHub
parent a0a11e3570
commit 6f0dbae121
75 changed files with 280 additions and 286 deletions

View File

@@ -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",

View File

@@ -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());

View File

@@ -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",

View File

@@ -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<typeof locationSearchCityOutput>;
}),
});

View File

@@ -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",

View File

@@ -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";

View File

@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];

View File

@@ -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"
}
}

View File

@@ -1,8 +0,0 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}

View File

@@ -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 });

View File

@@ -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 });
}

View File

@@ -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<typeof Agent>) {
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);
}
}

View File

@@ -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<typeof fetch>) => {
const controller = new AbortController();
// 10 seconds timeout:
const timeoutId = setTimeout(() => controller.abort(), 10000);
return fetch(url, { signal: controller.signal, ...requestInit }).finally(() => {
clearTimeout(timeoutId);
});
};

View File

@@ -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";

View File

@@ -1,5 +1,4 @@
export * from "./security";
export * from "./encryption";
export * from "./user-agent";
export * from "./fetch-agent";
export * from "./errors";

View File

@@ -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": {
"*": {

View File

@@ -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<typeof schema.trustedCertificateHostnames>;

View File

@@ -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;
}
}

View File

@@ -0,0 +1,8 @@
export { UndiciHttpAgent } from "./http-agent";
export {
createAxiosCertificateInstanceAsync,
createCertificateAgentAsync,
createCustomCheckServerIdentity,
createHttpsAgentAsync,
fetchWithTrustedCertificatesAsync,
} from "./request";

View File

@@ -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<typeof trustedCertificateHostnames>[],
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<Response> => {
export const fetchWithTrustedCertificatesAsync = async (
url: RequestInfo,
options?: RequestInit & { timeout?: number },
): Promise<Response> => {
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,

View File

@@ -0,0 +1,19 @@
import type { Response as UndiciResponse } from "undici";
// https://stackoverflow.com/questions/46946380/fetch-api-request-timeout
export const withTimeoutAsync = async <TResponse extends Response | UndiciResponse>(
callback: (signal: AbortSignal) => Promise<TResponse>,
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<typeof fetch>) => {
return await withTimeoutAsync((signal) => fetch(url, { ...requestInit, signal }));
};

View File

@@ -15,4 +15,12 @@ interface DefaultMetadata {
}
export const createLogger = (metadata: DefaultMetadata & Record<string, unknown>) => logger.child(metadata);
export type Logger = winston.Logger;
type LogMethod = ((message: string, metadata?: Record<string, unknown>) => void) | ((error: unknown) => void);
export interface ILogger {
debug: LogMethod;
info: LogMethod;
warn: LogMethod;
error: LogMethod;
}

View File

@@ -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} `);
});
});

View File

@@ -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<string, unknown>;
}
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<string, unknown>): 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<string, unknown>): void {
this.log("debug", param1, param2);
}
info(param1: unknown, param2?: Record<string, unknown>): void {
this.log("info", param1, param2);
}
warn(param1: unknown, param2?: Record<string, unknown>): void {
this.log("warn", param1, param2);
}
error(param1: unknown, param2?: Record<string, unknown>): void {
this.log("error", param1, param2);
}
}

View File

@@ -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 {

View File

@@ -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<RepositoryIconGroup> {
const response = await fetchWithTimeout(this.repositoryIndexingUrl);
const response = await fetchWithTimeoutAsync(this.repositoryIndexingUrl);
const listOfFiles = (await response.json()) as JsdelivrApiResponse;
return {

View File

@@ -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",

View File

@@ -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";

View File

@@ -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",

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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"
},

View File

@@ -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 {

View File

@@ -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"),

View File

@@ -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());

View File

@@ -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({

View File

@@ -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&current_weather=true&timezone=auto`,
);
const json: unknown = await res.json();

49
pnpm-lock.yaml generated
View File

@@ -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