mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 08:20:56 +01:00
feat: migrate to unifi package (#2894)
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -26,6 +26,7 @@
|
||||
"Sabnzbd",
|
||||
"SeDemal",
|
||||
"Sonarr",
|
||||
"sslverify",
|
||||
"superjson",
|
||||
"tabler",
|
||||
"trpc",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
106
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user