feat: migrate to unifi package (#2894)

This commit is contained in:
Manuel
2025-04-25 18:58:55 +00:00
committed by GitHub
parent 3dcee8cb86
commit 8550c69d51
7 changed files with 166 additions and 309 deletions

View File

@@ -26,6 +26,7 @@
"Sabnzbd",
"SeDemal",
"Sonarr",
"sslverify",
"superjson",
"tabler",
"trpc",

View File

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

View File

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

View File

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

View File

@@ -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<NetworkControllerSummary> {
if (!this.headers) {
await this.authenticateAndConstructSessionInHeaderAsync();
}
const requestUrl = this.url(`/${this.prefix}/api/stat/sites`);
const requestHeaders: Record<string, string> = {
"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<void> {
await this.authenticateAndConstructSessionInHeaderAsync();
const client = await this.createControllerClientAsync();
await client.getSitesStats();
}
private getStatusValueOverAllSites(
data: z.infer<typeof unifiSummaryResponseSchema>,
subsystem: Subsystem,
selectCallback: (obj: z.infer<typeof unifiSummaryResponseSchema>["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<S extends HealthSubsystem>(
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<z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"][number], { subsystem: S }>,
>(
data: z.infer<typeof unifiSummaryResponseSchema>,
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<SiteStats["health"][number], { subsystem: S }>,
>(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<typeof unifiSummaryResponseSchema>,
subsystem: Subsystem,
selectCallback: (obj: z.infer<typeof unifiSummaryResponseSchema>["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<typeof unifiSummaryResponseSchema>["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<string, string> | undefined = undefined;
private csrfToken: string | undefined;
private async authenticateAndConstructSessionInHeaderAsync(): Promise<void> {
await this.determineUDMVariantAsync();
await this.authenticateAndSetCookieAsync();
}
private async authenticateAndSetCookieAsync(): Promise<void> {
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<string, string> = { "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<string, string> = {};
const loginToken = UnifiControllerIntegration.extractLoginTokenFromCookies(responseHeaders);
newHeaders.Cookie = `${loginToken};`;
this.headers = newHeaders;
}
private async determineUDMVariantAsync(): Promise<void> {
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");
}
}

View File

@@ -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<typeof healthSchema>;
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<typeof siteSchema>;
export const unifiSummaryResponseSchema = z.object({
meta: z.object({
rc: z.enum(["ok"]),
}),
data: z.array(siteSchema),
});
export type HealthSubsystem = SiteStats["health"][number]["subsystem"];

106
pnpm-lock.yaml generated
View File

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