fix(integrations): keep unifi-os-types client-safe

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
zacharyandClaude Opus 5 committed 2026-10-08 00:23:44 +02:00
1 parent 277fbb841e
commit 3125bcf400
5 files changed
+42 -20

No files matched your search

@@ -1,4 +1,7 @@
// @vitest-environment node
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, test, vi } from "vitest";
vi.hoisted(() => {
@@ -147,6 +150,22 @@ describe("matchCloudSite", () => {
});
});
describe("client-safety", () => {
// `unifi-os-types.ts` is re-exported by `packages/integrations/src/types.ts`,
// which client components import (e.g. via the spotlight search group). An
// import of `@homarr/common/server` here drags `ioredis` — and transitively
// Node's `dns` module — into the browser bundle and breaks `pnpm dev`/build.
// Only a source-text check catches this: a type-only "does it compile" check
// would not, since the breakage is a bundler resolution failure, not a type
// error. This regression guard is what would have caught the original bug.
test("does not import @homarr/common/server", () => {
const path = fileURLToPath(new URL("../unifi-os-types.ts", import.meta.url));
const source = readFileSync(path, "utf-8");
expect(source).not.toContain("@homarr/common/server");
});
});
describe("unifiOsClientSchema", () => {
test("keeps fields the published OpenAPI does not document", () => {
const parsed = unifiOsClientSchema.parse({
@@ -2,11 +2,11 @@ import { ResponseError } from "@homarr/common/server";
import type { RequestInit, Response } from "undici";
import type { z } from "zod/v4";
import { parseUnifiOsAsync } from "./unifi-os-parse";
import type { UnifiOsClientCounts, UnifiOsDevice, UnifiOsDeviceStatistics, UnifiOsSite } from "./unifi-os-types";
import {
countClients,
createUnifiOsPageSchema,
parseUnifiOsAsync,
unifiOsClientSchema,
unifiOsDeviceSchema,
unifiOsDeviceStatisticsSchema,
@@ -0,0 +1,20 @@
import { ParseError } from "@homarr/common/server";
import type { z } from "zod/v4";
/**
* Server-only: pulled out of `unifi-os-types.ts` because `ParseError` comes
* from `@homarr/common/server`, which drags in `ioredis` and Node's `dns`.
* `unifi-os-types.ts` is re-exported by `packages/integrations/src/types.ts`,
* which client components import, so it must stay free of server-only code.
*/
export const parseUnifiOsAsync = async <TOutput>(
value: unknown,
schema: z.ZodType<TOutput>,
message: string,
): Promise<TOutput> => {
const result = await schema.safeParseAsync(value);
if (!result.success) {
throw new ParseError(message, { cause: result.error });
}
return result.data;
};
@@ -1,4 +1,3 @@
import { ParseError } from "@homarr/common/server";
import { z } from "zod/v4";
/**
@@ -153,18 +152,6 @@ export interface UnifiOsSummary {
gateway: UnifiOsGateway | null;
}
export const parseUnifiOsAsync = async <TOutput>(
value: unknown,
schema: z.ZodType<TOutput>,
message: string,
): Promise<TOutput> => {
const result = await schema.safeParseAsync(value);
if (!result.success) {
throw new ParseError(message, { cause: result.error });
}
return result.data;
};
/**
* L'API locale renvoie `aa:bb:cc:11:22:33`, le cloud peut renvoyer une autre
* casse ou d'autres séparateurs. La comparaison se fait sur une forme canonique.
@@ -1,13 +1,9 @@
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { UnifiOsFetch } from "./unifi-os-api";
import { parseUnifiOsAsync } from "./unifi-os-parse";
import type { UnifiOsIspMetrics } from "./unifi-os-types";
import {
matchCloudSite,
parseUnifiOsAsync,
unifiSmIspMetricsResponseSchema,
unifiSmSiteSchema,
} from "./unifi-os-types";
import { matchCloudSite, unifiSmIspMetricsResponseSchema, unifiSmSiteSchema } from "./unifi-os-types";
import { z } from "zod/v4";
const logger = createLogger({ module: "unifiSmApi" });