diff --git a/.vscode/settings.json b/.vscode/settings.json index 51d2500a1..f018a27b5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,6 +26,7 @@ "Sabnzbd", "SeDemal", "Sonarr", + "sslverify", "superjson", "tabler", "trpc", diff --git a/apps/tasks/package.json b/apps/tasks/package.json index cbf7791f1..ce39ff56d 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -10,7 +10,7 @@ "main": "./src/main.ts", "types": "./src/main.ts", "scripts": { - "build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:@opentelemetry/api --outfile=tasks.cjs", + "build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:@opentelemetry/api --external:http-cookie-agent --outfile=tasks.cjs", "clean": "rm -rf .turbo node_modules", "dev": "pnpm with-env tsx ./src/main.ts", "format": "prettier --check . --ignore-path ../../.gitignore", diff --git a/apps/websocket/package.json b/apps/websocket/package.json index 5d8c700fa..e7b8fafea 100644 --- a/apps/websocket/package.json +++ b/apps/websocket/package.json @@ -7,7 +7,7 @@ "main": "./src/main.ts", "types": "./src/main.ts", "scripts": { - "build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text", + "build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:http-cookie-agent --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text", "clean": "rm -rf .turbo node_modules", "dev": "pnpm with-env tsx ./src/main.ts", "format": "prettier --check . --ignore-path ../../.gitignore", diff --git a/packages/integrations/package.json b/packages/integrations/package.json index b3aa41a3c..c3f02434e 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -38,6 +38,7 @@ "@jellyfin/sdk": "^0.11.0", "maria2": "^0.4.0", "node-ical": "^0.20.1", + "node-unifi": "^2.5.1", "proxmox-api": "1.1.1", "tsdav": "^2.1.4", "undici": "7.8.0", @@ -48,6 +49,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", + "@types/node-unifi": "^2.5.1", "@types/xml2js": "^0.4.14", "eslint": "^9.25.1", "typescript": "^5.8.3" diff --git a/packages/integrations/src/unifi-controller/unifi-controller-integration.ts b/packages/integrations/src/unifi-controller/unifi-controller-integration.ts index 06e45adae..4d5233762 100644 --- a/packages/integrations/src/unifi-controller/unifi-controller-integration.ts +++ b/packages/integrations/src/unifi-controller/unifi-controller-integration.ts @@ -1,103 +1,78 @@ -import type z from "zod"; +import type { SiteStats } from "node-unifi"; +import { Controller } from "node-unifi"; -import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { logger } from "@homarr/log"; - -import { ParseError } from "../base/error"; -import { Integration, throwErrorByStatusCode } from "../base/integration"; -import { IntegrationTestConnectionError } from "../base/test-connection-error"; +import { Integration } from "../base/integration"; import type { NetworkControllerSummaryIntegration } from "../interfaces/network-controller-summary/network-controller-summary-integration"; import type { NetworkControllerSummary } from "../interfaces/network-controller-summary/network-controller-summary-types"; -import { unifiSummaryResponseSchema } from "./unifi-controller-types"; - -const udmpPrefix = "proxy/network"; -type Subsystem = "www" | "wan" | "wlan" | "lan" | "vpn"; +import type { HealthSubsystem } from "./unifi-controller-types"; export class UnifiControllerIntegration extends Integration implements NetworkControllerSummaryIntegration { - private prefix: string | undefined; - public async getNetworkSummaryAsync(): Promise { - if (!this.headers) { - await this.authenticateAndConstructSessionInHeaderAsync(); - } - - const requestUrl = this.url(`/${this.prefix}/api/stat/sites`); - - const requestHeaders: Record = { - "Content-Type": "application/json", - ...this.headers, - }; - if (this.csrfToken) { - requestHeaders["X-CSRF-TOKEN"] = this.csrfToken; - } - - const statsResponse = await fetchWithTrustedCertificatesAsync(requestUrl, { - method: "GET", - headers: { - ...requestHeaders, - }, - }).catch((err: TypeError) => { - const detailMessage = String(err.cause); - throw new IntegrationTestConnectionError("invalidUrl", detailMessage); - }); - - if (!statsResponse.ok) { - throwErrorByStatusCode(statsResponse.status); - } - - const result = unifiSummaryResponseSchema.safeParse(await statsResponse.json()); - - if (!result.success) { - throw new ParseError("Unifi controller", result.error); - } + const client = await this.createControllerClientAsync(); + const stats = await client.getSitesStats(); return { - wanStatus: this.getStatusValueOverAllSites(result.data, "wan", (site) => site.status === "ok"), + wanStatus: this.getStatusValueOverAllSites(stats, "wan", (site) => site.status === "ok"), www: { - status: this.getStatusValueOverAllSites(result.data, "wan", (site) => site.status === "ok"), - latency: this.getNumericValueOverAllSites(result.data, "www", (site) => site.latency, "max"), - ping: this.getNumericValueOverAllSites(result.data, "www", (site) => site.speedtest_ping, "max"), - uptime: this.getNumericValueOverAllSites(result.data, "www", (site) => site.uptime, "max"), + status: this.getStatusValueOverAllSites(stats, "wan", (site) => site.status === "ok"), + latency: this.getNumericValueOverAllSites(stats, "www", (site) => site.latency, "max"), + ping: this.getNumericValueOverAllSites(stats, "www", (site) => site.speedtest_ping, "max"), + uptime: this.getNumericValueOverAllSites(stats, "www", (site) => site.uptime, "max"), }, wifi: { - status: this.getStatusValueOverAllSites(result.data, "wlan", (site) => site.status === "ok"), - users: this.getNumericValueOverAllSites(result.data, "wlan", (site) => site.num_user, "sum"), - guests: this.getNumericValueOverAllSites(result.data, "wlan", (site) => site.num_guest, "sum"), + status: this.getStatusValueOverAllSites(stats, "wlan", (site) => site.status === "ok"), + users: this.getNumericValueOverAllSites(stats, "wlan", (site) => site.num_user, "sum"), + guests: this.getNumericValueOverAllSites(stats, "wlan", (site) => site.num_guest, "sum"), }, lan: { - status: this.getStatusValueOverAllSites(result.data, "lan", (site) => site.status === "ok"), - users: this.getNumericValueOverAllSites(result.data, "lan", (site) => site.num_user, "sum"), - guests: this.getNumericValueOverAllSites(result.data, "lan", (site) => site.num_guest, "sum"), + status: this.getStatusValueOverAllSites(stats, "lan", (site) => site.status === "ok"), + users: this.getNumericValueOverAllSites(stats, "lan", (site) => site.num_user, "sum"), + guests: this.getNumericValueOverAllSites(stats, "lan", (site) => site.num_guest, "sum"), }, vpn: { - status: this.getStatusValueOverAllSites(result.data, "vpn", (site) => site.status === "ok"), - users: this.getNumericValueOverAllSites(result.data, "vpn", (site) => site.remote_user_num_active, "sum"), + status: this.getStatusValueOverAllSites(stats, "vpn", (site) => site.status === "ok"), + users: this.getNumericValueOverAllSites(stats, "vpn", (site) => site.remote_user_num_active, "sum"), }, } satisfies NetworkControllerSummary; } public async testConnectionAsync(): Promise { - await this.authenticateAndConstructSessionInHeaderAsync(); + const client = await this.createControllerClientAsync(); + await client.getSitesStats(); } - private getStatusValueOverAllSites( - data: z.infer, - subsystem: Subsystem, - selectCallback: (obj: z.infer["data"][number]["health"][number]) => boolean, + private async createControllerClientAsync() { + const portString = new URL(this.integration.url).port; + const port = Number.isInteger(portString) ? Number(portString) : undefined; + const hostname = new URL(this.integration.url).hostname; + + const client = new Controller({ + host: hostname, + // @ts-expect-error the URL construction is incorrect and does not append the required / at the end: https://github.com/jens-maus/node-unifi/blob/05665e8f82a900a15a9ea8b1071750b29825b3bc/unifi.js#L56, https://github.com/jens-maus/node-unifi/blob/05665e8f82a900a15a9ea8b1071750b29825b3bc/unifi.js#L95 + port: port === undefined ? "/" : `${port}/`, + sslverify: false, // TODO: implement a "ignore certificate toggle", see https://github.com/homarr-labs/homarr/issues/2553 + username: this.getSecretValue("username"), + password: this.getSecretValue("password"), + }); + + // Object.defineProperty(client, '_baseurl', { value: url }); + await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null); + return client; + } + + private getStatusValueOverAllSites( + data: SiteStats[], + subsystem: S, + selectCallback: (obj: SiteStats["health"][number]) => boolean, ) { return this.getBooleanValueOverAllSites(data, subsystem, selectCallback) ? "enabled" : "disabled"; } private getNumericValueOverAllSites< - S extends Subsystem, - T extends Extract["data"][number]["health"][number], { subsystem: S }>, - >( - data: z.infer, - subsystem: S, - selectCallback: (obj: T) => number, - strategy: "average" | "sum" | "max", - ): number { - const values = data.data.map((site) => selectCallback(this.getSubsystem(site.health, subsystem) as T)); + S extends HealthSubsystem, + T extends Extract, + >(data: SiteStats[], subsystem: S, selectCallback: (obj: T) => number, strategy: "average" | "sum" | "max"): number { + const values = data.map((site) => selectCallback(this.getSubsystem(site.health, subsystem) as T)); if (strategy === "sum") { return values.reduce((first, second) => first + second, 0); @@ -111,118 +86,18 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo } private getBooleanValueOverAllSites( - data: z.infer, - subsystem: Subsystem, - selectCallback: (obj: z.infer["data"][number]["health"][number]) => boolean, + data: SiteStats[], + subsystem: HealthSubsystem, + selectCallback: (obj: SiteStats["health"][number]) => boolean, ): boolean { - return data.data.every((site) => selectCallback(this.getSubsystem(site.health, subsystem))); + return data.every((site) => selectCallback(this.getSubsystem(site.health, subsystem))); } - private getSubsystem( - health: z.infer["data"][number]["health"], - subsystem: Subsystem, - ) { + private getSubsystem(health: SiteStats["health"], subsystem: HealthSubsystem) { const value = health.find((health) => health.subsystem === subsystem); if (!value) { throw new Error(`Subsystem ${subsystem} not found!`); } return value; } - - private headers: Record | undefined = undefined; - private csrfToken: string | undefined; - - private async authenticateAndConstructSessionInHeaderAsync(): Promise { - await this.determineUDMVariantAsync(); - await this.authenticateAndSetCookieAsync(); - } - - private async authenticateAndSetCookieAsync(): Promise { - if (this.headers) { - return; - } - - const endpoint = this.prefix === udmpPrefix ? "auth/login" : "login"; - logger.debug("Authenticating at network console: " + endpoint); - - const loginUrl = this.url(`/api/${endpoint}`); - - const loginBody = { - username: this.getSecretValue("username"), - password: this.getSecretValue("password"), - remember: true, - }; - - const requestHeaders: Record = { "Content-Type": "application/json" }; - if (this.csrfToken) { - requestHeaders["X-CSRF-TOKEN"] = this.csrfToken; - } - - const loginResponse = await fetchWithTrustedCertificatesAsync(loginUrl, { - method: "POST", - headers: { - ...requestHeaders, - }, - body: JSON.stringify(loginBody), - }).catch((err: TypeError) => { - const detailMessage = String(err.cause); - throw new IntegrationTestConnectionError("invalidUrl", detailMessage); - }); - - if (!loginResponse.ok) { - throwErrorByStatusCode(loginResponse.status); - } - - const responseHeaders = loginResponse.headers; - const newHeaders: Record = {}; - const loginToken = UnifiControllerIntegration.extractLoginTokenFromCookies(responseHeaders); - newHeaders.Cookie = `${loginToken};`; - this.headers = newHeaders; - } - - private async determineUDMVariantAsync(): Promise { - if (this.prefix) { - return; - } - - logger.debug("Prefix for authentication not set; initial connect to determine UDM variant"); - const url = this.url("/"); - - const { status, ok, headers } = await fetchWithTrustedCertificatesAsync(url, { method: "HEAD" }) - .then((res) => res) - .catch((err: TypeError) => { - const detailMessage = String(err.cause); - throw new IntegrationTestConnectionError("invalidUrl", detailMessage); - }); - - if (!ok) { - throw new IntegrationTestConnectionError("invalidUrl", "status code: " + status); - } - - let prefix = ""; - if (headers.get("x-csrf-token") !== null) { - // Unifi OS < 3.2.5 passes & requires csrf-token - prefix = udmpPrefix; - const headersCSRFToken = headers.get("x-csrf-token"); - if (headersCSRFToken) { - this.csrfToken = headersCSRFToken; - } - } else if (headers.get("access-control-expose-headers") !== null) { - // Unifi OS ≥ 3.2.5 doesnt pass csrf token but still uses different endpoint - prefix = udmpPrefix; - } - this.prefix = prefix; - logger.debug("Final prefix: " + this.prefix); - } - - private static extractLoginTokenFromCookies(headers: Headers): string { - const cookies = headers.get("set-cookie") ?? ""; - const loginToken = cookies.split(";").find((cookie) => cookie.includes("TOKEN")); - - if (loginToken) { - return loginToken; - } - - throw new Error("Login token not found in cookies"); - } } diff --git a/packages/integrations/src/unifi-controller/unifi-controller-types.ts b/packages/integrations/src/unifi-controller/unifi-controller-types.ts index cbca72820..bed6923b5 100644 --- a/packages/integrations/src/unifi-controller/unifi-controller-types.ts +++ b/packages/integrations/src/unifi-controller/unifi-controller-types.ts @@ -1,130 +1,3 @@ -import { z } from "zod"; +import type { SiteStats } from "node-unifi"; -export const healthSchema = z.discriminatedUnion("subsystem", [ - z.object({ - subsystem: z.literal("wlan"), - num_user: z.number(), - num_guest: z.number(), - num_iot: z.number(), - "tx_bytes-r": z.number(), - "rx_bytes-r": z.number(), - status: z.string(), - num_ap: z.number(), - num_adopted: z.number(), - num_disabled: z.number(), - num_disconnected: z.number(), - num_pending: z.number(), - }), - z.object({ - subsystem: z.literal("wan"), - num_gw: z.number(), - num_adopted: z.number(), - num_disconnected: z.number(), - num_pending: z.number(), - status: z.string(), - wan_ip: z.string().ip(), - gateways: z.array(z.string().ip()), - netmask: z.string().ip(), - nameservers: z.array(z.string().ip()).optional(), - num_sta: z.number(), - "tx_bytes-r": z.number(), - "rx_bytes-r": z.number(), - gw_mac: z.string(), - gw_name: z.string(), - "gw_system-stats": z.object({ - cpu: z.string(), - mem: z.string(), - uptime: z.string(), - }), - gw_version: z.string(), - isp_name: z.string(), - isp_organization: z.string(), - uptime_stats: z.object({ - WAN: z.object({ - alerting_monitors: z.array( - z.object({ - availability: z.number(), - latency_average: z.number(), - target: z.string(), - type: z.enum(["icmp", "dns"]), - }), - ), - availability: z.number(), - latency_average: z.number(), - monitors: z.array( - z.object({ - availability: z.number(), - latency_average: z.number(), - target: z.string(), - type: z.enum(["icmp", "dns"]), - }), - ), - time_period: z.number(), - uptime: z.number(), - }), - }), - }), - z.object({ - subsystem: z.literal("www"), - status: z.string(), - "tx_bytes-r": z.number(), - "rx_bytes-r": z.number(), - latency: z.number(), - uptime: z.number(), - drops: z.number(), - xput_up: z.number(), - xput_down: z.number(), - speedtest_status: z.string(), - speedtest_lastrun: z.number(), - speedtest_ping: z.number(), - gw_mac: z.string(), - }), - z.object({ - subsystem: z.literal("lan"), - lan_ip: z.string().ip().nullish(), - status: z.string(), - num_user: z.number(), - num_guest: z.number(), - num_iot: z.number(), - "tx_bytes-r": z.number(), - "rx_bytes-r": z.number(), - num_sw: z.number(), - num_adopted: z.number(), - num_disconnected: z.number(), - num_pending: z.number(), - }), - z.object({ - subsystem: z.literal("vpn"), - status: z.string(), - remote_user_enabled: z.boolean(), - remote_user_num_active: z.number(), - remote_user_num_inactive: z.number(), - remote_user_rx_bytes: z.number(), - remote_user_tx_bytes: z.number(), - remote_user_rx_packets: z.number(), - remote_user_tx_packets: z.number(), - site_to_site_enabled: z.boolean(), - }), -]); - -export type Health = z.infer; - -export const siteSchema = z.object({ - anonymous_id: z.string().uuid(), - name: z.string(), - external_id: z.string().uuid(), - _id: z.string(), - attr_no_delete: z.boolean(), - attr_hidden_id: z.string(), - desc: z.string(), - health: z.array(healthSchema), - num_new_alarms: z.number(), -}); -export type Site = z.infer; - -export const unifiSummaryResponseSchema = z.object({ - meta: z.object({ - rc: z.enum(["ok"]), - }), - data: z.array(siteSchema), -}); +export type HealthSubsystem = SiteStats["health"][number]["subsystem"]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d043e25e..e97eeaf55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1312,6 +1312,9 @@ importers: node-ical: specifier: ^0.20.1 version: 0.20.1 + node-unifi: + specifier: ^2.5.1 + version: 2.5.1(undici@7.8.0) proxmox-api: specifier: 1.1.1 version: 1.1.1 @@ -1337,6 +1340,9 @@ importers: '@homarr/tsconfig': specifier: workspace:^0.1.0 version: link:../../tooling/typescript + '@types/node-unifi': + specifier: ^2.5.1 + version: 2.5.1 '@types/xml2js': specifier: ^0.4.14 version: 0.4.14 @@ -4857,6 +4863,9 @@ packages: '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + '@types/node-unifi@2.5.1': + resolution: {integrity: sha512-NgZ7Q7k6CehvneroTcqeeJT3lcpQEAyntwF8XA6QFwHsNIo0ZC7Ba5d1kCmBkRZU7+oX6YDlCLflYbbzEJPvbg==} + '@types/node@18.19.50': resolution: {integrity: sha512-xonK+NRrMBRtkL1hVCc3G+uXtjh1Al4opBLjqVmipe5ZAaBYWW6cNAiBVZ1BvmkBhep698rP3UM3aRAdSALuhg==} @@ -5388,6 +5397,9 @@ packages: resolution: {integrity: sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==} engines: {node: '>=4'} + axios@1.6.2: + resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} + axios@1.7.7: resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} @@ -6630,6 +6642,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} @@ -7146,6 +7161,19 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-cookie-agent@5.0.4: + resolution: {integrity: sha512-OtvikW69RvfyP6Lsequ0fN5R49S+8QcS9zwd58k6VSr6r57T8G29BkPdyrBcSwLq6ExLs9V+rBlfxu7gDstJag==} + engines: {node: '>=14.18.0 <15.0.0 || >=16.0.0'} + peerDependencies: + deasync: ^0.1.26 + tough-cookie: ^4.0.0 + undici: ^5.11.0 + peerDependenciesMeta: + deasync: + optional: true + undici: + optional: true + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -8343,6 +8371,10 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + node-unifi@2.5.1: + resolution: {integrity: sha512-mYLJFNKhONaXIFU2PeQ+p1fjr6C3q/Na8XyhZXpGalOArCAJLzpAoWl1rg9ZbmuJiVqwprqCq3u9Srn23CcpuA==} + engines: {node: '>=14.18.0 <15.0.0 || >=16.0.0'} + nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} @@ -8960,6 +8992,9 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -8967,6 +9002,9 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -10019,6 +10057,10 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tough-cookie@5.0.0: resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} engines: {node: '>=16'} @@ -10339,6 +10381,10 @@ packages: universal-user-agent@7.0.2: resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -10397,6 +10443,10 @@ packages: url-toolkit@2.2.5: resolution: {integrity: sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==} + url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -13587,6 +13637,10 @@ snapshots: '@types/node': 22.15.2 form-data: 4.0.1 + '@types/node-unifi@2.5.1': + dependencies: + eventemitter2: 6.4.9 + '@types/node@18.19.50': dependencies: undici-types: 5.26.5 @@ -14254,6 +14308,14 @@ snapshots: axe-core@4.10.0: {} + axios@1.6.2: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.7.7: dependencies: follow-redirects: 1.15.9 @@ -15707,6 +15769,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter2@6.4.9: {} + eventemitter3@4.0.7: {} events@3.3.0: {} @@ -16297,6 +16361,13 @@ snapshots: html-url-attributes@3.0.1: {} + http-cookie-agent@5.0.4(tough-cookie@4.1.4)(undici@7.8.0): + dependencies: + agent-base: 7.1.3 + tough-cookie: 4.1.4 + optionalDependencies: + undici: 7.8.0 + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -17641,6 +17712,21 @@ snapshots: node-releases@2.0.19: {} + node-unifi@2.5.1(undici@7.8.0): + dependencies: + axios: 1.6.2 + eventemitter2: 6.4.9 + http-cookie-agent: 5.0.4(tough-cookie@4.1.4)(undici@7.8.0) + tough-cookie: 4.1.4 + url: 0.11.4 + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - deasync + - debug + - undici + - utf-8-validate + nopt@5.0.0: dependencies: abbrev: 1.1.1 @@ -18289,6 +18375,10 @@ snapshots: proxy-from-env@1.1.0: {} + psl@1.15.0: + dependencies: + punycode: 2.3.1 + pump@3.0.2: dependencies: end-of-stream: 1.4.4 @@ -18296,6 +18386,8 @@ snapshots: punycode.js@2.3.1: {} + punycode@1.4.1: {} + punycode@2.3.1: {} pupa@2.1.1: @@ -19640,6 +19732,13 @@ snapshots: totalist@3.0.1: {} + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tough-cookie@5.0.0: dependencies: tldts: 6.1.69 @@ -19978,6 +20077,8 @@ snapshots: universal-user-agent@7.0.2: {} + universalify@0.2.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -20048,6 +20149,11 @@ snapshots: url-toolkit@2.2.5: {} + url@0.11.4: + dependencies: + punycode: 1.4.1 + qs: 6.13.1 + use-callback-ref@1.3.3(@types/react@19.1.2)(react@19.1.0): dependencies: react: 19.1.0