From cd9a781246ecfb96c617ef0c779587c146603a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=80=9D=E8=BF=BD=EF=BC=88shaco=EF=BC=89?= <1679924785@qq.com> Date: Sun, 2 Aug 2026 17:49:43 +0800 Subject: [PATCH 1/5] feat: add managed nginx config deployments --- .gitattributes | 5 + backend/.npmrc | 3 + backend/app.js | 205 +- backend/index.js | 108 +- backend/internal/nginx-config-artifacts.js | 81 + backend/internal/nginx-config-diagnostics.js | 150 + backend/internal/nginx-config-hash.js | 71 + backend/internal/nginx-config-normalizer.js | 443 ++ backend/internal/nginx-config-renderer.js | 356 ++ backend/internal/nginx-config-validator.js | 75 + .../internal/nginx-deployment-coordinator.js | 471 ++ backend/internal/nginx-deployment-store.js | 56 + backend/internal/nginx-host-adapters.js | 43 + backend/internal/nginx-preview-token.js | 27 + backend/internal/nginx.js | 488 +-- backend/internal/proxy-host.js | 527 ++- backend/lib/error.js | 33 + backend/lib/utils.js | 27 +- ...120000_proxy_host_nginx_desired_applied.js | 43 + .../20260731120100_nginx_deployment.js | 38 + backend/models/nginx_deployment.js | 28 + backend/models/proxy_host.js | 264 +- backend/package.json | 3 +- backend/pnpm-lock.yaml | 3143 ++++++++++++++ backend/routes/nginx/proxy_hosts.js | 36 + .../schema/components/proxy-host-object.json | 427 +- .../paths/nginx/proxy-hosts/hostID/put.json | 322 +- .../schema/paths/nginx/proxy-hosts/post.json | 316 +- backend/templates/proxy_host.conf | 8 +- backend/test/nginx/config-renderer.test.js | 536 +++ backend/test/nginx/config-validator.test.js | 50 + .../test/nginx/deployment-coordinator.test.js | 83 + .../fixtures/legacy-default-proxy-host.conf | 77 + .../fixtures/legacy-default-proxy-host.json | 21 + backend/test/nginx/legacy-golden.test.js | 14 + backend/test/nginx/migrations.test.js | 62 + backend/test/nginx/preview-token.test.js | 25 + docker/dev/Dockerfile | 12 +- docker/docker-compose.dev.yml | 6 + .../rootfs/etc/s6-overlay/s6-rc.d/backend/run | 4 + .../etc/s6-overlay/s6-rc.d/frontend/run | 4 +- docker/scripts/install-s6 | 9 +- frontend/.npmrc | 3 + frontend/pnpm-lock.yaml | 3779 +++++++++++++++++ .../api/backend/getProxyHostNginxConfig.ts | 5 + frontend/src/api/backend/index.ts | 2 + frontend/src/api/backend/models.ts | 534 ++- .../backend/previewProxyHostNginxConfig.ts | 5 + .../components/Form/AccessClientFields.tsx | 259 +- frontend/src/components/Form/AccessField.tsx | 3 + .../src/components/Form/BasicAuthFields.tsx | 207 +- .../src/components/Form/DomainNamesField.tsx | 18 +- .../src/components/Form/LocationsFields.tsx | 448 +- .../src/components/Form/NginxConfigField.tsx | 3 + .../components/Form/ProxyDirectivesFields.tsx | 803 ++++ .../components/Form/SSLCertificateField.tsx | 3 + .../src/components/Form/SSLOptionsFields.tsx | 74 +- frontend/src/components/Form/index.ts | 19 +- frontend/src/hooks/useProxyHost.ts | 12 - .../src/locale/src/HelpDoc/en/ProxyHosts.md | 22 +- .../src/locale/src/HelpDoc/zh/ProxyHosts.md | 22 +- frontend/src/locale/src/en.json | 756 ++++ frontend/src/locale/src/zh.json | 756 ++++ frontend/src/modals/ProxyHostModal.tsx | 1201 ++++-- frontend/src/pages/Nginx/ProxyHosts/Table.tsx | 407 +- .../pages/Nginx/ProxyHosts/TableWrapper.tsx | 260 +- 66 files changed, 15868 insertions(+), 2433 deletions(-) create mode 100644 .gitattributes create mode 100644 backend/.npmrc create mode 100644 backend/internal/nginx-config-artifacts.js create mode 100644 backend/internal/nginx-config-diagnostics.js create mode 100644 backend/internal/nginx-config-hash.js create mode 100644 backend/internal/nginx-config-normalizer.js create mode 100644 backend/internal/nginx-config-renderer.js create mode 100644 backend/internal/nginx-config-validator.js create mode 100644 backend/internal/nginx-deployment-coordinator.js create mode 100644 backend/internal/nginx-deployment-store.js create mode 100644 backend/internal/nginx-host-adapters.js create mode 100644 backend/internal/nginx-preview-token.js create mode 100644 backend/migrations/20260731120000_proxy_host_nginx_desired_applied.js create mode 100644 backend/migrations/20260731120100_nginx_deployment.js create mode 100644 backend/models/nginx_deployment.js create mode 100644 backend/pnpm-lock.yaml create mode 100644 backend/test/nginx/config-renderer.test.js create mode 100644 backend/test/nginx/config-validator.test.js create mode 100644 backend/test/nginx/deployment-coordinator.test.js create mode 100644 backend/test/nginx/fixtures/legacy-default-proxy-host.conf create mode 100644 backend/test/nginx/fixtures/legacy-default-proxy-host.json create mode 100644 backend/test/nginx/legacy-golden.test.js create mode 100644 backend/test/nginx/migrations.test.js create mode 100644 backend/test/nginx/preview-token.test.js create mode 100644 frontend/.npmrc create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/src/api/backend/getProxyHostNginxConfig.ts create mode 100644 frontend/src/api/backend/previewProxyHostNginxConfig.ts create mode 100644 frontend/src/components/Form/ProxyDirectivesFields.tsx diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..2441fa2f2b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Files copied into the Linux image and scripts executed by the host must use LF endings. +docker/rootfs/** text eol=lf +*.sh text eol=lf +scripts/** text eol=lf +docker/scripts/** text eol=lf \ No newline at end of file diff --git a/backend/.npmrc b/backend/.npmrc new file mode 100644 index 0000000000..b1211e14a2 --- /dev/null +++ b/backend/.npmrc @@ -0,0 +1,3 @@ +auto-install-peers=true +registry=https://registry.npmmirror.com +strict-ssl=false diff --git a/backend/app.js b/backend/app.js index b1f047661c..556e684783 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,92 +1,113 @@ -import bodyParser from "body-parser"; -import compression from "compression"; -import express from "express"; -import fileUpload from "express-fileupload"; -import { isDebugMode } from "./lib/config.js"; -import cors from "./lib/express/cors.js"; -import jwt from "./lib/express/jwt.js"; -import { debug, express as logger } from "./logger.js"; -import mainRoutes from "./routes/main.js"; - -/** - * App - */ -const app = express(); -app.use(fileUpload()); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); - -// Gzip -app.use(compression()); - -/** - * General Logging, BEFORE routes - */ - -app.disable("x-powered-by"); -app.enable("trust proxy", ["loopback", "linklocal", "uniquelocal"]); -app.enable("strict routing"); - -// pretty print JSON when not live -if (isDebugMode()) { - app.set("json spaces", 2); -} - -// CORS for everything -app.use(cors); - -// General security/cache related headers + server header -app.use((_, res, next) => { - let x_frame_options = "DENY"; - - if (typeof process.env.X_FRAME_OPTIONS !== "undefined" && process.env.X_FRAME_OPTIONS) { - x_frame_options = process.env.X_FRAME_OPTIONS; - } - - res.set({ - "X-XSS-Protection": "1; mode=block", - "X-Content-Type-Options": "nosniff", - "X-Frame-Options": x_frame_options, - "Cache-Control": "no-cache, no-store, max-age=0, must-revalidate", - Pragma: "no-cache", - Expires: 0, - }); - next(); -}); - -app.use(jwt()); -app.use("/", mainRoutes); - -// production error handler -// no stacktraces leaked to user -app.use((err, req, res, _) => { - const payload = { - error: { - code: err.status || 500, - message: err.public ? err.message : "Internal Error", - }, - }; - - if (typeof err.message_i18n !== "undefined") { - payload.error.message_i18n = err.message_i18n; - } - - if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) { - payload.debug = { - stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null, - previous: err.previous, - }; - } - - // Not every error is worth logging - but this is good for now until it gets annoying. - if (typeof err.stack !== "undefined" && err.stack) { - debug(logger, err.stack); - if (typeof err.public === "undefined" || !err.public) { - logger.warn(err.message); - } - } - - res.status(err.status || 500).send(payload); -}); - -export default app; +import bodyParser from "body-parser"; +import compression from "compression"; +import express from "express"; +import fileUpload from "express-fileupload"; +import { isDebugMode } from "./lib/config.js"; +import cors from "./lib/express/cors.js"; +import jwt from "./lib/express/jwt.js"; +import { debug, express as logger } from "./logger.js"; +import mainRoutes from "./routes/main.js"; +import nginxDeploymentCoordinator from "./internal/nginx-deployment-coordinator.js"; + +/** + * App + */ +const app = express(); +app.use(fileUpload()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +// Gzip +app.use(compression()); + +/** + * General Logging, BEFORE routes + */ + +app.disable("x-powered-by"); +app.enable("trust proxy", ["loopback", "linklocal", "uniquelocal"]); +app.enable("strict routing"); + +// pretty print JSON when not live +if (isDebugMode()) { + app.set("json spaces", 2); +} + +// CORS for everything +app.use(cors); + +// General security/cache related headers + server header +app.use((_, res, next) => { + let x_frame_options = "DENY"; + + if (typeof process.env.X_FRAME_OPTIONS !== "undefined" && process.env.X_FRAME_OPTIONS) { + x_frame_options = process.env.X_FRAME_OPTIONS; + } + + res.set({ + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": x_frame_options, + "Cache-Control": "no-cache, no-store, max-age=0, must-revalidate", + Pragma: "no-cache", + Expires: 0, + }); + next(); +}); + +app.use(jwt()); + +// Reconcile any interrupted atomic deployment before API routes can trigger a +// new configuration change. Recovery only touches journals left by the +// coordinator; an empty deployment directory is a no-op. +try { + const recoveredDeployments = await nginxDeploymentCoordinator.recover(); + if (recoveredDeployments.length) logger.warn(`Recovered ${recoveredDeployments.length} interrupted nginx deployment(s)`); +} catch (error) { + logger.error(`Unable to recover nginx deployments: ${error.message}`); + throw error; +} + +app.use("/", mainRoutes); + +// production error handler +// no stacktraces leaked to user +app.use((err, req, res, _) => { + const payload = { + error: { + code: err.status || 500, + message: err.public ? err.message : "Internal Error", + }, + }; + + if (typeof err.error_code !== "undefined") { + payload.error.error_code = err.error_code; + } + + if (typeof err.details !== "undefined") { + payload.error.details = err.details; + } + + if (typeof err.message_i18n !== "undefined") { + payload.error.message_i18n = err.message_i18n; + } + + if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) { + payload.debug = { + stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null, + previous: err.previous, + }; + } + + // Not every error is worth logging - but this is good for now until it gets annoying. + if (typeof err.stack !== "undefined" && err.stack) { + debug(logger, err.stack); + if (typeof err.public === "undefined" || !err.public) { + logger.warn(err.message); + } + } + + res.status(err.status || 500).send(payload); +}); + +export default app; diff --git a/backend/index.js b/backend/index.js index 0028566727..426d5e5b6b 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,54 +1,54 @@ -#!/usr/bin/env node - -import app from "./app.js"; -import internalCertificate from "./internal/certificate.js"; -import internalIpRanges from "./internal/ip_ranges.js"; -import { global as logger } from "./logger.js"; -import { migrateUp } from "./migrate.js"; -import { getCompiledSchema } from "./schema/index.js"; -import setup from "./setup.js"; - -const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== "false"; - -async function appStart() { - return migrateUp() - .then(setup) - .then(getCompiledSchema) - .then(() => { - if (!IP_RANGES_FETCH_ENABLED) { - logger.info("IP Ranges fetch is disabled by environment variable"); - return; - } - logger.info("IP Ranges fetch is enabled"); - return internalIpRanges.fetch().catch((err) => { - logger.error("IP Ranges fetch failed, continuing anyway:", err.message); - }); - }) - .then(() => { - internalCertificate.initTimer(); - internalIpRanges.initTimer(); - - const server = app.listen(3000, () => { - logger.info(`Backend PID ${process.pid} listening on port 3000 ...`); - - process.on("SIGTERM", () => { - logger.info(`PID ${process.pid} received SIGTERM`); - server.close(() => { - logger.info("Stopping."); - process.exit(0); - }); - }); - }); - }) - .catch((err) => { - logger.error(`Startup Error: ${err.message}`, err); - setTimeout(appStart, 1000); - }); -} - -try { - appStart(); -} catch (err) { - logger.fatal(err); - process.exit(1); -} +#!/usr/bin/env node + +import app from "./app.js"; +import internalCertificate from "./internal/certificate.js"; +import internalIpRanges from "./internal/ip_ranges.js"; +import { global as logger } from "./logger.js"; +import { migrateUp } from "./migrate.js"; +import { getCompiledSchema } from "./schema/index.js"; +import setup from "./setup.js"; + +const IP_RANGES_FETCH_ENABLED = process.env.IP_RANGES_FETCH_ENABLED !== "false"; + +async function appStart() { + return migrateUp() + .then(setup) + .then(getCompiledSchema) + .then(() => { + if (!IP_RANGES_FETCH_ENABLED) { + logger.info("IP Ranges fetch is disabled by environment variable"); + return; + } + logger.info("IP Ranges fetch is enabled"); + return internalIpRanges.fetch().catch((err) => { + logger.error("IP Ranges fetch failed, continuing anyway:", err.message); + }); + }) + .then(() => { + internalCertificate.initTimer(); + internalIpRanges.initTimer(); + + const server = app.listen(3000, () => { + logger.info(`Backend PID ${process.pid} listening on port 3000 ...`); + + process.on("SIGTERM", () => { + logger.info(`PID ${process.pid} received SIGTERM`); + server.close(() => { + logger.info("Stopping."); + process.exit(0); + }); + }); + }); + }) + .catch((err) => { + logger.error(`Startup Error: ${err.message}`, err); + setTimeout(appStart, 1000); + }); +} + +try { + appStart(); +} catch (err) { + logger.fatal(err); + process.exit(1); +} diff --git a/backend/internal/nginx-config-artifacts.js b/backend/internal/nginx-config-artifacts.js new file mode 100644 index 0000000000..81fd654788 --- /dev/null +++ b/backend/internal/nginx-config-artifacts.js @@ -0,0 +1,81 @@ +import fs from "node:fs/promises"; +import { dirname, isAbsolute, join, relative, resolve } from "node:path"; +import { randomUUID } from "node:crypto"; +import { getActivePath } from "./nginx-host-adapters.js"; + +export const deploymentRoot = (nginxRoot = "/data/nginx") => join(nginxRoot, ".deploy"); + +export const assertInside = (root, path) => { + const rootPath = resolve(root); + const target = resolve(path); + if (target !== rootPath && !target.startsWith(`${rootPath}${process.platform === "win32" ? "\\" : "/"}`)) { + throw new Error(`Path escapes configured nginx root: ${path}`); + } + return target; +}; + +export const activeArtifactPath = (hostType, hostId, nginxRoot = "/data/nginx") => getActivePath(hostType, hostId, nginxRoot); +export const candidateArtifactPath = (hostType, hostId, operationId, nginxRoot = "/data/nginx") => + assertInside(deploymentRoot(nginxRoot), join(deploymentRoot(nginxRoot), "candidates", hostType, String(hostId), `${operationId}.conf`)); +export const stagingArtifactPath = (hostType, hostId, operationId, nginxRoot = "/data/nginx") => + assertInside(deploymentRoot(nginxRoot), join(deploymentRoot(nginxRoot), "staging", operationId, hostType, `${hostId}.conf`)); +export const backupArtifactPath = (hostType, hostId, operationId, nginxRoot = "/data/nginx") => + assertInside(deploymentRoot(nginxRoot), join(deploymentRoot(nginxRoot), "backups", operationId, hostType, `${hostId}.conf`)); +export const journalPath = (operationId, nginxRoot = "/data/nginx") => assertInside(deploymentRoot(nginxRoot), join(deploymentRoot(nginxRoot), "journal", `${operationId}.json`)); + +export const readArtifact = async (path) => { + try { + return await fs.readFile(path, "utf8"); + } catch (error) { + if (error.code === "ENOENT") return null; + throw error; + } +}; + +/** Atomic within one filesystem: write, fsync, rename and fsync directory. */ +export const atomicWrite = async (path, content) => { + await fs.mkdir(dirname(path), { recursive: true }); + const temp = join(dirname(path), `.${randomUUID()}.tmp`); + let handle; + try { + handle = await fs.open(temp, "wx", 0o600); + await handle.writeFile(content, "utf8"); + await handle.sync(); + await handle.close(); + handle = null; + await fs.rename(temp, path); + try { + const directory = await fs.open(dirname(path), "r"); + await directory.sync(); + await directory.close(); + } catch (error) { + if (!["EINVAL", "EPERM", "ENOTSUP"].includes(error.code)) throw error; + } + } finally { + if (handle) await handle.close(); + await fs.rm(temp, { force: true }).catch(() => undefined); + } +}; + +export const removeArtifact = async (path) => fs.rm(path, { force: true }); + +export const writeJournal = async (journal, nginxRoot = "/data/nginx") => atomicWrite(journalPath(journal.operation_id, nginxRoot), `${JSON.stringify(journal, null, 2)}\n`); +export const readJournals = async (nginxRoot = "/data/nginx") => { + const directory = join(deploymentRoot(nginxRoot), "journal"); + try { + const names = await fs.readdir(directory); + return Promise.all(names.filter((name) => name.endsWith(".json")).map(async (name) => JSON.parse(await fs.readFile(join(directory, name), "utf8")))); + } catch (error) { + if (error.code === "ENOENT") return []; + throw error; + } +}; +export const deleteJournal = async (operationId, nginxRoot = "/data/nginx") => fs.rm(journalPath(operationId, nginxRoot), { force: true }); + +export const toLogicalPath = (path, nginxRoot = "/data/nginx") => { + const value = relative(nginxRoot, path).replace(/\\/g, "/"); + if (!value || value.startsWith("../") || isAbsolute(value)) throw new Error("Artifact path is outside nginx root"); + return value; +}; + +export default { deploymentRoot, assertInside, activeArtifactPath, candidateArtifactPath, stagingArtifactPath, backupArtifactPath, journalPath, readArtifact, atomicWrite, removeArtifact, writeJournal, readJournals, deleteJournal, toLogicalPath }; diff --git a/backend/internal/nginx-config-diagnostics.js b/backend/internal/nginx-config-diagnostics.js new file mode 100644 index 0000000000..aa26d1a525 --- /dev/null +++ b/backend/internal/nginx-config-diagnostics.js @@ -0,0 +1,150 @@ +const BLOCKING_DIRECTIVES = new Set([ + "proxy_pass", + "listen", + "ssl_certificate", + "ssl_certificate_key", +]); +const WARNING_DIRECTIVES = new Set([ + "proxy_connect_timeout", + "proxy_send_timeout", + "proxy_read_timeout", + "proxy_buffering", + "proxy_request_buffering", + "proxy_buffer_size", + "proxy_busy_buffers_size", + "proxy_buffers", + "proxy_max_temp_file_size", + "proxy_temp_file_write_size", + "proxy_limit_rate", + "proxy_headers_hash_bucket_size", + "proxy_headers_hash_max_size", + "proxy_http_version", + "proxy_method", + "proxy_pass_request_headers", + "proxy_pass_request_body", + "proxy_pass_trailers", + "proxy_ignore_client_abort", + "proxy_socket_keepalive", + "proxy_bind", + "proxy_set_header", + "proxy_hide_header", + "proxy_pass_header", + "proxy_ignore_headers", + "add_header", + "proxy_next_upstream", + "proxy_next_upstream_timeout", + "proxy_next_upstream_tries", + "proxy_intercept_errors", + "proxy_force_ranges", + "proxy_redirect", + "proxy_cookie_domain", + "proxy_cookie_path", + "proxy_ssl_server_name", + "proxy_ssl_name", + "proxy_ssl_verify", + "proxy_ssl_verify_depth", + "proxy_ssl_session_reuse", + "proxy_ssl_protocols", + "proxy_ssl_ciphers", +]); + +const diagnostic = (severity, code, line, message) => ({ severity, code, scope: "advanced_config", line, message }); + +/** + * A deliberately small lexer: it recognises top-level directive tokens while + * ignoring comments and quoted strings. It is not an nginx parser; nginx -t + * remains the final authority. + * + * @param {string|undefined|null} config + * @returns {Array} + */ +export const scanAdvancedConfig = (config) => { + if (!config) { + return []; + } + const result = []; + let token = ""; + let line = 1; + let tokenLine = 1; + let quote = null; + let escaped = false; + let comment = false; + let depth = 0; + + const flushDirective = (delimiter) => { + const directive = token.trim().split(/\s+/)[0]?.toLowerCase(); + if (!directive) { + token = ""; + return; + } + if (depth === 0) { + if (directive === "server" || directive === "location") { + result.push(diagnostic("error", "ADVANCED_MANAGED_BLOCK", tokenLine, `Advanced config may not define ${directive} blocks`)); + } else if (BLOCKING_DIRECTIVES.has(directive)) { + result.push(diagnostic("error", "ADVANCED_MANAGED_DIRECTIVE", tokenLine, `Advanced config may not define ${directive}`)); + } else if (directive === "include" && /(?:proxy\.conf|_access\.conf|_certificates\.conf)/i.test(token)) { + result.push(diagnostic("error", "ADVANCED_MANAGED_INCLUDE", tokenLine, "Advanced config may not replace managed includes")); + } else if (WARNING_DIRECTIVES.has(directive)) { + result.push(diagnostic("warning", "ADVANCED_STRUCTURED_CONFLICT", tokenLine, `Advanced config may override structured ${directive} settings`)); + } + } + token = ""; + if (delimiter === "{") { + depth += 1; + } + }; + + for (let index = 0; index < config.length; index += 1) { + const char = config[index]; + if (comment) { + if (char === "\n") { + comment = false; + line += 1; + } + continue; + } + if (quote) { + token += char; + if (char === "\n") line += 1; + if (escaped) { + escaped = false; + } else if (char === "\\") { + escaped = true; + } else if (char === quote) { + quote = null; + } + continue; + } + if (char === "#") { + comment = true; + continue; + } + if (char === "\"" || char === "'") { + quote = char; + token += char; + continue; + } + if (char === "\n") { + line += 1; + token += " "; + continue; + } + if (char === ";" || char === "{") { + flushDirective(char); + tokenLine = line; + continue; + } + if (char === "}") { + token = ""; + depth = Math.max(0, depth - 1); + tokenLine = line; + continue; + } + if (!token && /\S/.test(char)) tokenLine = line; + token += char; + } + return result; +}; + +export const hasDiagnosticErrors = (diagnostics) => diagnostics.some((item) => item.severity === "error"); +export default { scanAdvancedConfig, hasDiagnosticErrors }; diff --git a/backend/internal/nginx-config-hash.js b/backend/internal/nginx-config-hash.js new file mode 100644 index 0000000000..15880b49bc --- /dev/null +++ b/backend/internal/nginx-config-hash.js @@ -0,0 +1,71 @@ +import { createHash } from "node:crypto"; + +const isPlainObject = (value) => { + if (value === null || typeof value !== "object") { + return false; + } + const prototype = Object.getPrototypeOf(value); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Produces stable JSON without silently accepting values that JSON.stringify + * would drop or coerce. Arrays intentionally retain their order because order + * is semantically meaningful for locations, headers and access rules. + * + * @param {unknown} value + * @returns {string} + */ +export const canonicalize = (value) => { + if (value === null) { + return "null"; + } + + switch (typeof value) { + case "boolean": + return value ? "true" : "false"; + case "number": + if (!Number.isFinite(value)) { + throw new TypeError("Canonical JSON does not support non-finite numbers"); + } + return JSON.stringify(value); + case "string": + return JSON.stringify(value); + case "undefined": + throw new TypeError("Canonical JSON does not support undefined"); + case "object": + if (Array.isArray(value)) { + return `[${value.map((item) => canonicalize(item)).join(",")}]`; + } + if (!isPlainObject(value)) { + throw new TypeError("Canonical JSON only supports plain objects"); + } + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`) + .join(",")}}`; + default: + throw new TypeError(`Canonical JSON does not support ${typeof value}`); + } +}; + +/** @param {string|Buffer} value */ +export const sha256 = (value) => `sha256:${createHash("sha256").update(value).digest("hex")}`; + +/** @param {unknown} value */ +export const hashCanonical = (value) => sha256(canonicalize(value)); + +/** + * Hashes fixed-path dependency/template inputs without embedding their content + * into database records. Callers must pass a deterministic relative path. + * + * @param {Array<{path: string, content: string|Buffer}>} entries + */ +export const hashFileManifest = (entries) => { + const normalized = entries + .map(({ path, content }) => ({ path, content: Buffer.isBuffer(content) ? content.toString("base64") : String(content) })) + .sort((left, right) => left.path.localeCompare(right.path)); + return hashCanonical(normalized); +}; + +export default { canonicalize, sha256, hashCanonical, hashFileManifest }; diff --git a/backend/internal/nginx-config-normalizer.js b/backend/internal/nginx-config-normalizer.js new file mode 100644 index 0000000000..48adc5069f --- /dev/null +++ b/backend/internal/nginx-config-normalizer.js @@ -0,0 +1,443 @@ +import { isIP } from "node:net"; +import errs from "../lib/error.js"; + +export const NGINX_CONFIG_SCHEMA_VERSION = 1; +export const MATCH_TYPES = new Set(["prefix", "priority_prefix", "exact", "regex", "regex_i"]); +export const PATH_MODES = new Set(["preserve_uri", "strip_prefix", "replace_prefix"]); +const HEADER_TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/; +const DURATION = /^(0|[1-9][0-9]*)(ms|s|m|h|d)?$/; +const SIZE = /^(0|[1-9][0-9]*)(k|m|g)?$/; +const VARIABLE = /^\$(?:host|scheme|remote_addr|proxy_add_x_forwarded_for|http_upgrade|http_connection|request_id)$/; +const PROTECTED_REQUEST_HEADERS = new Set([ + "host", + "x-forwarded-scheme", + "x-forwarded-proto", + "x-forwarded-for", + "x-real-ip", +]); + +const clone = (value) => structuredClone(value ?? {}); +const invalid = (code, message, details = {}) => { + throw new errs.UnprocessableConfigError(message, { code, ...details }); +}; +const assertNoControl = (value, field) => { + if (typeof value !== "string" || /[\r\n\0]/.test(value)) + invalid("INVALID_NGINX_VALUE", `${field} contains an invalid control character`); +}; + +export const normalizeDuration = (value, field = "duration") => { + if (typeof value !== "string" || !DURATION.test(value)) + invalid("INVALID_DURATION", `${field} must be a whole nginx duration`); + return value; +}; +export const normalizeSize = (value, field = "size") => { + if (typeof value !== "string" || !SIZE.test(value.toLowerCase())) + invalid("INVALID_SIZE", `${field} must be a whole nginx size`); + return value.toLowerCase(); +}; +export const normalizePort = (value, field = "port") => { + if (!Number.isInteger(value) || value < 1 || value > 65535) + invalid("INVALID_PORT", `${field} must be an integer from 1 to 65535`); + return value; +}; +export const normalizeHeaderName = (value) => { + if (typeof value !== "string" || !HEADER_TOKEN.test(value)) + invalid("INVALID_HEADER_NAME", "Header name is invalid"); + return value; +}; +export const normalizeHeaderValue = (value, mode = "literal") => { + assertNoControl(value, "Header value"); + if (Buffer.byteLength(value, "utf8") > 8192) invalid("HEADER_VALUE_TOO_LONG", "Header value exceeds 8192 bytes"); + if (mode === "variable" && !VARIABLE.test(value)) + invalid("INVALID_HEADER_VARIABLE", "Header variable is not allowed"); + if (mode !== "literal" && mode !== "variable") invalid("INVALID_HEADER_VALUE_MODE", "Header value mode is invalid"); + return value; +}; + +/** @param {unknown} value */ +export const normalizeNginxConfig = (value) => { + if (value === null || typeof value === "undefined") return { schema_version: NGINX_CONFIG_SCHEMA_VERSION }; + if (typeof value !== "object" || Array.isArray(value)) + invalid("INVALID_NGINX_CONFIG", "nginx_config must be an object"); + const config = clone(value); + if (typeof config.schema_version === "undefined") config.schema_version = NGINX_CONFIG_SCHEMA_VERSION; + if (config.schema_version !== NGINX_CONFIG_SCHEMA_VERSION) { + invalid("UNSUPPORTED_NGINX_CONFIG_SCHEMA", `nginx_config schema ${config.schema_version} is not supported`); + } + return config; +}; + +const normalizePath = (path, matchType) => { + assertNoControl(path, "Location path"); + if (!path || Buffer.byteLength(path, "utf8") > 1024) invalid("INVALID_LOCATION_PATH", "Location path is invalid"); + if (matchType === "regex" || matchType === "regex_i") return path; + if (!path.startsWith("/")) invalid("INVALID_LOCATION_PATH", "Location path must start with /"); + return path; +}; + +export const normalizeUpstreamHost = (host) => { + assertNoControl(host, "Upstream host"); + if (!host || host.includes("://") || host.includes("@") || host.includes("?") || host.includes("#")) { + invalid("INVALID_UPSTREAM_HOST", "Upstream host must not contain a scheme, credentials, query, or fragment"); + } + if (host.includes("/") || /\s/.test(host)) + invalid("INVALID_UPSTREAM_HOST", "Upstream host must not contain a path or whitespace"); + if (host.startsWith("[") ? !/^\[[0-9a-fA-F:.]+\]$/.test(host) : !/^[A-Za-z0-9.-]+$/.test(host)) { + invalid("INVALID_UPSTREAM_HOST", "Upstream host must be DNS, IPv4, or bracketed IPv6"); + } + return host; +}; + +const normalizeHeaderOperations = (items, field) => { + if (typeof items === "undefined") return undefined; + if (!Array.isArray(items)) invalid("INVALID_HEADERS", `${field} must be an array`); + const seen = new Set(); + return items.map((item, index) => { + if (!item || typeof item !== "object" || Array.isArray(item)) + invalid("INVALID_HEADERS", `${field}[${index}] must be an object`); + const name = normalizeHeaderName(item.name); + const key = name.toLowerCase(); + if (seen.has(key)) invalid("DUPLICATE_HEADER", `${field} contains duplicate header ${name}`); + seen.add(key); + const operation = item.operation || "set"; + if (operation !== "set" && operation !== "remove" && operation !== "add") + invalid("INVALID_HEADER_OPERATION", "Header operation is invalid"); + if (operation === "remove") { + if (field === "request_headers" && PROTECTED_REQUEST_HEADERS.has(key)) + invalid("PROTECTED_HEADER_REMOVE", `Header ${name} is managed by the system`); + return { name, operation }; + } + const value_mode = item.value_mode || "literal"; + return { name, operation, value_mode, value: normalizeHeaderValue(item.value, value_mode) }; + }); +}; + +const DEFAULTABLE_BOOLEAN_FIELDS = [ + "default_location_enabled", + "proxy_buffering", + "proxy_request_buffering", + "proxy_ssl_server_name", + "proxy_ignore_client_abort", + "proxy_intercept_errors", + "proxy_force_ranges", + "proxy_pass_request_body", + "proxy_pass_request_headers", + "proxy_pass_trailers", + "proxy_socket_keepalive", + "proxy_ssl_session_reuse", + "proxy_ssl_verify", +]; +const DURATION_FIELDS = [ + "proxy_connect_timeout", + "proxy_send_timeout", + "proxy_read_timeout", + "proxy_next_upstream_timeout", +]; +const SIZE_FIELDS = [ + "client_max_body_size", + "proxy_buffer_size", + "proxy_busy_buffers_size", + "proxy_max_temp_file_size", + "proxy_temp_file_write_size", + "proxy_limit_rate", +]; +const INTEGER_FIELDS = [ + "proxy_headers_hash_bucket_size", + "proxy_headers_hash_max_size", + "proxy_next_upstream_tries", + "proxy_ssl_verify_depth", +]; +const HTTP_VERSIONS = new Set(["1.0", "1.1"]); +const NEXT_UPSTREAM_VALUES = new Set([ + "error", + "timeout", + "invalid_header", + "http_500", + "http_502", + "http_503", + "http_504", + "http_403", + "http_404", + "http_429", + "non_idempotent", + "off", +]); +const IGNORE_HEADER_VALUES = new Set([ + "X-Accel-Expires", + "X-Accel-Redirect", + "X-Accel-Limit-Rate", + "X-Accel-Buffering", + "X-Accel-Charset", + "Expires", + "Cache-Control", + "Set-Cookie", + "Vary", +]); +const SSL_PROTOCOL_VALUES = new Set(["TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"]); +const METHOD = /^[A-Z][A-Z0-9_-]{0,31}$/; +const CIPHER_LIST = /^[A-Za-z0-9_!+\-:@.]+$/; +const COOKIE_VALUE = /^[^\s;{}"'\\]+$/; + +const optionalText = (value, field, pattern, message) => { + if (typeof value !== "string") invalid("INVALID_NGINX_VALUE", `${field} must be a string`); + const text = value.trim(); + if (!text) return undefined; + assertNoControl(text, field); + if (!pattern.test(text)) invalid("INVALID_NGINX_VALUE", message || `${field} is invalid`); + return text; +}; + +const normalizeStringArray = (items, field, allowed) => { + if (!Array.isArray(items)) invalid("INVALID_NGINX_LIST", `${field} must be an array`); + const values = [...new Set(items.map((item) => String(item).trim()).filter(Boolean))]; + for (const item of values) { + if (allowed ? !allowed.has(item) : !HEADER_TOKEN.test(item)) + invalid("INVALID_NGINX_LIST", `${field} contains an unsupported value`); + } + return values; +}; + +const normalizeCookieRewrites = (items, field) => { + if (!Array.isArray(items)) invalid("INVALID_COOKIE_REWRITES", `${field} must be an array`); + if (items.length > 32) invalid("INVALID_COOKIE_REWRITES", `${field} may contain at most 32 rules`); + return items.map((item, index) => { + if (!item || typeof item !== "object" || Array.isArray(item)) + invalid("INVALID_COOKIE_REWRITES", `${field}[${index}] must be an object`); + const from = optionalText(item.from, `${field}[${index}].from`, COOKIE_VALUE); + const to = optionalText(item.to, `${field}[${index}].to`, COOKIE_VALUE); + if (!from || !to) invalid("INVALID_COOKIE_REWRITES", `${field}[${index}] requires from and to values`); + return { from, to }; + }); +}; + +const normalizeOptions = (options, field) => { + if (typeof options === "undefined" || options === null) return {}; + if (typeof options !== "object" || Array.isArray(options)) + invalid("INVALID_NGINX_OPTIONS", `${field} must be an object`); + const result = {}; + for (const key of DURATION_FIELDS) { + if (typeof options[key] !== "undefined" && options[key] !== "") + result[key] = normalizeDuration(options[key], key); + } + for (const key of SIZE_FIELDS) { + if (typeof options[key] !== "undefined" && options[key] !== "") result[key] = normalizeSize(options[key], key); + } + for (const key of INTEGER_FIELDS) { + if (typeof options[key] !== "undefined" && options[key] !== "") { + const minimum = ["proxy_headers_hash_bucket_size", "proxy_headers_hash_max_size"].includes(key) ? 1 : 0; + if (!Number.isInteger(options[key]) || options[key] < minimum || options[key] > 2147483647) + invalid("INVALID_NGINX_INTEGER", `${key} must be a whole integer of at least ${minimum}`); + result[key] = options[key]; + } + } + for (const key of DEFAULTABLE_BOOLEAN_FIELDS) { + if (typeof options[key] !== "undefined") { + if (typeof options[key] !== "boolean") invalid("INVALID_NGINX_BOOLEAN", `${key} must be boolean`); + result[key] = options[key]; + } + } + if (typeof options.proxy_buffers !== "undefined") { + if ( + !Array.isArray(options.proxy_buffers) || + options.proxy_buffers.length !== 2 || + !Number.isInteger(options.proxy_buffers[0]) || + options.proxy_buffers[0] < 1 + ) { + invalid("INVALID_PROXY_BUFFERS", "proxy_buffers must be [count, size]"); + } + result.proxy_buffers = [ + options.proxy_buffers[0], + normalizeSize(options.proxy_buffers[1], "proxy_buffers size"), + ]; + } + if (typeof options.proxy_http_version !== "undefined" && options.proxy_http_version !== "") { + if (!HTTP_VERSIONS.has(options.proxy_http_version)) + invalid("INVALID_PROXY_HTTP_VERSION", "proxy_http_version must be 1.0 or 1.1"); + result.proxy_http_version = options.proxy_http_version; + } + if (typeof options.proxy_method !== "undefined" && options.proxy_method !== "") + result.proxy_method = optionalText(options.proxy_method, "proxy_method", METHOD, "proxy_method is invalid"); + if (typeof options.proxy_ssl_name !== "undefined" && options.proxy_ssl_name !== "") + result.proxy_ssl_name = normalizeUpstreamHost(options.proxy_ssl_name); + if (typeof options.proxy_ssl_ciphers !== "undefined" && options.proxy_ssl_ciphers !== "") + result.proxy_ssl_ciphers = optionalText( + options.proxy_ssl_ciphers, + "proxy_ssl_ciphers", + CIPHER_LIST, + "proxy_ssl_ciphers is invalid", + ); + if (typeof options.proxy_ssl_protocols !== "undefined") { + const protocols = normalizeStringArray(options.proxy_ssl_protocols, "proxy_ssl_protocols", SSL_PROTOCOL_VALUES); + if (!protocols.length) invalid("INVALID_SSL_PROTOCOLS", "proxy_ssl_protocols may not be empty"); + result.proxy_ssl_protocols = protocols; + } + if (typeof options.proxy_next_upstream !== "undefined") { + const next = normalizeStringArray(options.proxy_next_upstream, "proxy_next_upstream", NEXT_UPSTREAM_VALUES); + if (!next.length || (next.includes("off") && next.length !== 1)) + invalid("INVALID_PROXY_NEXT_UPSTREAM", "proxy_next_upstream must contain conditions, or only off"); + result.proxy_next_upstream = next; + } + if (typeof options.proxy_ignore_headers !== "undefined") + result.proxy_ignore_headers = normalizeStringArray( + options.proxy_ignore_headers, + "proxy_ignore_headers", + IGNORE_HEADER_VALUES, + ); + if (typeof options.proxy_pass_headers !== "undefined") + result.proxy_pass_headers = normalizeStringArray(options.proxy_pass_headers, "proxy_pass_headers"); + if (typeof options.proxy_cookie_domain !== "undefined") + result.proxy_cookie_domain = normalizeCookieRewrites(options.proxy_cookie_domain, "proxy_cookie_domain"); + if (typeof options.proxy_cookie_path !== "undefined") + result.proxy_cookie_path = normalizeCookieRewrites(options.proxy_cookie_path, "proxy_cookie_path"); + if (typeof options.proxy_redirect !== "undefined" && options.proxy_redirect !== "") { + if (!["default", "off"].includes(options.proxy_redirect)) + invalid("INVALID_PROXY_REDIRECT", "proxy_redirect must be default or off"); + result.proxy_redirect = options.proxy_redirect; + } + if (typeof options.proxy_bind !== "undefined" && options.proxy_bind !== "") { + const bind = optionalText( + options.proxy_bind, + "proxy_bind", + /^[0-9A-Fa-f:.]+$/, + "proxy_bind must be an IP address", + ); + if (!bind || !isIP(bind.replace(/^\[|\]$/g, ""))) + invalid("INVALID_PROXY_BIND", "proxy_bind must be an IP address"); + result.proxy_bind = bind; + } + const requestHeaders = normalizeHeaderOperations(options.request_headers, "request_headers"); + if (requestHeaders) result.request_headers = requestHeaders; + const responseHeaders = normalizeHeaderOperations(options.response_headers, "response_headers"); + if (responseHeaders) result.response_headers = responseHeaders; + if (typeof options.hide_response_headers !== "undefined") { + if (!Array.isArray(options.hide_response_headers)) + invalid("INVALID_HEADERS", "hide_response_headers must be an array"); + result.hide_response_headers = [ + ...new Set(options.hide_response_headers.map(normalizeHeaderName).map((name) => name.toLowerCase())), + ].sort(); + } + return result; +}; + +export const normalizeLocation = (location, index = 0) => { + if (!location || typeof location !== "object" || Array.isArray(location)) + invalid("INVALID_LOCATION", `Location ${index} must be an object`); + const value = clone(location); + const legacyHostWithPath = typeof value.forward_host === "string" && value.forward_host.includes("/"); + const match_type = value.match_type || "prefix"; + if (!MATCH_TYPES.has(match_type)) + invalid("INVALID_LOCATION_MATCH_TYPE", "Location match_type is invalid", { index }); + let path_mode = value.path_mode || "preserve_uri"; + if (!PATH_MODES.has(path_mode)) invalid("INVALID_LOCATION_PATH_MODE", "Location path_mode is invalid", { index }); + let forward_host = value.forward_host; + let forward_path = value.forward_path; + const warnings = []; + if (legacyHostWithPath) { + const slash = value.forward_host.indexOf("/"); + forward_host = value.forward_host.slice(0, slash); + forward_path = value.forward_host.slice(slash) || "/"; + if (match_type === "prefix" && value.path?.startsWith("/") && value.path?.endsWith("/")) { + path_mode = "replace_prefix"; + } else { + warnings.push({ + severity: "warning", + code: "LEGACY_LOCATION_REQUIRES_REVIEW", + scope: "location", + path: value.path, + message: "Legacy upstream URI is retained in compatibility mode", + }); + } + } + const path = normalizePath(value.path, match_type); + if (!warnings.length) forward_host = normalizeUpstreamHost(forward_host); + normalizePort(value.forward_port, "forward_port"); + if (typeof value.forward_scheme !== "string" || !["http", "https"].includes(value.forward_scheme)) + invalid("INVALID_FORWARD_SCHEME", "forward_scheme must be http or https"); + if (["exact", "regex", "regex_i"].includes(match_type) && path_mode !== "preserve_uri") + invalid("INVALID_LOCATION_PATH_MODE", `${match_type} locations only support preserve_uri`); + if ( + ["strip_prefix", "replace_prefix"].includes(path_mode) && + (match_type === "regex" || + match_type === "regex_i" || + !path.startsWith("/") || + !path.endsWith("/") || + path === "/") + ) { + invalid( + "INVALID_LOCATION_URI_COMBINATION", + "strip/replace locations require a non-root slash-terminated prefix path", + ); + } + if (path_mode === "replace_prefix") { + assertNoControl(forward_path, "forward_path"); + if (!forward_path?.startsWith("/") || !forward_path.endsWith("/")) + invalid("INVALID_FORWARD_PATH", "replace_prefix requires a slash-terminated forward_path"); + } + return { + ...value, + forward_host, + forward_port: value.forward_port, + match_type, + path_mode, + ...(typeof forward_path === "string" ? { forward_path } : {}), + nginx_config: normalizeOptions(value.nginx_config, `locations[${index}].nginx_config`), + _normalization_warnings: warnings, + }; +}; + +const RESERVED_PORT_LISTENER_PORTS = new Set([80, 81, 443]); + +const normalizeListener = (listener) => { + if (typeof listener === "undefined" || listener === null) return { mode: "domain" }; + if (typeof listener !== "object" || Array.isArray(listener)) + invalid("INVALID_LISTENER", "nginx_config.listener must be an object"); + const mode = listener.mode || "domain"; + if (mode === "domain") return { mode }; + if (mode !== "port") invalid("INVALID_LISTENER_MODE", "nginx_config.listener.mode must be domain or port"); + const port = normalizePort(listener.port, "nginx_config.listener.port"); + if (RESERVED_PORT_LISTENER_PORTS.has(port)) + invalid("RESERVED_LISTENER_PORT", `nginx_config.listener.port ${port} is reserved by Nginx Proxy Manager`); + return { mode, port }; +}; + +export const normalizeProxyHost = (host) => { + if (!host || typeof host !== "object") invalid("INVALID_PROXY_HOST", "Proxy Host must be an object"); + const value = clone(host); + value.nginx_config = normalizeNginxConfig(value.nginx_config); + value.nginx_listener = normalizeListener(value.nginx_config.listener); + if (value.nginx_listener.mode === "domain" && (!Array.isArray(value.domain_names) || !value.domain_names.length)) + invalid("DOMAIN_LISTENER_REQUIRES_DOMAINS", "A domain listener requires at least one domain_name"); + if (value.nginx_listener.mode === "port") { + if (Array.isArray(value.domain_names) && value.domain_names.length) + invalid("PORT_LISTENER_REQUIRES_NO_DOMAINS", "A port listener cannot include domain_names"); + if ( + Number(value.certificate_id || 0) > 0 || + value.ssl_forced || + value.http2_support || + value.hsts_enabled || + value.hsts_subdomains + ) + invalid( + "PORT_LISTENER_TLS_UNSUPPORTED", + "A port listener currently supports HTTP only; remove the TLS settings", + ); + } + value.nginx_options = normalizeOptions( + value.nginx_config.server || value.nginx_config.options, + "nginx_config.server", + ); + value.locations = (value.locations || []).map(normalizeLocation); + return value; +}; + +export default { + normalizeNginxConfig, + normalizeProxyHost, + normalizeLocation, + normalizeUpstreamHost, + normalizeDuration, + normalizeSize, + normalizePort, + normalizeHeaderName, + normalizeHeaderValue, +}; diff --git a/backend/internal/nginx-config-renderer.js b/backend/internal/nginx-config-renderer.js new file mode 100644 index 0000000000..609ec4f047 --- /dev/null +++ b/backend/internal/nginx-config-renderer.js @@ -0,0 +1,356 @@ +import fs from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import errs from "../lib/error.js"; +import utils from "../lib/utils.js"; +import { hasDiagnosticErrors, scanAdvancedConfig } from "./nginx-config-diagnostics.js"; +import { hashCanonical, hashFileManifest, sha256 } from "./nginx-config-hash.js"; +import { normalizeProxyHost } from "./nginx-config-normalizer.js"; +import { buildSnapshot, getHostAdapter } from "./nginx-host-adapters.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const templatesDir = join(__dirname, "../templates"); +const TEMPLATE_VERSION = "nginx-config-renderer-v2"; +const PROTECTED_HEADERS = new Set(["host", "x-forwarded-scheme", "x-forwarded-proto", "x-forwarded-for", "x-real-ip"]); + +const quote = (value) => `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$")}"`; +const directiveBoolean = (value) => (value ? "on" : "off"); +const asLf = (value) => `${String(value).replace(/\r\n?/g, "\n").replace(/\n*$/, "")}\n`; + +const renderPortOnlyListener = (config, listener, ipv6) => { + const start = config.indexOf(" listen 80;"); + const end = config.indexOf("http2 off;", start); + if (start < 0 || end < 0) throw new errs.UnprocessableConfigError("Unable to render the port-only listener"); + const port = listener.port; + const listen = [ + ` listen ${port} default_server;`, + ipv6 ? `listen [::]:${port} default_server;` : `#listen [::]:${port} default_server;`, + "", + "", + " server_name _;", + "http2 off;", + ].join("\n"); + return `${config.slice(0, start)}${listen}${config.slice(end + "http2 off;".length)}`; +}; + +const readTemplateManifest = async (template) => { + const files = [ + template, + "_location.conf", + "_access.conf", + "_assets.conf", + "_certificates.conf", + "_exploits.conf", + "_forced_ssl.conf", + "_hsts.conf", + "_listen.conf", + ]; + const entries = []; + for (const name of files) { + try { + entries.push({ path: name, content: await fs.readFile(join(templatesDir, name)) }); + } catch (error) { + if (name === template) throw error; + } + } + return hashFileManifest(entries); +}; + +const renderRequestHeaders = (options, websocket, useProxyIncludeDefaults = false) => { + const values = new Map([ + ["host", { name: "Host", value: "$host", variable: true, protected: true }], + [ + "x-forwarded-scheme", + { + name: "X-Forwarded-Scheme", + value: useProxyIncludeDefaults ? "$x_forwarded_scheme" : "$scheme", + variable: true, + protected: true, + }, + ], + [ + "x-forwarded-proto", + { + name: "X-Forwarded-Proto", + value: useProxyIncludeDefaults ? "$x_forwarded_proto" : "$scheme", + variable: true, + protected: true, + }, + ], + [ + "x-forwarded-for", + { + name: "X-Forwarded-For", + value: useProxyIncludeDefaults ? "$proxy_add_x_forwarded_for" : "$remote_addr", + variable: true, + protected: true, + }, + ], + ["x-real-ip", { name: "X-Real-IP", value: "$remote_addr", variable: true, protected: true }], + ]); + if (websocket) { + values.set("upgrade", { name: "Upgrade", value: "$http_upgrade", variable: true }); + values.set("connection", { name: "Connection", value: "$http_connection", variable: true }); + } + for (const operation of options.request_headers || []) { + const key = operation.name.toLowerCase(); + if (operation.operation === "remove") { + if (PROTECTED_HEADERS.has(key)) + throw new errs.UnprocessableConfigError("A system proxy header cannot be removed", { + code: "PROTECTED_HEADER_REMOVE", + header: operation.name, + }); + values.set(key, { name: operation.name, value: "", remove: true }); + } else { + values.set(key, { + name: operation.name, + value: operation.value, + variable: operation.value_mode === "variable", + }); + } + } + return [...values.values()] + .sort((left, right) => left.name.toLowerCase().localeCompare(right.name.toLowerCase())) + .map( + (item) => + `proxy_set_header ${item.name} ${item.remove ? '""' : item.variable ? item.value : quote(item.value)};`, + ) + .join("\n"); +}; + +const renderOptions = (options = {}, websocket = false, useProxyIncludeDefaults = false) => { + const lines = []; + for (const field of [ + "client_max_body_size", + "proxy_connect_timeout", + "proxy_send_timeout", + "proxy_read_timeout", + "proxy_next_upstream_timeout", + "proxy_buffer_size", + "proxy_busy_buffers_size", + "proxy_max_temp_file_size", + "proxy_temp_file_write_size", + "proxy_limit_rate", + "proxy_headers_hash_bucket_size", + "proxy_headers_hash_max_size", + "proxy_next_upstream_tries", + "proxy_ssl_verify_depth", + ]) { + if (typeof options[field] !== "undefined") lines.push(`${field} ${options[field]};`); + } + for (const field of [ + "proxy_buffering", + "proxy_request_buffering", + "proxy_ignore_client_abort", + "proxy_intercept_errors", + "proxy_force_ranges", + "proxy_pass_request_body", + "proxy_pass_request_headers", + "proxy_pass_trailers", + "proxy_socket_keepalive", + "proxy_ssl_server_name", + "proxy_ssl_session_reuse", + "proxy_ssl_verify", + ]) { + if (typeof options[field] !== "undefined") lines.push(`${field} ${directiveBoolean(options[field])};`); + } + if (options.proxy_buffers) lines.push(`proxy_buffers ${options.proxy_buffers[0]} ${options.proxy_buffers[1]};`); + if (options.proxy_http_version) lines.push(`proxy_http_version ${options.proxy_http_version};`); + if (options.proxy_method) lines.push(`proxy_method ${options.proxy_method};`); + if (options.proxy_bind) lines.push(`proxy_bind ${options.proxy_bind};`); + if (options.proxy_next_upstream) lines.push(`proxy_next_upstream ${options.proxy_next_upstream.join(" ")};`); + if (options.proxy_redirect) lines.push(`proxy_redirect ${options.proxy_redirect};`); + if (options.proxy_ssl_name) lines.push(`proxy_ssl_name ${options.proxy_ssl_name};`); + if (options.proxy_ssl_protocols) lines.push(`proxy_ssl_protocols ${options.proxy_ssl_protocols.join(" ")};`); + if (options.proxy_ssl_ciphers) lines.push(`proxy_ssl_ciphers ${options.proxy_ssl_ciphers};`); + if (options.proxy_ssl_verify) lines.push("proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;"); + for (const item of options.proxy_cookie_domain || []) lines.push(`proxy_cookie_domain ${item.from} ${item.to};`); + for (const item of options.proxy_cookie_path || []) lines.push(`proxy_cookie_path ${item.from} ${item.to};`); + const requestHeaders = renderRequestHeaders(options, websocket, useProxyIncludeDefaults); + if (requestHeaders) lines.push(requestHeaders); + for (const item of options.response_headers || []) { + if (item.operation === "remove") continue; + if (item.operation === "add" || item.operation === "set") + lines.push( + `add_header ${item.name} ${item.value_mode === "variable" ? item.value : quote(item.value)} always;`, + ); + } + for (const header of options.hide_response_headers || []) lines.push(`proxy_hide_header ${header};`); + for (const header of options.proxy_pass_headers || []) lines.push(`proxy_pass_header ${header};`); + if (options.proxy_ignore_headers?.length) + lines.push(`proxy_ignore_headers ${options.proxy_ignore_headers.join(" ")};`); + return lines.join("\n"); +}; + +const renderLocation = (location, host) => { + const options = { ...host.nginx_options, ...location.nginx_config }; + const match = { + prefix: location.path, + priority_prefix: `^~ ${location.path}`, + exact: `= ${location.path}`, + regex: `~ ${quote(location.path)}`, + regex_i: `~* ${quote(location.path)}`, + }[location.match_type]; + const authority = `${location.forward_host}:${location.forward_port}`; + const uri = + location.path_mode === "preserve_uri" + ? "" + : location.path_mode === "strip_prefix" + ? "/" + : location.forward_path; + const proxyPass = `proxy_pass ${location.forward_scheme}://${authority}${uri};`; + const lines = [ + ` location ${match} {`, + location.advanced_config ? ` ${location.advanced_config.replace(/\n/g, "\n ")}` : "", + renderOptions(options, host.allow_websocket_upgrade, true) + .split("\n") + .map((line) => ` ${line}`) + .join("\n"), + " add_header X-Served-By $host;", + ` ${proxyPass}`, + " }", + ]; + return lines.filter(Boolean).join("\n"); +}; + +// API validation may coerce an optional foreign key such as certificate_id +// between its numeric and string representation. The resolved dependency is +// semantically the same, so its preview hash must be representation-independent. +const canonicalDependencyId = (value) => { + if (value === null || typeof value === "undefined") return null; + if (value === "new") return value; + const numeric = Number(value); + return Number.isSafeInteger(numeric) ? numeric : value; +}; + +const buildDependencyManifest = (host, dependencies) => ({ + certificate: dependencies.certificate + ? { + id: canonicalDependencyId(dependencies.certificate.id ?? host.certificate_id), + fullchain_hash: dependencies.certificate.fullchain_hash ?? null, + key_hash: dependencies.certificate.key_hash ?? null, + } + : { id: canonicalDependencyId(host.certificate_id) }, + access_list: dependencies.access_list + ? { + id: canonicalDependencyId(dependencies.access_list.id ?? host.access_list_id), + clients: dependencies.access_list.clients ?? [], + items: dependencies.access_list.items ?? [], + } + : { id: canonicalDependencyId(host.access_list_id) }, + includes: dependencies.includes ?? [], +}); + +/** + * Pure renderer. It reads immutable template files but never reads or writes an + * active nginx artifact, DB record, or deployment state. + */ +export const buildProxyHostCandidate = async ({ host, dependencies = {}, capability = {} }) => { + const normalized = normalizeProxyHost(host); + const diagnostics = [ + ...scanAdvancedConfig(normalized.advanced_config), + ...normalized.locations.flatMap((location) => [ + ...location._normalization_warnings, + ...scanAdvancedConfig(location.advanced_config).map((item) => ({ + ...item, + scope: "location", + path: location.path, + })), + ]), + ]; + if (hasDiagnosticErrors(diagnostics)) + throw new errs.UnprocessableConfigError("Advanced nginx config conflicts with managed configuration", { + diagnostics, + }); + const templateHash = await readTemplateManifest("proxy_host.conf"); + const dependencyManifest = buildDependencyManifest(normalized, dependencies); + const dependencyHash = hashCanonical(dependencyManifest); + const capabilityHash = hashCanonical(capability); + const payload = { + id: normalized.id ?? null, + enabled: Boolean(normalized.enabled), + domain_names: normalized.domain_names ?? [], + forward_scheme: normalized.forward_scheme, + forward_host: normalized.forward_host, + forward_port: normalized.forward_port, + locations: normalized.locations.map(({ _normalization_warnings, ...location }) => location), + advanced_config: normalized.advanced_config ?? "", + nginx_config: normalized.nginx_config, + ssl_forced: Boolean(normalized.ssl_forced), + caching_enabled: Boolean(normalized.caching_enabled), + block_exploits: Boolean(normalized.block_exploits), + allow_websocket_upgrade: Boolean(normalized.allow_websocket_upgrade), + http2_support: Boolean(normalized.http2_support), + hsts_enabled: Boolean(normalized.hsts_enabled), + hsts_subdomains: Boolean(normalized.hsts_subdomains), + trust_forwarded_proto: Boolean(normalized.trust_forwarded_proto), + dependencies: dependencyManifest, + }; + const payloadHash = hashCanonical(payload); + const renderEngine = utils.getRenderEngine(); + const template = await fs.readFile(join(templatesDir, "proxy_host.conf"), "utf8"); + const defaultLocationEnabled = normalized.nginx_options.default_location_enabled !== false; + const useDefaultLocation = + defaultLocationEnabled && + !normalized.locations.some((location) => location.path === "/" && location.match_type === "prefix") && + !/^(?:.*;)?\s*?location\s*?\/\s*?{/im.test(normalized.advanced_config || ""); + const renderContext = { + ...normalized, + certificate: dependencies.certificate ?? normalized.certificate, + access_list: dependencies.access_list ?? normalized.access_list, + ipv6: typeof capability.ipv6 === "boolean" ? capability.ipv6 : true, + use_default_location: useDefaultLocation, + locations: normalized.locations.map((location) => renderLocation(location, normalized)).join("\n\n"), + nginx_options: renderOptions(normalized.nginx_options, normalized.allow_websocket_upgrade), + managed_nginx_location_options: renderOptions( + normalized.nginx_options, + normalized.allow_websocket_upgrade, + true, + ), + // proxy.conf carries its own proxy_set_header defaults. When a user manages + // request headers, render proxy_pass directly so the upstream receives one + // authoritative value per header rather than duplicate Host headers. + use_managed_request_headers: Boolean(normalized.nginx_options.request_headers?.length), + }; + const rendered = await renderEngine.parseAndRender(template, renderContext); + const config = asLf( + normalized.nginx_listener.mode === "port" + ? renderPortOnlyListener(rendered, normalized.nginx_listener, renderContext.ipv6) + : rendered, + ); + const configHash = sha256(Buffer.from(config, "utf8")); + const partial = { + config, + configHash, + payloadHash, + dependencyHash, + templateVersion: TEMPLATE_VERSION, + templateHash, + capabilityHash, + diagnostics, + sourceMap: [], + }; + return Object.freeze({ ...partial, snapshot: buildSnapshot("proxy_host", normalized, partial) }); +}; + +export const buildCandidate = async ({ hostType, host, ...input }) => { + const adapter = getHostAdapter(hostType); + if (hostType === "proxy_host") return buildProxyHostCandidate({ host, ...input }); + const template = await fs.readFile(join(templatesDir, adapter.template), "utf8"); + const engine = utils.getRenderEngine(); + const config = asLf(await engine.parseAndRender(template, structuredClone(host))); + const partial = { + config, + configHash: sha256(Buffer.from(config)), + payloadHash: hashCanonical(host), + dependencyHash: hashCanonical(input.dependencies ?? {}), + templateVersion: TEMPLATE_VERSION, + templateHash: await readTemplateManifest(adapter.template), + capabilityHash: hashCanonical(input.capability ?? {}), + diagnostics: [], + sourceMap: [], + }; + return Object.freeze({ ...partial, snapshot: buildSnapshot(hostType, host, partial) }); +}; + +export default { buildCandidate, buildProxyHostCandidate }; diff --git a/backend/internal/nginx-config-validator.js b/backend/internal/nginx-config-validator.js new file mode 100644 index 0000000000..5118f4e9fe --- /dev/null +++ b/backend/internal/nginx-config-validator.js @@ -0,0 +1,75 @@ +import { randomUUID } from "node:crypto"; +import fs from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import utils from "../lib/utils.js"; +import { deploymentRoot } from "./nginx-config-artifacts.js"; + +const cleanOutput = (output, mirror) => + String(output || "") + .replaceAll(mirror, "/data/nginx") + .replace(/\/data\/nginx\/.deploy\/validation\/[^/]+/g, "/data/nginx/.deploy/validation/"); + +/** + * Builds a read-only mirror of the managed nginx directory and rewrites the + * master configuration to point its managed includes at that mirror. No active + * artifact is touched during preview validation. + */ +export const validateInMirror = async ({ + nginxRoot = "/data/nginx", + nginxConfigPath = "/etc/nginx/nginx.conf", + nginxPrefix = "/etc/nginx", + operationId = randomUUID(), + candidatePath, + targetPath, + commandRunner = utils.execFileResult, +}) => { + const root = deploymentRoot(nginxRoot); + // fs.cp rejects copying a directory into any of its own descendants before + // its filter callback runs. Keep the validation tree beside nginxRoot rather + // than under nginxRoot/.deploy so the mirror can be created safely. + const validationId = String(operationId) + .replace(/[^a-zA-Z0-9_-]/g, "-") + .slice(0, 64); + const validationRoot = await fs.mkdtemp(join(dirname(resolve(nginxRoot)), `.nginx-validation-${validationId}-`)); + const mirror = join(validationRoot, "nginx"); + const master = join(validationRoot, "nginx.conf"); + try { + await fs.cp(nginxRoot, mirror, { recursive: true, filter: (source) => !source.includes(root) }); + // Included files such as /etc/nginx/conf.d/default.conf may themselves use + // paths relative to the main config directory. Mirror conf.d beside the + // temporary main config so those includes resolve without touching them. + await fs.cp(join(nginxPrefix, "conf.d"), join(validationRoot, "conf.d"), { recursive: true }); + const replacement = join(mirror, targetPath.slice(nginxRoot.length).replace(/^[/\\]/, "")); + await fs.mkdir(join(replacement, ".."), { recursive: true }); + await fs.copyFile(candidatePath, replacement); + const sourceMaster = await fs.readFile(nginxConfigPath, "utf8"); + await fs.writeFile(master, sourceMaster.replaceAll(nginxRoot, mirror.replace(/\\/g, "/")), "utf8"); + const result = await commandRunner("/usr/sbin/nginx", [ + "-t", + "-c", + master, + "-p", + nginxPrefix, + "-g", + "error_log /dev/null crit;", + ]); + return { + valid: true, + stdout: cleanOutput(result.stdout, mirror), + stderr: cleanOutput(result.stderr, mirror), + validation_scope: "full", + }; + } catch (error) { + return { + valid: false, + stdout: cleanOutput(error.stdout, mirror), + stderr: cleanOutput(error.stderr || error.message, mirror), + validation_scope: "full", + error, + }; + } finally { + await fs.rm(validationRoot, { recursive: true, force: true }).catch(() => undefined); + } +}; + +export default { validateInMirror }; diff --git a/backend/internal/nginx-deployment-coordinator.js b/backend/internal/nginx-deployment-coordinator.js new file mode 100644 index 0000000000..288be9ba97 --- /dev/null +++ b/backend/internal/nginx-deployment-coordinator.js @@ -0,0 +1,471 @@ +import { randomUUID } from "node:crypto"; +import fs from "node:fs/promises"; +import { join } from "node:path"; +import utils from "../lib/utils.js"; +import { + activeArtifactPath, + atomicWrite, + backupArtifactPath, + candidateArtifactPath, + deleteJournal, + deploymentRoot, + readArtifact, + readJournals, + removeArtifact, + stagingArtifactPath, + writeJournal, +} from "./nginx-config-artifacts.js"; +import { sha256 } from "./nginx-config-hash.js"; +import { buildCandidate } from "./nginx-config-renderer.js"; +import { validateInMirror } from "./nginx-config-validator.js"; + +const terminalStates = new Set(["applied", "failed", "rolled_back", "recovered_rollback"]); +const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)); +const now = () => new Date().toISOString(); + +export const deriveDeploymentStatus = ({ + is_deleted, + enabled, + nginx_applied_enabled, + nginx_config_revision, + nginx_applied_revision, + nginx_applied_hash, + active_hash, + deployment_state, + has_unfinished_journal, +}) => { + if (has_unfinished_journal) return "recovering"; + if (["queued", "preparing", "validating", "swapping", "reloading"].includes(deployment_state)) return "pending"; + if (is_deleted) return "deleted"; + if (!enabled && !nginx_applied_enabled && nginx_applied_revision === nginx_config_revision) return "disabled"; + if ( + enabled && + nginx_applied_enabled && + nginx_applied_revision === nginx_config_revision && + nginx_applied_hash && + nginx_applied_hash === active_hash + ) + return "online"; + if (nginx_applied_enabled && nginx_applied_hash) return "degraded"; + if (enabled && !nginx_applied_enabled) return "error"; + return "degraded"; +}; + +export class NginxDeploymentCoordinator { + #queue = Promise.resolve(); + + constructor({ + nginxRoot = "/data/nginx", + nginxConfigPath = "/etc/nginx/nginx.conf", + nginxPrefix = "/etc/nginx", + commandRunner = utils.execFileResult, + renderCandidate = buildCandidate, + validator = validateInMirror, + lockTimeoutMs = 30000, + staleLockMs = 120000, + } = {}) { + this.nginxRoot = nginxRoot; + this.nginxConfigPath = nginxConfigPath; + this.nginxPrefix = nginxPrefix; + this.commandRunner = commandRunner; + this.renderCandidate = renderCandidate; + this.validator = validator; + this.lockTimeoutMs = lockTimeoutMs; + this.staleLockMs = staleLockMs; + } + + get lockPath() { + return join(deploymentRoot(this.nginxRoot), "lock.json"); + } + + async #acquireLock(operationId) { + const startedAt = Date.now(); + await fs.mkdir(deploymentRoot(this.nginxRoot), { recursive: true }); + while (Date.now() - startedAt < this.lockTimeoutMs) { + try { + await fs.writeFile( + this.lockPath, + JSON.stringify({ operation_id: operationId, pid: process.pid, started_on: now() }), + { flag: "wx", mode: 0o600 }, + ); + return async () => fs.rm(this.lockPath, { force: true }); + } catch (error) { + if (error.code !== "EEXIST") throw error; + try { + const current = JSON.parse(await fs.readFile(this.lockPath, "utf8")); + if (Date.now() - Date.parse(current.started_on) > this.staleLockMs) { + await fs.rm(this.lockPath, { force: true }); + continue; + } + } catch (readError) { + if (readError.code === "ENOENT") continue; + } + await sleep(50); + } + } + const error = new Error("Timed out waiting for nginx deployment lock"); + error.code = "DEPLOYMENT_LOCK_TIMEOUT"; + throw error; + } + + enqueue(operation) { + const scheduled = this.#queue.then(() => operation()); + this.#queue = scheduled.catch(() => undefined); + return scheduled; + } + + async #nginxTest() { + return this.commandRunner("/usr/sbin/nginx", ["-t", "-g", "error_log /dev/null crit;"]); + } + + async #reload() { + await this.#nginxTest(); + return this.commandRunner("/usr/sbin/nginx", ["-s", "reload"]); + } + + /** Runs a global nginx syntax test under the same serialized lock. */ + testOnly(operation = "test") { + return this.enqueue(async () => { + const operationId = randomUUID(); + const release = await this.#acquireLock(`${operation}-${operationId}`); + try { + return await this.#nginxTest(); + } finally { + await release().catch(() => undefined); + } + }); + } + + /** Reloads nginx only after the global syntax test, serialized with deploys. */ + reloadOnly(operation = "reload") { + return this.enqueue(async () => { + const operationId = randomUUID(); + const release = await this.#acquireLock(`${operation}-${operationId}`); + try { + return await this.#reload(); + } finally { + await release().catch(() => undefined); + } + }); + } + + /** + * Deploy exactly one managed artifact. All callbacks execute in documented + * phases so service code can persist desired/applied state without the + * coordinator owning any business model. + */ + deploy({ + hostType, + host, + dependencies = {}, + capability = {}, + operation = "deploy", + renderResult = null, + beforeCommit = async () => undefined, + commitApplied = async () => undefined, + commitFailure = async () => undefined, + deploymentStore = null, + }) { + return this.enqueue(async () => { + const operationId = randomUUID(); + const target = activeArtifactPath(hostType, host.id, this.nginxRoot); + const candidate = candidateArtifactPath(hostType, host.id, operationId, this.nginxRoot); + const staging = stagingArtifactPath(hostType, host.id, operationId, this.nginxRoot); + const backup = backupArtifactPath(hostType, host.id, operationId, this.nginxRoot); + let journal = { + operation_id: operationId, + host_type: hostType, + host_id: host.id, + operation, + phase: "queued", + started_on: now(), + target, + candidate, + staging, + backup, + had_active: false, + }; + let release; + let result; + try { + if (deploymentStore) + await deploymentStore.create?.({ + operation_id: operationId, + host_type: hostType, + host_id: host.id, + operation, + state: "queued", + requested_revision: host.nginx_config_revision ?? null, + started_on: now(), + }); + release = await this.#acquireLock(operationId); + journal.phase = "preparing"; + await writeJournal(journal, this.nginxRoot); + const rendered = + renderResult || (await this.renderCandidate({ hostType, host, dependencies, capability })); + await atomicWrite(candidate, rendered.config); + if (deploymentStore) await deploymentStore.setCandidatePath?.(operationId, candidate); + await atomicWrite(staging, rendered.config); + journal = { + ...journal, + phase: "validating", + config_hash: rendered.configHash, + candidate_hash: sha256(Buffer.from(rendered.config)), + diagnostics: rendered.diagnostics, + }; + await writeJournal(journal, this.nginxRoot); + if (deploymentStore) await deploymentStore.transition?.(operationId, "validating", rendered); + const isolated = await this.validator({ + nginxRoot: this.nginxRoot, + nginxConfigPath: this.nginxConfigPath, + nginxPrefix: this.nginxPrefix, + operationId, + candidatePath: candidate, + targetPath: target, + commandRunner: this.commandRunner, + }); + if (!isolated.valid) { + const error = new Error(isolated.stderr || "Isolated nginx validation failed"); + error.code = "NGINX_VALIDATION_FAILED"; + error.diagnostics = [ + { severity: "error", code: "nginx_test_failed", scope: "candidate", message: isolated.stderr }, + ]; + throw error; + } + await beforeCommit({ operationId, rendered, isolated }); + journal.phase = "swapping"; + journal.had_active = (await readArtifact(target)) !== null; + await writeJournal(journal, this.nginxRoot); + if (journal.had_active) { + await fs.mkdir(join(backup, ".."), { recursive: true }); + await fs.rename(target, backup); + } + await fs.mkdir(join(target, ".."), { recursive: true }); + await fs.rename(staging, target); + journal.phase = "swapped"; + await writeJournal(journal, this.nginxRoot); + if (deploymentStore) await deploymentStore.transition?.(operationId, "reloading", rendered); + await this.#reload(); + journal.phase = "reloaded"; + journal.reloaded_on = now(); + await writeJournal(journal, this.nginxRoot); + await commitApplied({ operationId, rendered, isolated }); + journal.phase = "applied"; + journal.finished_on = now(); + await writeJournal(journal, this.nginxRoot); + if (deploymentStore) await deploymentStore.transition?.(operationId, "applied", rendered); + await deleteJournal(operationId, this.nginxRoot); + await fs.rm(join(deploymentRoot(this.nginxRoot), "staging", operationId), { + recursive: true, + force: true, + }); + result = { operationId, state: "applied", rendered, isolated }; + } catch (error) { + let rolledBack = false; + try { + if (journal.phase === "swapped" || journal.phase === "reloaded") { + await removeArtifact(target); + if (journal.had_active) { + await fs.mkdir(join(target, ".."), { recursive: true }); + await fs.rename(backup, target); + } + await this.#reload(); + rolledBack = true; + } + } catch (rollbackError) { + error.rollback_error = rollbackError.message; + } + journal.phase = rolledBack ? "rolled_back" : "failed"; + journal.finished_on = now(); + journal.error = { + code: error.code || "DEPLOYMENT_FAILED", + message: error.message, + rollback_error: error.rollback_error, + }; + await writeJournal(journal, this.nginxRoot).catch(() => undefined); + await commitFailure({ operationId, error, journal, rolledBack }).catch((commitError) => { + error.commit_error = commitError.message; + }); + if (deploymentStore) + await deploymentStore + .transition?.(operationId, rolledBack ? "rolled_back" : "failed", null, journal.error) + .catch(() => undefined); + throw error; + } finally { + if (release) await release().catch(() => undefined); + } + return result; + }); + } + + /** + * Removes one managed artifact with the same validate/swap/reload/rollback + * guarantees as deploy(). A zero-byte candidate is validated in the isolated + * mirror, so the active include is never removed before Nginx accepts the + * resulting configuration. + */ + remove({ + hostType, + host, + operation = "remove", + beforeCommit = async () => undefined, + commitApplied = async () => undefined, + commitFailure = async () => undefined, + deploymentStore = null, + }) { + return this.enqueue(async () => { + const operationId = randomUUID(); + const target = activeArtifactPath(hostType, host.id, this.nginxRoot); + const candidate = candidateArtifactPath(hostType, host.id, operationId, this.nginxRoot); + const backup = backupArtifactPath(hostType, host.id, operationId, this.nginxRoot); + const journal = { + operation_id: operationId, + host_type: hostType, + host_id: host.id, + operation, + phase: "queued", + started_on: now(), + target, + candidate, + backup, + had_active: false, + }; + let release; + try { + if (deploymentStore) + await deploymentStore.create?.({ + operation_id: operationId, + host_type: hostType, + host_id: host.id, + operation, + state: "queued", + requested_revision: host.nginx_config_revision ?? null, + started_on: now(), + }); + release = await this.#acquireLock(operationId); + journal.phase = "preparing"; + await writeJournal(journal, this.nginxRoot); + journal.had_active = (await readArtifact(target)) !== null; + if (!journal.had_active) { + await beforeCommit({ + operationId, + rendered: null, + isolated: { valid: true, validation_scope: "not_applicable" }, + }); + await commitApplied({ + operationId, + rendered: null, + isolated: { valid: true, validation_scope: "not_applicable" }, + }); + journal.phase = "applied"; + journal.finished_on = now(); + await writeJournal(journal, this.nginxRoot); + if (deploymentStore) await deploymentStore.transition?.(operationId, "applied", null); + await deleteJournal(operationId, this.nginxRoot); + return { + operationId, + state: "applied", + rendered: null, + isolated: { valid: true, validation_scope: "not_applicable" }, + }; + } + await atomicWrite(candidate, ""); + if (deploymentStore) await deploymentStore.setCandidatePath?.(operationId, candidate); + journal.phase = "validating"; + await writeJournal(journal, this.nginxRoot); + if (deploymentStore) await deploymentStore.transition?.(operationId, "validating", null); + const isolated = await this.validator({ + nginxRoot: this.nginxRoot, + nginxConfigPath: this.nginxConfigPath, + nginxPrefix: this.nginxPrefix, + operationId, + candidatePath: candidate, + targetPath: target, + commandRunner: this.commandRunner, + }); + if (!isolated.valid) { + const error = new Error(isolated.stderr || "Isolated nginx validation failed"); + error.code = "NGINX_VALIDATION_FAILED"; + throw error; + } + await beforeCommit({ operationId, rendered: null, isolated }); + journal.phase = "swapping"; + await writeJournal(journal, this.nginxRoot); + await fs.mkdir(join(backup, ".."), { recursive: true }); + await fs.rename(target, backup); + journal.phase = "swapped"; + await writeJournal(journal, this.nginxRoot); + if (deploymentStore) await deploymentStore.transition?.(operationId, "reloading", null); + await this.#reload(); + journal.phase = "reloaded"; + await writeJournal(journal, this.nginxRoot); + await commitApplied({ operationId, rendered: null, isolated }); + journal.phase = "applied"; + journal.finished_on = now(); + await writeJournal(journal, this.nginxRoot); + if (deploymentStore) await deploymentStore.transition?.(operationId, "applied", null); + await deleteJournal(operationId, this.nginxRoot); + return { operationId, state: "applied", rendered: null, isolated }; + } catch (error) { + let rolledBack = false; + try { + if (["swapped", "reloaded"].includes(journal.phase) && journal.had_active) { + await fs.mkdir(join(target, ".."), { recursive: true }); + await fs.rename(backup, target); + await this.#reload(); + rolledBack = true; + } + } catch (rollbackError) { + error.rollback_error = rollbackError.message; + } + journal.phase = rolledBack ? "rolled_back" : "failed"; + journal.finished_on = now(); + journal.error = { + code: error.code || "DEPLOYMENT_FAILED", + message: error.message, + rollback_error: error.rollback_error, + }; + await writeJournal(journal, this.nginxRoot).catch(() => undefined); + await commitFailure({ operationId, error, journal, rolledBack }).catch((commitError) => { + error.commit_error = commitError.message; + }); + if (deploymentStore) + await deploymentStore + .transition?.(operationId, rolledBack ? "rolled_back" : "failed", null, journal.error) + .catch(() => undefined); + throw error; + } finally { + if (release) await release().catch(() => undefined); + } + }); + } + + async recover() { + const journals = await readJournals(this.nginxRoot); + const recovered = []; + for (const journal of journals) { + if (terminalStates.has(journal.phase)) continue; + const release = await this.#acquireLock(`recover-${journal.operation_id}`); + try { + if (["swapped", "reloaded"].includes(journal.phase)) { + await removeArtifact(journal.target); + if (journal.had_active) { + await fs.mkdir(join(journal.target, ".."), { recursive: true }); + await fs.rename(journal.backup, journal.target); + } + await this.#reload(); + } + journal.phase = "recovered_rollback"; + journal.finished_on = now(); + await writeJournal(journal, this.nginxRoot); + recovered.push({ operationId: journal.operation_id, state: journal.phase }); + } finally { + await release(); + } + } + return recovered; + } +} + +export const nginxDeploymentCoordinator = new NginxDeploymentCoordinator(); +export default nginxDeploymentCoordinator; diff --git a/backend/internal/nginx-deployment-store.js b/backend/internal/nginx-deployment-store.js new file mode 100644 index 0000000000..33aa33b8ea --- /dev/null +++ b/backend/internal/nginx-deployment-store.js @@ -0,0 +1,56 @@ +import nginxDeploymentModel from "../models/nginx_deployment.js"; +import databaseNow from "../models/now_helper.js"; +import { toLogicalPath } from "./nginx-config-artifacts.js"; + +/** + * Small persistence adapter deliberately kept out of the deployment + * coordinator. The coordinator can therefore be exercised without a database, + * while production receives a durable operation timeline for diagnostics and + * startup reconciliation. + */ +export const createDeploymentStore = ({ + nginxRoot = "/data/nginx", + ownerUserId = null, + parentOperationId = null, +} = {}) => ({ + create: async (deployment) => { + // Coordinator journals use ISO-8601 timestamps, but MariaDB DATETIME does + // not accept values such as `2026-08-01T13:57:13.098Z` in strict mode. + // Keep database timestamps database-native for both SQLite and MariaDB. + const timestamp = databaseNow(); + await nginxDeploymentModel.query().insert({ + ...deployment, + parent_operation_id: parentOperationId, + owner_user_id: ownerUserId, + started_on: timestamp, + created_on: timestamp, + modified_on: timestamp, + }); + }, + transition: async (operationId, state, rendered = null, error = null) => { + const patch = { + state, + }; + if (rendered) { + patch.payload_hash = rendered.payloadHash; + patch.dependency_hash = rendered.dependencyHash; + patch.template_version = rendered.templateVersion; + patch.template_hash = rendered.templateHash; + patch.capability_hash = rendered.capabilityHash; + patch.config_hash = rendered.configHash; + patch.diagnostics = rendered.diagnostics; + } + if (error) patch.journal_summary = { error }; + if (["applied", "failed", "rolled_back", "recovered_rollback"].includes(state)) + patch.finished_on = databaseNow(); + await nginxDeploymentModel.query().where("operation_id", operationId).patch(patch); + }, + setCandidatePath: async (operationId, path) => { + await nginxDeploymentModel + .query() + .where("operation_id", operationId) + .patch({ candidate_path: toLogicalPath(path, nginxRoot) }); + }, +}); + +export default { createDeploymentStore }; diff --git a/backend/internal/nginx-host-adapters.js b/backend/internal/nginx-host-adapters.js new file mode 100644 index 0000000000..732e04f3cb --- /dev/null +++ b/backend/internal/nginx-host-adapters.js @@ -0,0 +1,43 @@ +import { join } from "node:path"; +import errs from "../lib/error.js"; + +const definitions = { + proxy_host: { + directory: "proxy_host", + template: "proxy_host.conf", + expand: ["certificate", "owner", "access_list.[clients,items]"], + }, + redirection_host: { directory: "redirection_host", template: "redirection_host.conf", expand: ["certificate", "owner", "access_list"] }, + dead_host: { directory: "dead_host", template: "dead_host.conf", expand: ["certificate", "owner", "access_list"] }, + stream: { directory: "stream", template: "stream.conf", expand: ["certificate", "owner"] }, + default: { directory: "default_host", template: "default.conf", expand: ["certificate"] }, +}; + +export const getHostAdapter = (hostType) => { + const adapter = definitions[hostType]; + if (!adapter) throw new errs.AssertionFailedError(`Unsupported nginx host type: ${hostType}`); + return { hostType, ...adapter }; +}; + +export const getActivePath = (hostType, hostId, root = "/data/nginx") => { + const adapter = getHostAdapter(hostType); + if (hostType === "default") return join(root, adapter.directory, "site.conf"); + if (!Number.isInteger(Number(hostId)) || Number(hostId) < 1) throw new errs.AssertionFailedError("A numeric host id is required"); + return join(root, adapter.directory, `${Number(hostId)}.conf`); +}; + +export const buildSnapshot = (hostType, host, renderResult) => ({ + schema_version: 1, + host_type: hostType, + host_id: host.id ?? null, + desired: structuredClone(host), + payload_hash: renderResult.payloadHash, + dependency_hash: renderResult.dependencyHash, + template_version: renderResult.templateVersion, + template_hash: renderResult.templateHash, + capability_hash: renderResult.capabilityHash, + config_hash: renderResult.configHash, +}); + +export const hostAdapters = Object.freeze(definitions); +export default { hostAdapters, getHostAdapter, getActivePath, buildSnapshot }; diff --git a/backend/internal/nginx-preview-token.js b/backend/internal/nginx-preview-token.js new file mode 100644 index 0000000000..b58bc2b1f0 --- /dev/null +++ b/backend/internal/nginx-preview-token.js @@ -0,0 +1,27 @@ +import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; + +const secret = process.env.NPM_NGINX_PREVIEW_TOKEN_SECRET || process.env.SECRET_KEY || randomBytes(32).toString("base64url"); +const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url"); +const signature = (value) => createHmac("sha256", secret).update(value).digest("base64url"); + +export const issuePreviewToken = ({ hostId = null, baseRevision = null, payloadHash, dependencyHash, templateHash, capabilityHash, expiresInSeconds = 300 }) => { + const body = encode({ host_id: hostId, base_revision: baseRevision, payload_hash: payloadHash, dependency_hash: dependencyHash, template_hash: templateHash, capability_hash: capabilityHash, expires_at: Math.floor(Date.now() / 1000) + expiresInSeconds }); + return `${body}.${signature(body)}`; +}; + +export const verifyPreviewToken = (token, expected = {}) => { + if (typeof token !== "string" || !token.includes(".")) return { valid: false, reason: "missing" }; + const [body, received] = token.split("."); + const actual = signature(body); + if (received.length !== actual.length || !timingSafeEqual(Buffer.from(received), Buffer.from(actual))) return { valid: false, reason: "signature" }; + try { + const data = JSON.parse(Buffer.from(body, "base64url").toString("utf8")); + if (data.expires_at < Math.floor(Date.now() / 1000)) return { valid: false, reason: "expired" }; + for (const [key, value] of Object.entries(expected)) if (value !== undefined && data[key] !== value) return { valid: false, reason: key }; + return { valid: true, data }; + } catch { + return { valid: false, reason: "malformed" }; + } +}; + +export default { issuePreviewToken, verifyPreviewToken }; diff --git a/backend/internal/nginx.js b/backend/internal/nginx.js index fe84607f96..babdf60132 100644 --- a/backend/internal/nginx.js +++ b/backend/internal/nginx.js @@ -1,437 +1,93 @@ -import fs from "node:fs"; -import { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; import _ from "lodash"; -import errs from "../lib/error.js"; -import utils from "../lib/utils.js"; -import { debug, nginx as logger } from "../logger.js"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - +import { nginx as logger } from "../logger.js"; +import databaseNow from "../models/now_helper.js"; +import { activeArtifactPath, readArtifact } from "./nginx-config-artifacts.js"; +import nginxDeploymentCoordinator from "./nginx-deployment-coordinator.js"; +import { createDeploymentStore } from "./nginx-deployment-store.js"; + +const deploymentStoreFor = (host) => createDeploymentStore({ ownerUserId: host?.owner_user_id ?? null }); + +/** + * Compatibility facade for existing services. The legacy public API is kept so + * callers can migrate independently, but every active-file mutation and every + * reload now goes through NginxDeploymentCoordinator. + */ const internalNginx = { - /** - * This will: - * - test the nginx config first to make sure it's OK - * - create / recreate the config for the host - * - test again - * - IF OK: update the meta with online status - * - IF BAD: update the meta with offline status and remove the config entirely - * - then reload nginx - * - * @param {Object|String} model - * @param {String} host_type - * @param {Object} host - * @returns {Promise} - */ - configure: (model, host_type, host) => { - let combined_meta = {}; - - return internalNginx - .test() - .then(() => { - // Nginx is OK - // We're deleting this config regardless. - // Don't throw errors, as the file may not exist at all - // Delete the .err file too - return internalNginx.deleteConfig(host_type, host, false, true); - }) - .then(() => { - return internalNginx.generateConfig(host_type, host); - }) - .then(() => { - // Test nginx again and update meta with result - return internalNginx - .test() - .then(() => { - // nginx is ok - combined_meta = _.assign({}, host.meta, { - nginx_online: true, - nginx_err: null, - }); - - return model.query().where("id", host.id).patch({ - meta: combined_meta, - }); - }) - .catch((err) => { - // Remove the error_log line because it's a docker-ism false positive that doesn't need to be reported. - // It will always look like this: - // nginx: [alert] could not open error log file: open() "/var/log/nginx/error.log" failed (6: No such device or address) - - const valid_lines = []; - const err_lines = err.message.split("\n"); - err_lines.map((line) => { - if (line.indexOf("/var/log/nginx/error.log") === -1) { - valid_lines.push(line); - } - return true; - }); - - debug(logger, "Nginx test failed:", valid_lines.join("\n")); - - // config is bad, update meta and delete config - combined_meta = _.assign({}, host.meta, { - nginx_online: false, - nginx_err: valid_lines.join("\n"), - }); - - return model - .query() - .where("id", host.id) - .patch({ - meta: combined_meta, - }) - .then(() => { - internalNginx.renameConfigAsError(host_type, host); - }) - .then(() => { - return internalNginx.deleteConfig(host_type, host, true); - }); + configure: async (model, hostType, host) => { + const priorMeta = host.meta || {}; + let appliedMeta = priorMeta; + await nginxDeploymentCoordinator.deploy({ + hostType, + host, + dependencies: { certificate: host.certificate, access_list: host.access_list }, + operation: `${hostType}_configure`, + deploymentStore: deploymentStoreFor(host), + commitApplied: async ({ rendered }) => { + appliedMeta = _.assign({}, priorMeta, { nginx_online: true, nginx_err: null }); + if (model?.query && "meta" in host) + await model.query().where("id", host.id).patch({ meta: appliedMeta }); + // Proxy Host normal workflows use their dedicated service callbacks. + // This fallback keeps maintenance scripts consistent with Applied state. + if (model?.tableName === "proxy_host") { + await model.query().where("id", host.id).patch({ + nginx_applied_revision: host.nginx_config_revision, + nginx_applied_enabled: 1, + nginx_applied_hash: rendered.configHash, + nginx_applied_snapshot: rendered.snapshot, + nginx_deployment_status: "online", + nginx_checked_at: databaseNow(), + nginx_last_error: null, }); - }) - .then(() => { - return internalNginx.reload(); - }) - .then(() => { - return combined_meta; - }); - }, - - /** - * @returns {Promise} - */ - test: () => { - debug(logger, "Testing Nginx configuration"); - return utils.execFile("/usr/sbin/nginx", ["-t", "-g", "error_log off;"]); - }, - - /** - * @returns {Promise} - */ - reload: () => { - return internalNginx.test().then(() => { - logger.info("Reloading Nginx"); - return utils.execFile("/usr/sbin/nginx", ["-s", "reload"]); - }); - }, - - /** - * @param {String} host_type - * @param {Integer} host_id - * @returns {String} - */ - getConfigName: (host_type, host_id) => { - if (host_type === "default") { - return "/data/nginx/default_host/site.conf"; - } - return `/data/nginx/${internalNginx.getFileFriendlyHostType(host_type)}/${host_id}.conf`; - }, - - /** - * Generates custom locations - * @param {Object} host - * @returns {Promise} - */ - renderLocations: (host) => { - return new Promise((resolve, reject) => { - let template; - - try { - template = fs.readFileSync(`${__dirname}/../templates/_location.conf`, { encoding: "utf8" }); - } catch (err) { - reject(new errs.ConfigurationError(err.message)); - return; - } - - const renderEngine = utils.getRenderEngine(); - let renderedLocations = ""; - - const locationRendering = async () => { - for (let i = 0; i < host.locations.length; i++) { - const locationCopy = Object.assign( - {}, - { access_list_id: host.access_list_id }, - { certificate_id: host.certificate_id }, - { ssl_forced: host.ssl_forced }, - { caching_enabled: host.caching_enabled }, - { block_exploits: host.block_exploits }, - { allow_websocket_upgrade: host.allow_websocket_upgrade }, - { http2_support: host.http2_support }, - { hsts_enabled: host.hsts_enabled }, - { hsts_subdomains: host.hsts_subdomains }, - { access_list: host.access_list }, - { certificate: host.certificate }, - host.locations[i], - ); - - if (locationCopy.forward_host.indexOf("/") > -1) { - const splitted = locationCopy.forward_host.split("/"); - - locationCopy.forward_host = splitted.shift(); - locationCopy.forward_path = `/${splitted.join("/")}`; - } - - renderedLocations += await renderEngine.parseAndRender(template, locationCopy); } - }; - - locationRendering().then(() => resolve(renderedLocations)); - }); - }, - - /** - * @param {String} host_type - * @param {Object} host - * @returns {Promise} - */ - generateConfig: (host_type, host_row) => { - // Prevent modifying the original object: - const host = JSON.parse(JSON.stringify(host_row)); - const nice_host_type = internalNginx.getFileFriendlyHostType(host_type); - - debug(logger, `Generating ${nice_host_type} Config:`, JSON.stringify(host, null, 2)); - - const renderEngine = utils.getRenderEngine(); - - return new Promise((resolve, reject) => { - let template = null; - const filename = internalNginx.getConfigName(nice_host_type, host.id); - - try { - template = fs.readFileSync(`${__dirname}/../templates/${nice_host_type}.conf`, { encoding: "utf8" }); - } catch (err) { - reject(new errs.ConfigurationError(err.message)); - return; - } - - let locationsPromise; - let origLocations; - - // Manipulate the data a bit before sending it to the template - if (nice_host_type !== "default") { - host.use_default_location = true; - if (typeof host.advanced_config !== "undefined" && host.advanced_config) { - host.use_default_location = !internalNginx.advancedConfigHasDefaultLocation(host.advanced_config); + }, + commitFailure: async ({ error }) => { + if (model?.query && "meta" in host) { + await model + .query() + .where("id", host.id) + .patch({ + meta: _.assign({}, priorMeta, { nginx_online: false, nginx_err: error.message }), + }); } - } - - // For redirection hosts, if the scheme is not http or https, set it to $scheme - if (nice_host_type === "redirection_host" && ['http', 'https'].indexOf(host.forward_scheme.toLowerCase()) === -1) { - host.forward_scheme = "$scheme"; - } - - if (host.locations) { - //logger.info ('host.locations = ' + JSON.stringify(host.locations, null, 2)); - origLocations = [].concat(host.locations); - locationsPromise = internalNginx.renderLocations(host).then((renderedLocations) => { - host.locations = renderedLocations; - }); - - // Allow someone who is using / custom location path to use it, and skip the default / location - _.map(host.locations, (location) => { - if (location.path === "/") { - host.use_default_location = false; - } - }); - } else { - locationsPromise = Promise.resolve(); - } - - // Set the IPv6 setting for the host - host.ipv6 = internalNginx.ipv6Enabled(); - - locationsPromise.then(() => { - renderEngine - .parseAndRender(template, host) - .then((config_text) => { - fs.writeFileSync(filename, config_text, { encoding: "utf8" }); - debug(logger, "Wrote config:", filename, config_text); - - // Restore locations array - host.locations = origLocations; - - resolve(true); - }) - .catch((err) => { - debug(logger, `Could not write ${filename}:`, err.message); - reject(new errs.ConfigurationError(err.message)); - }); - }); - }); - }, - - /** - * This generates a temporary nginx config listening on port 80 for the domain names listed - * in the certificate setup. It allows the letsencrypt acme challenge to be requested by letsencrypt - * when requesting a certificate without having a hostname set up already. - * - * @param {Object} certificate - * @returns {Promise} - */ - generateLetsEncryptRequestConfig: (certificate) => { - debug(logger, "Generating LetsEncrypt Request Config:", certificate); - const renderEngine = utils.getRenderEngine(); - - return new Promise((resolve, reject) => { - let template = null; - const filename = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`; - - try { - template = fs.readFileSync(`${__dirname}/../templates/letsencrypt-request.conf`, { encoding: "utf8" }); - } catch (err) { - reject(new errs.ConfigurationError(err.message)); - return; - } - - certificate.ipv6 = internalNginx.ipv6Enabled(); - - renderEngine - .parseAndRender(template, certificate) - .then((config_text) => { - fs.writeFileSync(filename, config_text, { encoding: "utf8" }); - debug(logger, "Wrote config:", filename, config_text); - resolve(true); - }) - .catch((err) => { - debug(logger, `Could not write ${filename}:`, err.message); - reject(new errs.ConfigurationError(err.message)); - }); + }, }); + return appliedMeta; }, - /** - * A simple wrapper around unlinkSync that writes to the logger - * - * @param {String} filename - */ - deleteFile: (filename) => { - if (!fs.existsSync(filename)) { - return; - } - try { - debug(logger, `Deleting file: ${filename}`); - fs.unlinkSync(filename); - } catch (err) { - debug(logger, "Could not delete file:", JSON.stringify(err, null, 2)); - } - }, - - /** - * - * @param {String} host_type - * @returns String - */ - getFileFriendlyHostType: (host_type) => { - return host_type.replace(/-/g, "_"); - }, - - /** - * This removes the temporary nginx config file generated by `generateLetsEncryptRequestConfig` - * - * @param {Object} certificate - * @returns {Promise} - */ - deleteLetsEncryptRequestConfig: (certificate) => { - const config_file = `/data/nginx/temp/letsencrypt_${certificate.id}.conf`; - return new Promise((resolve /*, reject*/) => { - internalNginx.deleteFile(config_file); - resolve(); + generateConfig: async (hostType, host) => { + await nginxDeploymentCoordinator.deploy({ + hostType, + host, + dependencies: { certificate: host.certificate, access_list: host.access_list }, + operation: `${hostType}_generate`, + deploymentStore: deploymentStoreFor(host), }); }, - /** - * @param {String} host_type - * @param {Object} [host] - * @param {Boolean} [delete_err_file] - * @returns {Promise} - */ - deleteConfig: (host_type, host, delete_err_file) => { - const config_file = internalNginx.getConfigName( - internalNginx.getFileFriendlyHostType(host_type), - typeof host === "undefined" ? 0 : host.id, - ); - const config_file_err = `${config_file}.err`; - - return new Promise((resolve /*, reject*/) => { - internalNginx.deleteFile(config_file); - if (delete_err_file) { - internalNginx.deleteFile(config_file_err); - } - resolve(); + deleteConfig: async (hostType, host) => { + await nginxDeploymentCoordinator.remove({ + hostType, + host, + operation: `${hostType}_remove`, + deploymentStore: deploymentStoreFor(host), }); }, - /** - * @param {String} host_type - * @param {Object} [host] - * @returns {Promise} - */ - renameConfigAsError: (host_type, host) => { - const config_file = internalNginx.getConfigName( - internalNginx.getFileFriendlyHostType(host_type), - typeof host === "undefined" ? 0 : host.id, - ); - const config_file_err = `${config_file}.err`; - - return new Promise((resolve /*, reject*/) => { - fs.unlink(config_file, () => { - // ignore result, continue - fs.rename(config_file, config_file_err, () => { - // also ignore result, as this is a debugging informative file anyway - resolve(); - }); - }); - }); + bulkGenerateConfigs: async (hostType, hosts) => { + for (const host of hosts || []) await internalNginx.generateConfig(hostType, host); }, - /** - * @param {String} hostType - * @param {Array} hosts - * @returns {Promise} - */ - bulkGenerateConfigs: (hostType, hosts) => { - const promises = []; - hosts.map((host) => { - promises.push(internalNginx.generateConfig(hostType, host)); - return true; - }); - - return Promise.all(promises); - }, - - /** - * @param {String} host_type - * @param {Array} hosts - * @returns {Promise} - */ - bulkDeleteConfigs: (host_type, hosts) => { - const promises = []; - hosts.map((host) => { - promises.push(internalNginx.deleteConfig(host_type, host, true)); - return true; - }); - - return Promise.all(promises); + bulkDeleteConfigs: async (hostType, hosts) => { + for (const host of hosts || []) await internalNginx.deleteConfig(hostType, host); }, - /** - * @param {string} config - * @returns {boolean} - */ - advancedConfigHasDefaultLocation: (cfg) => !!cfg.match(/^(?:.*;)?\s*?location\s*?\/\s*?{/im), - - /** - * @returns {boolean} - */ - ipv6Enabled: () => { - if (typeof process.env.DISABLE_IPV6 !== "undefined") { - const disabled = process.env.DISABLE_IPV6.toLowerCase(); - return !(disabled === "on" || disabled === "true" || disabled === "1" || disabled === "yes"); - } - - return true; - }, + test: () => nginxDeploymentCoordinator.testOnly("legacy_test"), + reload: () => nginxDeploymentCoordinator.reloadOnly("legacy_reload"), + getConfigName: (hostType, hostId) => activeArtifactPath(hostType, hostId), + getFileFriendlyHostType: (hostType) => hostType, + getConfigPath: (hostType, hostId) => activeArtifactPath(hostType, hostId), + getConfig: async (hostType, hostId) => readArtifact(activeArtifactPath(hostType, hostId)), }; +logger.debug?.("Nginx compatibility facade initialized with deployment coordinator"); export default internalNginx; diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js index 2c159d48ad..c649b1f11c 100644 --- a/backend/internal/proxy-host.js +++ b/backend/internal/proxy-host.js @@ -1,17 +1,340 @@ +import { randomUUID } from "node:crypto"; +import fs from "node:fs/promises"; import _ from "lodash"; import errs from "../lib/error.js"; import { castJsonIfNeed } from "../lib/helpers.js"; import utils from "../lib/utils.js"; +import nginxDeploymentModel from "../models/nginx_deployment.js"; +import databaseNow from "../models/now_helper.js"; import proxyHostModel from "../models/proxy_host.js"; +import internalAccessList from "./access-list.js"; import internalAuditLog from "./audit-log.js"; import internalCertificate from "./certificate.js"; import internalHost from "./host.js"; -import internalNginx from "./nginx.js"; +import { + activeArtifactPath, + atomicWrite, + candidateArtifactPath, + readArtifact, + removeArtifact, + toLogicalPath, +} from "./nginx-config-artifacts.js"; +import { sha256 } from "./nginx-config-hash.js"; +import { buildProxyHostCandidate } from "./nginx-config-renderer.js"; +import { validateInMirror } from "./nginx-config-validator.js"; +import nginxDeploymentCoordinator, { deriveDeploymentStatus } from "./nginx-deployment-coordinator.js"; +import { createDeploymentStore } from "./nginx-deployment-store.js"; +import { issuePreviewToken, verifyPreviewToken } from "./nginx-preview-token.js"; const omissions = () => { return ["is_deleted", "owner.is_deleted"]; }; +const deploymentOccurredAt = () => new Date().toISOString(); + +const getPortListenerPort = (host) => + host?.nginx_config?.listener?.mode === "port" ? Number(host.nginx_config.listener.port) : null; + +const assertPortListenerAvailable = async (host, excludedId) => { + const port = getPortListenerPort(host); + if (!Number.isInteger(port)) return; + const query = proxyHostModel.query().where("is_deleted", 0).select("id", "nginx_config"); + if (excludedId) query.whereNot("id", excludedId); + const rows = await query; + if (rows.some((row) => getPortListenerPort(row) === port)) + throw new errs.ValidationError(`Port ${port} is already in use by another port listener`); +}; + +const previewFields = (payload) => + _.omit(payload, [ + "host_id", + "base_revision", + "preview_token", + "id", + "created_on", + "modified_on", + "owner", + "certificate", + "access_list", + ]); + +/** + * Resolve the same persisted dependencies the deploy path will use. A preview + * must not sign a rendering that was produced with a different certificate or + * access list than the one being saved. + */ +const resolvePreviewCandidate = async (access, payload) => { + let persisted = null; + if (payload.host_id) { + persisted = await internalProxyHost.get(access, { + id: payload.host_id, + expand: ["certificate", "access_list.[clients,items]"], + }); + } + + const host = { + ...(persisted || {}), + ...previewFields(payload), + id: persisted?.id ?? payload.host_id ?? payload.id ?? null, + }; + const unresolved = []; + let certificate = persisted?.certificate ?? null; + let accessList = persisted?.access_list ?? null; + + if (host.certificate_id === "new") { + unresolved.push({ + code: "CERTIFICATE_PENDING_CREATE", + message: "A new certificate must be created before this preview can be fully validated.", + }); + certificate = null; + } else if ( + Number.isInteger(host.certificate_id) && + host.certificate_id > 0 && + certificate?.id !== host.certificate_id + ) { + certificate = await internalCertificate.get(access, { id: host.certificate_id }); + } + + if (Number.isInteger(host.access_list_id) && host.access_list_id > 0 && accessList?.id !== host.access_list_id) { + accessList = await internalAccessList.get(access, { id: host.access_list_id, expand: ["clients", "items"] }); + } + + return { host, dependencies: { certificate, access_list: accessList }, unresolved }; +}; + +const validatePreviewInMirror = async ({ host, config }) => { + if (!host.id) + return { + validation_scope: "partial", + diagnostics: [ + { + severity: "info", + code: "PREVIEW_NEW_HOST", + message: "A new host has no active artifact path yet, so only static validation was performed.", + }, + ], + }; + try { + await fs.access("/usr/sbin/nginx"); + } catch { + return { + validation_scope: "partial", + diagnostics: [ + { + severity: "warning", + code: "NGINX_BINARY_UNAVAILABLE", + message: "The nginx binary is unavailable in this runtime; only static validation was performed.", + }, + ], + }; + } + + const operationId = `preview-${randomUUID()}`; + const candidatePath = candidateArtifactPath( + "proxy_host", + host.id, + operationId, + nginxDeploymentCoordinator.nginxRoot, + ); + const targetPath = activeArtifactPath("proxy_host", host.id, nginxDeploymentCoordinator.nginxRoot); + try { + await atomicWrite(candidatePath, config); + const result = await validateInMirror({ + nginxRoot: nginxDeploymentCoordinator.nginxRoot, + nginxConfigPath: nginxDeploymentCoordinator.nginxConfigPath, + nginxPrefix: nginxDeploymentCoordinator.nginxPrefix, + operationId, + candidatePath, + targetPath, + commandRunner: nginxDeploymentCoordinator.commandRunner, + }); + return { + validation_scope: result.validation_scope, + diagnostics: result.valid + ? [] + : [ + { + severity: "error", + code: "NGINX_TEST_FAILED", + message: result.stderr || "nginx configuration test failed", + }, + ], + }; + } finally { + await removeArtifact(candidatePath).catch(() => undefined); + } +}; + +const assertPreviewMatchesSave = async (access, row, payload, baseRevision, previewToken, createCertificate) => { + if (!previewToken) return; + const verified = verifyPreviewToken(previewToken, { + host_id: row.id, + base_revision: baseRevision ?? row.nginx_config_revision, + }); + if (!verified.valid) + throw new errs.ConflictError("Nginx preview is no longer valid", "PREVIEW_TOKEN_INVALID", { + reason: verified.reason, + current_revision: row.nginx_config_revision, + }); + if (createCertificate) + throw new errs.ConflictError("Nginx preview is no longer valid", "PREVIEW_TOKEN_INVALID", { + reason: "certificate_pending_create", + current_revision: row.nginx_config_revision, + }); + const candidate = await resolvePreviewCandidate(access, { ...payload, host_id: row.id }); + const rendered = await buildProxyHostCandidate(candidate); + for (const field of ["payload_hash", "dependency_hash", "template_hash", "capability_hash"]) { + const expected = verified.data[field]; + const actual = + field === "payload_hash" + ? rendered.payloadHash + : field === "dependency_hash" + ? rendered.dependencyHash + : field === "template_hash" + ? rendered.templateHash + : rendered.capabilityHash; + if (expected !== actual) + throw new errs.ConflictError( + "Nginx preview does not match the current save request", + "PREVIEW_TOKEN_INVALID", + { + reason: field, + current_revision: row.nginx_config_revision, + expected_hash: expected, + actual_hash: actual, + }, + ); + } +}; + +const deploymentIdFor = async (operationId) => { + const deployment = await nginxDeploymentModel.query().findOne("operation_id", operationId); + return deployment?.id ?? null; +}; + +const deploymentError = (operationId, error, journal) => ({ + operation_id: operationId, + code: error.code || "DEPLOYMENT_FAILED", + message: error.message, + diagnostics: error.diagnostics ?? null, + journal_phase: journal.phase, + occurred_at: deploymentOccurredAt(), +}); + +/** Persist Applied only after the coordinator has swapped, reloaded, and + * successfully committed. This keeps Desired durable even when a deployment + * fails and makes an old active artifact explicitly visible as degraded. */ +const deployProxyHost = async (host) => { + const deploymentStore = createDeploymentStore({ ownerUserId: host.owner_user_id }); + return nginxDeploymentCoordinator.deploy({ + hostType: "proxy_host", + host, + dependencies: { certificate: host.certificate, access_list: host.access_list }, + operation: "proxy_host_deploy", + deploymentStore, + beforeCommit: async ({ operationId }) => { + await proxyHostModel + .query() + .where("id", host.id) + .patch({ + nginx_deployment_status: "pending", + nginx_last_error: null, + nginx_checked_at: databaseNow(), + nginx_last_deployment_id: await deploymentIdFor(operationId), + }); + }, + commitApplied: async ({ operationId, rendered }) => { + const meta = _.assign({}, host.meta, { nginx_online: true, nginx_err: null }); + await proxyHostModel + .query() + .where("id", host.id) + .patch({ + meta, + nginx_applied_revision: host.nginx_config_revision, + nginx_applied_enabled: 1, + nginx_applied_hash: rendered.configHash, + nginx_applied_snapshot: rendered.snapshot, + nginx_deployment_status: "online", + nginx_checked_at: databaseNow(), + nginx_last_error: null, + nginx_last_deployment_id: await deploymentIdFor(operationId), + }); + }, + commitFailure: async ({ operationId, error, journal }) => { + const previous = await proxyHostModel.query().findById(host.id); + const status = deriveDeploymentStatus({ + ...previous, + active_hash: previous?.nginx_applied_hash, + deployment_state: "failed", + }); + const meta = _.assign({}, previous?.meta, { nginx_online: false, nginx_err: error.message }); + await proxyHostModel + .query() + .where("id", host.id) + .patch({ + meta, + nginx_deployment_status: status, + nginx_checked_at: databaseNow(), + nginx_last_error: deploymentError(operationId, error, journal), + nginx_last_deployment_id: await deploymentIdFor(operationId), + }); + }, + }); +}; + +const removeProxyHostArtifact = async (host, statusWhenApplied = "disabled") => { + const deploymentStore = createDeploymentStore({ ownerUserId: host.owner_user_id }); + return nginxDeploymentCoordinator.remove({ + hostType: "proxy_host", + host, + operation: "proxy_host_remove", + deploymentStore, + beforeCommit: async ({ operationId }) => { + await proxyHostModel + .query() + .where("id", host.id) + .patch({ + nginx_deployment_status: "pending", + nginx_checked_at: databaseNow(), + nginx_last_deployment_id: await deploymentIdFor(operationId), + }); + }, + commitApplied: async ({ operationId }) => { + const meta = _.assign({}, host.meta, { nginx_online: false, nginx_err: null }); + await proxyHostModel + .query() + .where("id", host.id) + .patch({ + meta, + nginx_applied_revision: host.nginx_config_revision, + nginx_applied_enabled: 0, + nginx_applied_hash: null, + nginx_deployment_status: statusWhenApplied, + nginx_checked_at: databaseNow(), + nginx_last_error: null, + nginx_last_deployment_id: await deploymentIdFor(operationId), + }); + }, + commitFailure: async ({ operationId, error, journal }) => { + const previous = await proxyHostModel.query().findById(host.id); + const meta = _.assign({}, previous?.meta, { + nginx_online: Boolean(previous?.nginx_applied_enabled), + nginx_err: error.message, + }); + await proxyHostModel + .query() + .where("id", host.id) + .patch({ + meta, + nginx_deployment_status: previous?.nginx_applied_enabled ? "degraded" : "error", + nginx_checked_at: databaseNow(), + nginx_last_error: deploymentError(operationId, error, journal), + nginx_last_deployment_id: await deploymentIdFor(operationId), + }); + }, + }); +}; + const internalProxyHost = { /** * @param {Access} access @@ -46,7 +369,8 @@ const internalProxyHost = { }); }); }) - .then(() => { + .then(async () => { + await assertPortListenerAvailable(thisData); // At this point the domains should have been checked thisData.owner_user_id = access.token.getUserId(1); thisData = internalHost.cleanSslHstsData(thisData); @@ -83,27 +407,23 @@ const internalProxyHost = { expand: ["certificate", "owner", "access_list.[clients,items]"], }); }) - .then((row) => { - // Configure nginx - return internalNginx.configure(proxyHostModel, "proxy_host", row).then(() => { - return row; - }); - }) - .then((row) => { - // Audit log + .then(async (row) => { + // Desired is already durable at this point, so audit it before the + // deployment attempt. A deployment failure must not turn a successful + // create into an ambiguous HTTP 500 that encourages a duplicate retry. thisData.meta = _.assign({}, thisData.meta || {}, row.meta); + await internalAuditLog.add(access, { + action: "created", + object_type: "proxy-host", + object_id: row.id, + meta: thisData, + }); - // Add to audit log - return internalAuditLog - .add(access, { - action: "created", - object_type: "proxy-host", - object_id: row.id, - meta: thisData, - }) - .then(() => { - return row; - }); + await deployProxyHost(row).catch(() => undefined); + return internalProxyHost.get(access, { + id: row.id, + expand: ["certificate", "owner", "access_list.[clients,items]"], + }); }); }, @@ -120,6 +440,10 @@ const internalProxyHost = { if (createCertificate) { delete thisData.certificate_id; } + const baseRevision = thisData.base_revision; + const previewToken = thisData.preview_token; + delete thisData.base_revision; + delete thisData.preview_token; return access .can("proxy_hosts:update", thisData.id) @@ -145,9 +469,23 @@ const internalProxyHost = { } }) .then(() => { - return internalProxyHost.get(access, { id: thisData.id }); + return internalProxyHost.get(access, { + id: thisData.id, + expand: ["certificate", "access_list.[clients,items]"], + }); }) - .then((row) => { + .then(async (row) => { + await assertPortListenerAvailable( + { ...row, ...thisData, nginx_config: thisData.nginx_config ?? row.nginx_config }, + row.id, + ); + if (typeof baseRevision !== "undefined" && baseRevision !== row.nginx_config_revision) { + throw new errs.ConflictError("Proxy Host has changed", "REVISION_CONFLICT", { + current_revision: row.nginx_config_revision, + }); + } + await assertPreviewMatchesSave(access, row, thisData, baseRevision, previewToken, createCertificate); + if (row.id !== thisData.id) { // Sanity check that something crazy hasn't happened throw new errs.InternalValidationError( @@ -185,8 +523,12 @@ const internalProxyHost = { return proxyHostModel .query() - .where({ id: thisData.id }) - .patch(thisData) + .where({ id: thisData.id, nginx_config_revision: row.nginx_config_revision }) + .patch({ + ...thisData, + nginx_config_revision: (row.nginx_config_revision || 1) + 1, + nginx_deployment_status: row.enabled || thisData.enabled ? "pending" : "disabled", + }) .then(utils.omitRow(omissions())) .then((saved_row) => { // Add to audit log @@ -214,10 +556,15 @@ const internalProxyHost = { return row; } // Configure nginx - return internalNginx.configure(proxyHostModel, "proxy_host", row).then((new_meta) => { - row.meta = new_meta; - return _.omit(internalHost.cleanRowCertificateMeta(row), omissions()); - }); + return deployProxyHost(row) + .catch(() => undefined) + .then(() => + internalProxyHost.get(access, { + id: row.id, + expand: ["owner", "certificate", "access_list.[clients,items]"], + }), + ) + .then((updated) => _.omit(internalHost.cleanRowCertificateMeta(updated), omissions())); }); }); }, @@ -288,13 +635,15 @@ const internalProxyHost = { .where("id", row.id) .patch({ is_deleted: 1, + nginx_config_revision: (row.nginx_config_revision || 1) + 1, + nginx_deployment_status: "pending", }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig("proxy_host", row).then(() => { - return internalNginx.reload(); - }); - }) + .then(() => + removeProxyHostArtifact( + { ...row, is_deleted: true, nginx_config_revision: (row.nginx_config_revision || 1) + 1 }, + "deleted", + ), + ) .then(() => { // Add to audit log return internalAuditLog.add(access, { @@ -341,11 +690,16 @@ const internalProxyHost = { .where("id", row.id) .patch({ enabled: 1, + nginx_config_revision: (row.nginx_config_revision || 1) + 1, + nginx_deployment_status: "pending", }) - .then(() => { - // Configure nginx - return internalNginx.configure(proxyHostModel, "proxy_host", row); - }) + .then(() => + deployProxyHost({ + ...row, + enabled: true, + nginx_config_revision: (row.nginx_config_revision || 1) + 1, + }), + ) .then(() => { // Add to audit log return internalAuditLog.add(access, { @@ -389,13 +743,15 @@ const internalProxyHost = { .where("id", row.id) .patch({ enabled: 0, + nginx_config_revision: (row.nginx_config_revision || 1) + 1, + nginx_deployment_status: "pending", }) - .then(() => { - // Delete Nginx Config - return internalNginx.deleteConfig("proxy_host", row).then(() => { - return internalNginx.reload(); - }); - }) + .then(() => + removeProxyHostArtifact( + { ...row, enabled: false, nginx_config_revision: (row.nginx_config_revision || 1) + 1 }, + "disabled", + ), + ) .then(() => { // Add to audit log return internalAuditLog.add(access, { @@ -411,6 +767,87 @@ const internalProxyHost = { }); }, + /** + * Returns the actual active artifact and the last failed candidate without + * accepting caller-controlled filesystem paths. + */ + getNginxArtifacts: async (access, id, includeContent = []) => { + const row = await internalProxyHost.get(access, { id }); + const deployedPath = activeArtifactPath("proxy_host", row.id); + const deployedContent = await readArtifact(deployedPath); + let candidate = null; + if (row.nginx_last_deployment_id && row.nginx_last_error?.operation_id) { + const candidatePath = candidateArtifactPath("proxy_host", row.id, row.nginx_last_error.operation_id); + const content = await readArtifact(candidatePath); + if (content !== null) + candidate = { + logical_path: toLogicalPath(candidatePath), + hash: sha256(Buffer.from(content)), + ...(includeContent.includes("candidate") ? { config: content } : {}), + }; + } + const activeHash = deployedContent === null ? null : sha256(Buffer.from(deployedContent)); + const status = row.nginx_deployment_status || (row.enabled ? "pending" : "disabled"); + return { + host_id: row.id, + status, + desired_revision: row.nginx_config_revision ?? 1, + applied_revision: row.nginx_applied_revision ?? null, + deployed: + deployedContent === null + ? null + : { + logical_path: toLogicalPath(deployedPath), + hash: activeHash, + ...(includeContent.includes("deployed") ? { config: deployedContent } : {}), + }, + candidate, + last_error: row.nginx_last_error ?? null, + last_checked_at: row.nginx_checked_at ?? null, + }; + }, + + previewNginxConfig: async (access, payload) => { + if (payload.host_id) await access.can("proxy_hosts:update", payload.host_id); + else await access.can("proxy_hosts:create", payload); + const candidate = await resolvePreviewCandidate(access, payload); + const result = await buildProxyHostCandidate(candidate); + const mirror = candidate.unresolved.length + ? { validation_scope: "partial", diagnostics: [] } + : await validatePreviewInMirror({ host: candidate.host, config: result.config }); + const diagnostics = [ + ...result.diagnostics, + ...candidate.unresolved.map((item) => ({ severity: "warning", ...item })), + ...mirror.diagnostics, + ]; + const valid = !diagnostics.some((item) => item.severity === "error"); + return { + valid, + config: result.config, + payload_hash: result.payloadHash, + hash: result.configHash, + dependency_hash: result.dependencyHash, + capability_hash: result.capabilityHash, + template_version: result.templateVersion, + template_hash: result.templateHash, + base_revision: payload.base_revision ?? null, + preview_token: + valid && !candidate.unresolved.length && mirror.validation_scope === "full" + ? issuePreviewToken({ + hostId: candidate.host.id, + baseRevision: payload.base_revision ?? null, + payloadHash: result.payloadHash, + dependencyHash: result.dependencyHash, + templateHash: result.templateHash, + capabilityHash: result.capabilityHash, + }) + : null, + validation_scope: mirror.validation_scope, + unresolved_dependencies: candidate.unresolved, + diagnostics, + }; + }, + /** * All Hosts * diff --git a/backend/lib/error.js b/backend/lib/error.js index d7dbf0c965..7df9d5cbf5 100644 --- a/backend/lib/error.js +++ b/backend/lib/error.js @@ -86,6 +86,39 @@ const errs = { this.status = 400; }, + ConflictError: function (message, errorCode = "CONFLICT", details = undefined, previous = undefined) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = message; + this.status = 409; + this.public = true; + this.error_code = errorCode; + this.details = details; + }, + + UnprocessableConfigError: function (message, details = undefined, previous = undefined) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = message; + this.status = 422; + this.public = true; + this.error_code = "UNPROCESSABLE_NGINX_CONFIG"; + this.details = details; + }, + + ServiceUnavailableError: function (message, errorCode = "SERVICE_UNAVAILABLE", details = undefined, previous = undefined) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.previous = previous; + this.message = message; + this.status = 503; + this.public = true; + this.error_code = errorCode; + this.details = details; + }, + CommandError: function (stdErr, code, previous) { Error.captureStackTrace(this, this.constructor); this.name = this.constructor.name; diff --git a/backend/lib/utils.js b/backend/lib/utils.js index a19142af03..7d0631e772 100644 --- a/backend/lib/utils.js +++ b/backend/lib/utils.js @@ -61,6 +61,31 @@ const omitRow = (omissions) => { }; }; +/** + * Executes a file command and returns both output streams. This is deliberately + * separate from execFile() to preserve the historical stdout-only contract. + * + * @param {String} cmd + * @param {Array} args + * @param {Object|undefined} options + * @returns {Promise<{stdout: string, stderr: string}>} + */ +const execFileResult = (cmd, args, options) => { + const opts = options || {}; + return new Promise((resolve, reject) => { + nodeExecFile(cmd, args, opts, (err, stdout, stderr) => { + if (err && typeof err === "object") { + const commandError = new errs.CommandError(stderr, err.code || 1, err); + commandError.stdout = stdout || ""; + commandError.stderr = stderr || ""; + reject(commandError); + return; + } + resolve({ stdout: (stdout || "").trim(), stderr: (stderr || "").trim() }); + }); + }); +}; + /** * Used in objection query builder * @@ -104,4 +129,4 @@ const getRenderEngine = () => { return renderEngine; }; -export default { exec, execFile, omitRow, omitRows, getRenderEngine }; +export default { exec, execFile, execFileResult, omitRow, omitRows, getRenderEngine }; diff --git a/backend/migrations/20260731120000_proxy_host_nginx_desired_applied.js b/backend/migrations/20260731120000_proxy_host_nginx_desired_applied.js new file mode 100644 index 0000000000..fcbe7531a2 --- /dev/null +++ b/backend/migrations/20260731120000_proxy_host_nginx_desired_applied.js @@ -0,0 +1,43 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "proxy_host_nginx_desired_applied"; + +export const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + await knex.schema.alterTable("proxy_host", (table) => { + table.json("nginx_config").nullable(); + table.integer("nginx_config_revision").unsigned().notNullable().defaultTo(1); + table.integer("nginx_applied_revision").unsigned().nullable(); + table.tinyint("nginx_applied_enabled").notNullable().defaultTo(0); + table.string("nginx_applied_hash", 71).nullable(); + table.string("nginx_deployment_status", 20).notNullable().defaultTo("pending"); + table.dateTime("nginx_checked_at").nullable(); + table.json("nginx_last_error").nullable(); + table.json("nginx_applied_snapshot").nullable(); + table.integer("nginx_last_deployment_id").unsigned().nullable(); + }); + // Do not infer Applied from enabled: migration has no authority to inspect the + // active filesystem. Startup reconciliation establishes that relationship. + await knex("proxy_host").update({ + nginx_config: JSON.stringify({ schema_version: 1 }), + nginx_config_revision: 1, + nginx_deployment_status: "pending", + }); + logger.info(`[${migrateName}] proxy_host Table altered`); +}; + +export const down = async (knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + await knex.schema.alterTable("proxy_host", (table) => { + table.dropColumn("nginx_last_deployment_id"); + table.dropColumn("nginx_applied_snapshot"); + table.dropColumn("nginx_last_error"); + table.dropColumn("nginx_checked_at"); + table.dropColumn("nginx_deployment_status"); + table.dropColumn("nginx_applied_hash"); + table.dropColumn("nginx_applied_enabled"); + table.dropColumn("nginx_applied_revision"); + table.dropColumn("nginx_config_revision"); + table.dropColumn("nginx_config"); + }); +}; diff --git a/backend/migrations/20260731120100_nginx_deployment.js b/backend/migrations/20260731120100_nginx_deployment.js new file mode 100644 index 0000000000..a7dd9f3d9c --- /dev/null +++ b/backend/migrations/20260731120100_nginx_deployment.js @@ -0,0 +1,38 @@ +import { migrate as logger } from "../logger.js"; + +const migrateName = "nginx_deployment"; + +export const up = async (knex) => { + logger.info(`[${migrateName}] Migrating Up...`); + await knex.schema.createTable("nginx_deployment", (table) => { + table.increments("id").primary(); + table.string("operation_id", 36).notNullable().unique(); + table.string("parent_operation_id", 36).nullable().index(); + table.string("host_type", 32).notNullable(); + table.integer("host_id").unsigned().nullable(); + table.integer("owner_user_id").unsigned().nullable(); + table.string("operation", 32).notNullable(); + table.string("state", 32).notNullable().index(); + table.integer("requested_revision").unsigned().nullable(); + table.string("payload_hash", 71).nullable(); + table.string("dependency_hash", 71).nullable(); + table.string("template_version", 80).nullable(); + table.string("template_hash", 71).nullable(); + table.string("capability_hash", 71).nullable(); + table.string("config_hash", 71).nullable(); + table.string("candidate_path", 255).nullable(); + table.json("diagnostics").nullable(); + table.json("journal_summary").nullable(); + table.dateTime("started_on").notNullable(); + table.dateTime("finished_on").nullable(); + table.dateTime("created_on").notNullable(); + table.dateTime("modified_on").notNullable(); + table.index(["host_type", "host_id", "id"]); + }); + logger.info(`[${migrateName}] nginx_deployment Table created`); +}; + +export const down = async (knex) => { + logger.info(`[${migrateName}] Migrating Down...`); + await knex.schema.dropTableIfExists("nginx_deployment"); +}; diff --git a/backend/models/nginx_deployment.js b/backend/models/nginx_deployment.js new file mode 100644 index 0000000000..04eccae781 --- /dev/null +++ b/backend/models/nginx_deployment.js @@ -0,0 +1,28 @@ +import { Model } from "objection"; +import db from "../db.js"; +import now from "./now_helper.js"; + +Model.knex(db()); + +class NginxDeployment extends Model { + $beforeInsert() { + const timestamp = now(); + this.created_on ??= timestamp; + this.modified_on ??= timestamp; + this.started_on ??= timestamp; + } + + $beforeUpdate() { + this.modified_on = now(); + } + + static get tableName() { + return "nginx_deployment"; + } + + static get jsonAttributes() { + return ["diagnostics", "journal_summary"]; + } +} + +export default NginxDeployment; diff --git a/backend/models/proxy_host.js b/backend/models/proxy_host.js index acb8da9358..bb678602a4 100644 --- a/backend/models/proxy_host.js +++ b/backend/models/proxy_host.js @@ -1,127 +1,137 @@ -// Objection Docs: -// http://vincit.github.io/objection.js/ - -import { Model } from "objection"; -import db from "../db.js"; -import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; -import AccessList from "./access_list.js"; -import Certificate from "./certificate.js"; -import now from "./now_helper.js"; -import User from "./user.js"; - -Model.knex(db()); - -const boolFields = [ - "is_deleted", - "ssl_forced", - "caching_enabled", - "block_exploits", - "allow_websocket_upgrade", - "http2_support", - "enabled", - "hsts_enabled", - "hsts_subdomains", - "trust_forwarded_proto", -]; - -class ProxyHost extends Model { - $beforeInsert() { - this.created_on = now(); - this.modified_on = now(); - - // Default for domain_names - if (typeof this.domain_names === "undefined") { - this.domain_names = []; - } - - // Default for meta - if (typeof this.meta === "undefined") { - this.meta = {}; - } - - this.domain_names.sort(); - } - - $beforeUpdate() { - this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== "undefined") { - this.domain_names.sort(); - } - } - - $parseDatabaseJson(json) { - const thisJson = super.$parseDatabaseJson(json); - return convertIntFieldsToBool(thisJson, boolFields); - } - - $formatDatabaseJson(json) { - const thisJson = convertBoolFieldsToInt(json, boolFields); - return super.$formatDatabaseJson(thisJson); - } - - static get name() { - return "ProxyHost"; - } - - static get tableName() { - return "proxy_host"; - } - - static get jsonAttributes() { - return ["domain_names", "meta", "locations"]; - } - - static get defaultAllowGraph() { - return "[owner,access_list.[clients,items],certificate]"; - } - - static get defaultExpand() { - return ["owner", "certificate", "access_list.[clients,items]"]; - } - - static get defaultOrder() { - return [castJsonIfNeed("domain_names"), "ASC"]; - } - - static get relationMappings() { - return { - owner: { - relation: Model.HasOneRelation, - modelClass: User, - join: { - from: "proxy_host.owner_user_id", - to: "user.id", - }, - modify: (qb) => { - qb.where("user.is_deleted", 0); - }, - }, - access_list: { - relation: Model.HasOneRelation, - modelClass: AccessList, - join: { - from: "proxy_host.access_list_id", - to: "access_list.id", - }, - modify: (qb) => { - qb.where("access_list.is_deleted", 0); - }, - }, - certificate: { - relation: Model.HasOneRelation, - modelClass: Certificate, - join: { - from: "proxy_host.certificate_id", - to: "certificate.id", - }, - modify: (qb) => { - qb.where("certificate.is_deleted", 0); - }, - }, - }; - } -} - -export default ProxyHost; +// Objection Docs: +// http://vincit.github.io/objection.js/ + +import { Model } from "objection"; +import db from "../db.js"; +import { castJsonIfNeed, convertBoolFieldsToInt, convertIntFieldsToBool } from "../lib/helpers.js"; +import AccessList from "./access_list.js"; +import Certificate from "./certificate.js"; +import now from "./now_helper.js"; +import { normalizeNginxConfig } from "../internal/nginx-config-normalizer.js"; +import User from "./user.js"; + +Model.knex(db()); + +const boolFields = [ + "is_deleted", + "ssl_forced", + "caching_enabled", + "block_exploits", + "allow_websocket_upgrade", + "http2_support", + "enabled", + "hsts_enabled", + "hsts_subdomains", + "trust_forwarded_proto", + "nginx_applied_enabled", +]; + +class ProxyHost extends Model { + $beforeInsert() { + this.created_on = now(); + this.modified_on = now(); + + // Default for domain_names + if (typeof this.domain_names === "undefined") { + this.domain_names = []; + } + + // Default for meta + if (typeof this.meta === "undefined") { + this.meta = {}; + } + + this.nginx_config = normalizeNginxConfig(this.nginx_config); + this.nginx_config_revision ??= 1; + + this.domain_names.sort(); + } + + $beforeUpdate() { + this.modified_on = now(); + + if (typeof this.nginx_config !== "undefined") { + this.nginx_config = normalizeNginxConfig(this.nginx_config); + } + + // Sort domain_names + if (typeof this.domain_names !== "undefined") { + this.domain_names.sort(); + } + } + + $parseDatabaseJson(json) { + const thisJson = super.$parseDatabaseJson(json); + thisJson.nginx_config = normalizeNginxConfig(thisJson.nginx_config); + return convertIntFieldsToBool(thisJson, boolFields); + } + + $formatDatabaseJson(json) { + const thisJson = convertBoolFieldsToInt(json, boolFields); + return super.$formatDatabaseJson(thisJson); + } + + static get name() { + return "ProxyHost"; + } + + static get tableName() { + return "proxy_host"; + } + + static get jsonAttributes() { + return ["domain_names", "meta", "locations", "nginx_config", "nginx_last_error", "nginx_applied_snapshot"]; + } + + static get defaultAllowGraph() { + return "[owner,access_list.[clients,items],certificate]"; + } + + static get defaultExpand() { + return ["owner", "certificate", "access_list.[clients,items]"]; + } + + static get defaultOrder() { + return [castJsonIfNeed("domain_names"), "ASC"]; + } + + static get relationMappings() { + return { + owner: { + relation: Model.HasOneRelation, + modelClass: User, + join: { + from: "proxy_host.owner_user_id", + to: "user.id", + }, + modify: (qb) => { + qb.where("user.is_deleted", 0); + }, + }, + access_list: { + relation: Model.HasOneRelation, + modelClass: AccessList, + join: { + from: "proxy_host.access_list_id", + to: "access_list.id", + }, + modify: (qb) => { + qb.where("access_list.is_deleted", 0); + }, + }, + certificate: { + relation: Model.HasOneRelation, + modelClass: Certificate, + join: { + from: "proxy_host.certificate_id", + to: "certificate.id", + }, + modify: (qb) => { + qb.where("certificate.is_deleted", 0); + }, + }, + }; + } +} + +export default ProxyHost; diff --git a/backend/package.json b/backend/package.json index 2c91ca4412..d688340224 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,7 +10,8 @@ "lint": "biome lint", "prettier": "biome format --write .", "validate-schema": "node validate-schema.js", - "regenerate-config": "node scripts/regenerate-config" + "regenerate-config": "node scripts/regenerate-config", + "test": "node --test" }, "dependencies": { "@apidevtools/json-schema-ref-parser": "^15.3.5", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml new file mode 100644 index 0000000000..af72f73f1a --- /dev/null +++ b/backend/pnpm-lock.yaml @@ -0,0 +1,3143 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@apidevtools/json-schema-ref-parser': + specifier: ^15.3.5 + version: 15.5.1(@types/json-schema@7.0.15) + ajv: + specifier: ^8.20.0 + version: 8.20.0 + archiver: + specifier: ^8.0.0 + version: 8.0.0 + batchflow: + specifier: ^0.4.0 + version: 0.4.0 + bcrypt: + specifier: ^6.0.0 + version: 6.0.0 + better-sqlite3: + specifier: ^12.10.0 + version: 12.11.1 + body-parser: + specifier: ^2.2.2 + version: 2.3.0 + chalk: + specifier: 5.6.2 + version: 5.6.2 + compression: + specifier: ^1.8.1 + version: 1.8.1 + express: + specifier: ^5.2.1 + version: 5.2.1 + express-fileupload: + specifier: ^1.5.2 + version: 1.5.2 + gravatar: + specifier: ^1.8.2 + version: 1.8.2 + jsonwebtoken: + specifier: ^9.0.3 + version: 9.0.3 + knex: + specifier: 3.2.10 + version: 3.2.10(better-sqlite3@12.11.1)(mysql2@3.23.2(@types/node@26.1.2))(pg@8.22.0)(sqlite3@6.0.1) + liquidjs: + specifier: 10.27.0 + version: 10.27.0 + lodash: + specifier: ^4.18.1 + version: 4.18.1 + moment: + specifier: ^2.30.1 + version: 2.30.1 + mysql2: + specifier: ^3.22.3 + version: 3.23.2(@types/node@26.1.2) + node-rsa: + specifier: ^2.0.0 + version: 2.0.0 + objection: + specifier: 3.1.5 + version: 3.1.5(knex@3.2.10(better-sqlite3@12.11.1)(mysql2@3.23.2(@types/node@26.1.2))(pg@8.22.0)(sqlite3@6.0.1)) + otplib: + specifier: ^13.4.0 + version: 13.4.1 + path: + specifier: ^0.12.7 + version: 0.12.7 + pg: + specifier: ^8.21.0 + version: 8.22.0 + proxy-agent: + specifier: ^8.0.1 + version: 8.0.2 + signale: + specifier: 1.4.0 + version: 1.4.0 + sqlite3: + specifier: ^6.0.1 + version: 6.0.1 + temp-write: + specifier: ^6.0.1 + version: 6.0.1 + devDependencies: + '@apidevtools/swagger-parser': + specifier: ^12.1.0 + version: 12.1.0(openapi-types@12.1.3) + '@biomejs/biome': + specifier: ^2.4.15 + version: 2.5.6 + nodemon: + specifier: ^3.1.14 + version: 3.1.14 + +packages: + + '@apidevtools/json-schema-ref-parser@14.0.1': + resolution: {integrity: sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==} + engines: {node: '>= 16'} + + '@apidevtools/json-schema-ref-parser@15.5.1': + resolution: {integrity: sha512-69KDKWQvk5jfDQfSTzAUHkI731X7CaJLwQxlS87AXyFHoJzsd/Pufrqsz7PqXMgrtAA99D9Y6NujkEkzli6kVA==} + engines: {node: '>=20'} + peerDependencies: + '@types/json-schema': ^7.0.15 + + '@apidevtools/openapi-schemas@2.1.0': + resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} + engines: {node: '>=10'} + + '@apidevtools/swagger-methods@3.0.2': + resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} + + '@apidevtools/swagger-parser@12.1.0': + resolution: {integrity: sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==} + peerDependencies: + openapi-types: '>=7' + + '@biomejs/biome@2.5.6': + resolution: {integrity: sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.6': + resolution: {integrity: sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.6': + resolution: {integrity: sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.5.6': + resolution: {integrity: sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.5.6': + resolution: {integrity: sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.5.6': + resolution: {integrity: sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.6': + resolution: {integrity: sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@noble/hashes@2.2.0': + resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} + engines: {node: '>= 20.19.0'} + + '@otplib/core@13.4.1': + resolution: {integrity: sha512-KIXgK1hNtWJEBMTastbe1bpmuais+3f+ATeO8TkMs2rNkfGO1FbQy8+/UWVEu3TR/iTJerU0idkPudaPmLP2BA==} + + '@otplib/hotp@13.4.1': + resolution: {integrity: sha512-g9q04SwpG5ZtMnVkUcgcoAlwCH4YLROZN1qhyBwgkBzqYYVSYhpP6gSGaxGHwePLt1c+e6NqDlgIZN+e1/XPuA==} + + '@otplib/plugin-base32-scure@13.4.1': + resolution: {integrity: sha512-Fs/r5qisC05SRhT6xWXaypB6PVC0vgWf6zztmi0J5RnQ09OJiPDWCJFH6cDm6ANsrdvB9di7X+Jb7L13BoEbUA==} + + '@otplib/plugin-crypto-noble@13.4.1': + resolution: {integrity: sha512-PJfVW8/1hdS6CfxLheKPZSLTwDq4TijZbN4yRjxlv0ODdzmxpM+wGwWr1JXMdy0xJPxLziydQD5gdVqrR4/gAg==} + + '@otplib/totp@13.4.1': + resolution: {integrity: sha512-QOkBVPrf6AM4qZaReZPSk9/I8ATVdZpIISJz115MqeVtcrbcr5llPZ0J7804tpnjnp1vCRkI5Qjd47HhgVteBQ==} + + '@otplib/uri@13.4.1': + resolution: {integrity: sha512-xaIm7bvICMhoB2rZIR5luiaMdssWR5nY5nXnR1fdezUgZuEO58D6zrGzLp7pQuBmlpmL0HagnscDQFoskp9yiA==} + + '@scure/base@2.2.0': + resolution: {integrity: sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@26.1.2': + resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} + + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + agent-base@9.0.0: + resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} + engines: {node: '>= 20'} + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + archiver@8.0.0: + resolution: {integrity: sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g==} + engines: {node: '>=18'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.9.1: + resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.4: + resolution: {integrity: sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} + + bare-stream@2.13.3: + resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.6: + resolution: {integrity: sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + basic-ftp@5.3.1: + resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} + engines: {node: '>=10.0.0'} + + batchflow@0.4.0: + resolution: {integrity: sha512-XwQQoCGPUjdLWzmpAvRNZc91wnBYuKLmj52d9LLZ1Ww06ow5RBqBt8kUmU9/3ZvPq88j7Elh3V4cEhgNKXbIlQ==} + + bcrypt@6.0.0: + resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} + engines: {node: '>= 18'} + + better-sqlite3@12.11.1: + resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + blueimp-md5@2.19.0: + resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.19: + resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + compress-commons@7.0.1: + resolution: {integrity: sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==} + engines: {node: '>=18'} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@7.0.1: + resolution: {integrity: sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==} + engines: {node: '>=18'} + + data-uri-to-buffer@8.0.0: + resolution: {integrity: sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==} + engines: {node: '>= 20'} + + db-errors@0.2.3: + resolution: {integrity: sha512-OOgqgDuCavHXjYSJoV2yGhv6SeG8nk42aoCSoyXLZUH7VwFG27rxbavU1z+VrZbZjphw5UkDQwUlD21MwZpUng==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + degenerator@7.0.1: + resolution: {integrity: sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==} + engines: {node: '>= 20'} + peerDependencies: + quickjs-wasi: ^2.2.0 + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + email-validator@2.0.4: + resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==} + engines: {node: '>4.0'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + express-fileupload@1.5.2: + resolution: {integrity: sha512-wxUJn2vTHvj/kZCVmc5/bJO15C7aSMyHeuXYY3geKpeKibaAoQGcEv5+sM6nHS2T7VF+QHS4hTWPiY2mKofEdg==} + engines: {node: '>=12.0.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@2.0.0: + resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} + engines: {node: '>=4'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-uri@8.0.1: + resolution: {integrity: sha512-/5N/P4Lrh0p/mDwlDRi7Y1+P2o/OyzZI3l6Iz1Ov6XXwwm1y3RlZLuo3gVgML99djrEDtV980bBxSuOeHLk8ww==} + engines: {node: '>= 20'} + + getopts@2.3.0: + resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gravatar@1.8.2: + resolution: {integrity: sha512-GdRwLM3oYpFQKy47MKuluw9hZ2gaCtiKPbDGdcDEuYDKlc8eNnW27KYL9LVbIDzEsx88WtDWQm2ClBcsgBnj6w==} + engines: {node: '>=10'} + hasBin: true + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@9.1.0: + resolution: {integrity: sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==} + engines: {node: '>= 20'} + + https-proxy-agent@9.1.0: + resolution: {integrity: sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==} + engines: {node: '>= 20'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + interpret@2.2.0: + resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} + engines: {node: '>= 0.10'} + + ip-address@10.3.1: + resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + js-yaml@5.2.2: + resolution: {integrity: sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==} + hasBin: true + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + knex@3.2.10: + resolution: {integrity: sha512-oypTHfrc9i72iyxaUQBKHOxhcr0xM65MPf6FpN02nimsftXwzXprIkLjfXdubvhbu4PMWLp023q8o8CYvHSuZw==} + engines: {node: '>=16'} + hasBin: true + peerDependencies: + better-sqlite3: '*' + mysql: '*' + mysql2: '*' + pg: '*' + pg-native: '*' + pg-query-stream: ^4.14.0 + sqlite3: '*' + tedious: '*' + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + sqlite3: + optional: true + tedious: + optional: true + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + liquidjs@10.27.0: + resolution: {integrity: sha512-tw/OA59K7aIBlMKIrKlumr37fiZUheShVHXY8cVctWisgY1p9mc5hreOvlreoS0wTiwlWk14Ya7305c2a/Cg5w==} + engines: {node: '>=16'} + hasBin: true + + load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mysql2@3.23.2: + resolution: {integrity: sha512-fxh3HpQ8vJtu/Mmnd4Xsur19jGjHGzRLMxptiDtOkbX7EVBgnafGSGDx1WGGVmJLClVh2LeeBMMo24IFv8wCyQ==} + engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + netmask@2.1.1: + resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} + engines: {node: '>= 0.4.0'} + + node-abi@3.94.0: + resolution: {integrity: sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==} + engines: {node: '>=10'} + + node-addon-api@8.9.0: + resolution: {integrity: sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==} + engines: {node: ^18 || ^20 || >= 21} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-gyp@12.4.0: + resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + node-rsa@2.0.0: + resolution: {integrity: sha512-7yfzccmBd5rm7pALJf709tPwLL987WvdT7LC/QK5Oxa5S2aabdBPjxRDxhWKkbfCc9/FiC82lwSc+ulGZHH44g==} + engines: {node: '>=20'} + + nodemon@3.1.14: + resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} + engines: {node: '>=10'} + hasBin: true + + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + objection@3.1.5: + resolution: {integrity: sha512-Hx/ipAwXSuRBbOMWFKtRsAN0yITafqXtWB4OT4Z9wED7ty1h7bOnBdhLtcNus23GwLJqcMsRWdodL2p5GwlnfQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + knex: '>=1.0.1' + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + + otplib@13.4.1: + resolution: {integrity: sha512-o5CxfDw6bh7hoDv0NUUIcc0RqzJ9ipfUrzeKheKJ+vs4rXZnDlA9n4a/7R1cDjpmLjKLix4BgNVRmoDkm5rLSQ==} + + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + pac-proxy-agent@9.1.0: + resolution: {integrity: sha512-1aU+1mpj3DrQPfo3gh+3Gap3G5x+axnMx1P/y0ZF2ch7kb2meyOCAH8K2k9d27ROsTE7TnAerzxqF9aon2jqnA==} + engines: {node: '>= 20'} + + pac-resolver@9.0.1: + resolution: {integrity: sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==} + engines: {node: '>= 20'} + peerDependencies: + quickjs-wasi: ^2.2.0 + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + path@0.12.7: + resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pkg-conf@2.1.0: + resolution: {integrity: sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==} + engines: {node: '>=4'} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-agent-negotiate@1.1.0: + resolution: {integrity: sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==} + engines: {node: '>= 20'} + peerDependencies: + kerberos: ^2.0.0 + peerDependenciesMeta: + kerberos: + optional: true + + proxy-agent@8.0.2: + resolution: {integrity: sha512-idLLRewuemWd7GH/BDJzGiB0dWGfT2SQs3jy6NtZtGWU9uPTTSdeC1/cdbqLwgzhfv027daGFuXX426e2Eg20A==} + engines: {node: '>= 20'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + + quickjs-wasi@2.2.0: + resolution: {integrity: sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@3.0.0: + resolution: {integrity: sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==} + engines: {node: '>=18'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + signale@1.4.0: + resolution: {integrity: sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==} + engines: {node: '>=6'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@10.1.0: + resolution: {integrity: sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==} + engines: {node: '>= 20'} + + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sql-escaper@1.5.1: + resolution: {integrity: sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==} + engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} + + sqlite3@6.0.1: + resolution: {integrity: sha512-X0czUUMG2tmSqJpEQa3tCuZSHKIx8PwM53vLZzKp/o6Rpy25fiVfjdbnZ988M8+O3ZWR1ih0K255VumCb3MAnQ==} + engines: {node: '>=20.17.0'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + streamx@2.28.0: + resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + tarn@3.1.2: + resolution: {integrity: sha512-3RTvqKZcK/17jnJ8rMKFXbyNogywTs1z0gVPPwFsJGX46rkmUHOdIaSQ/aVO1rS7nH+soiXiWk7rvUXxndm8Dg==} + engines: {node: '>=8.0.0'} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + temp-dir@3.0.0: + resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} + engines: {node: '>=14.16'} + + temp-write@6.0.1: + resolution: {integrity: sha512-6bj9LlNld+knzEOQvnZK6YxiPF+foOUjvG/WoWj1/Mt9c6f2kQCPsh8KZ+NyTk0AejubTQSPpx2alcswE1bF8g==} + engines: {node: '>=18'} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + + tildify@2.0.0: + resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} + engines: {node: '>=8'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + undici@6.28.0: + resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==} + engines: {node: '>=18.17'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.10.4: + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + zip-stream@7.0.5: + resolution: {integrity: sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==} + engines: {node: '>=18'} + +snapshots: + + '@apidevtools/json-schema-ref-parser@14.0.1': + dependencies: + '@types/json-schema': 7.0.15 + js-yaml: 4.3.0 + + '@apidevtools/json-schema-ref-parser@15.5.1(@types/json-schema@7.0.15)': + dependencies: + '@types/json-schema': 7.0.15 + js-yaml: 5.2.2 + undici: 6.28.0 + + '@apidevtools/openapi-schemas@2.1.0': {} + + '@apidevtools/swagger-methods@3.0.2': {} + + '@apidevtools/swagger-parser@12.1.0(openapi-types@12.1.3)': + dependencies: + '@apidevtools/json-schema-ref-parser': 14.0.1 + '@apidevtools/openapi-schemas': 2.1.0 + '@apidevtools/swagger-methods': 3.0.2 + ajv: 8.20.0 + ajv-draft-04: 1.0.0(ajv@8.20.0) + call-me-maybe: 1.0.2 + openapi-types: 12.1.3 + + '@biomejs/biome@2.5.6': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.6 + '@biomejs/cli-darwin-x64': 2.5.6 + '@biomejs/cli-linux-arm64': 2.5.6 + '@biomejs/cli-linux-arm64-musl': 2.5.6 + '@biomejs/cli-linux-x64': 2.5.6 + '@biomejs/cli-linux-x64-musl': 2.5.6 + '@biomejs/cli-win32-arm64': 2.5.6 + '@biomejs/cli-win32-x64': 2.5.6 + + '@biomejs/cli-darwin-arm64@2.5.6': + optional: true + + '@biomejs/cli-darwin-x64@2.5.6': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.6': + optional: true + + '@biomejs/cli-linux-arm64@2.5.6': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.6': + optional: true + + '@biomejs/cli-linux-x64@2.5.6': + optional: true + + '@biomejs/cli-win32-arm64@2.5.6': + optional: true + + '@biomejs/cli-win32-x64@2.5.6': + optional: true + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@noble/hashes@2.2.0': {} + + '@otplib/core@13.4.1': {} + + '@otplib/hotp@13.4.1': + dependencies: + '@otplib/core': 13.4.1 + '@otplib/uri': 13.4.1 + + '@otplib/plugin-base32-scure@13.4.1': + dependencies: + '@otplib/core': 13.4.1 + '@scure/base': 2.2.0 + + '@otplib/plugin-crypto-noble@13.4.1': + dependencies: + '@noble/hashes': 2.2.0 + '@otplib/core': 13.4.1 + + '@otplib/totp@13.4.1': + dependencies: + '@otplib/core': 13.4.1 + '@otplib/hotp': 13.4.1 + '@otplib/uri': 13.4.1 + + '@otplib/uri@13.4.1': + dependencies: + '@otplib/core': 13.4.1 + + '@scure/base@2.2.0': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@26.1.2': + dependencies: + undici-types: 8.3.0 + + abbrev@4.0.0: + optional: true + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + agent-base@9.0.0: {} + + ajv-draft-04@1.0.0(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.4 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + archiver@8.0.0: + dependencies: + async: 3.2.6 + buffer-crc32: 1.0.0 + is-stream: 4.0.1 + lazystream: 1.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + readdir-glob: 3.0.0 + tar-stream: 3.2.0 + zip-stream: 7.0.5 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + argparse@2.0.1: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + async@3.2.6: {} + + aws-ssl-profiles@1.1.2: {} + + b4a@1.8.1: {} + + balanced-match@4.0.4: {} + + bare-events@2.9.1: {} + + bare-fs@4.7.4: + dependencies: + bare-events: 2.9.1 + bare-path: 3.1.1 + bare-stream: 2.13.3(bare-events@2.9.1) + bare-url: 2.4.6 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-path@3.1.1: {} + + bare-stream@2.13.3(bare-events@2.9.1): + dependencies: + b4a: 1.8.1 + streamx: 2.28.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.6: + dependencies: + bare-path: 3.1.1 + + base64-js@1.5.1: {} + + basic-ftp@5.3.1: {} + + batchflow@0.4.0: {} + + bcrypt@6.0.0: + dependencies: + node-addon-api: 8.9.0 + node-gyp-build: 4.8.4 + + better-sqlite3@12.11.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + blueimp-md5@2.19.0: {} + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3(supports-color@5.5.0) + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-crc32@1.0.0: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + call-me-maybe@1.0.2: {} + + camelcase@5.3.1: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@5.6.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + chownr@3.0.0: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + colorette@2.0.19: {} + + commander@10.0.1: {} + + compress-commons@7.0.1: + dependencies: + crc-32: 1.2.2 + crc32-stream: 7.0.1 + is-stream: 4.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + crc32-stream@7.0.1: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + + data-uri-to-buffer@8.0.0: {} + + db-errors@0.2.3: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.3.4: + dependencies: + ms: 2.1.2 + + debug@4.4.3(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + decamelize@1.2.0: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + degenerator@7.0.1(quickjs-wasi@2.2.0): + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + quickjs-wasi: 2.2.0 + + denque@2.1.0: {} + + depd@2.0.0: {} + + detect-libc@2.1.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + email-validator@2.0.4: {} + + emoji-regex@8.0.0: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + env-paths@2.2.1: + optional: true + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esm@3.2.25: {} + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + + expand-template@2.0.3: {} + + exponential-backoff@3.1.3: + optional: true + + express-fileupload@1.5.2: + dependencies: + busboy: 1.6.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-fifo@1.3.2: {} + + fast-uri@3.1.4: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + optional: true + + figures@2.0.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-uri-to-path@1.0.0: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@2.1.0: + dependencies: + locate-path: 2.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-uri@8.0.1: + dependencies: + basic-ftp: 5.3.1 + data-uri-to-buffer: 8.0.0 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + getopts@2.3.0: {} + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + gravatar@1.8.2: + dependencies: + blueimp-md5: 2.19.0 + email-validator: 2.0.4 + querystring: 0.2.0 + yargs: 15.4.1 + + has-flag@3.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@9.1.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@5.5.0) + proxy-agent-negotiate: 1.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + + https-proxy-agent@9.1.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@5.5.0) + proxy-agent-negotiate: 1.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + interpret@2.2.0: {} + + ip-address@10.3.1: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-promise@4.0.0: {} + + is-property@1.0.2: {} + + is-stream@4.0.1: {} + + isarray@1.0.0: {} + + isexe@4.0.0: + optional: true + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + js-yaml@5.2.2: + dependencies: + argparse: 2.0.1 + + json-parse-better-errors@1.0.2: {} + + json-schema-traverse@1.0.0: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + knex@3.2.10(better-sqlite3@12.11.1)(mysql2@3.23.2(@types/node@26.1.2))(pg@8.22.0)(sqlite3@6.0.1): + dependencies: + colorette: 2.0.19 + commander: 10.0.1 + debug: 4.3.4 + escalade: 3.2.0 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.18.1 + pg-connection-string: 2.6.2 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.1.2 + tildify: 2.0.0 + optionalDependencies: + better-sqlite3: 12.11.1 + mysql2: 3.23.2(@types/node@26.1.2) + pg: 8.22.0 + sqlite3: 6.0.1 + transitivePeerDependencies: + - supports-color + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + liquidjs@10.27.0: + dependencies: + commander: 10.0.1 + + load-json-file@4.0.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + + locate-path@2.0.0: + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + lodash@4.18.1: {} + + long@5.3.2: {} + + lru-cache@7.18.3: {} + + lru.min@1.1.4: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-response@3.1.0: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + mkdirp-classic@0.5.3: {} + + moment@2.30.1: {} + + ms@2.0.0: {} + + ms@2.1.2: {} + + ms@2.1.3: {} + + mysql2@3.23.2(@types/node@26.1.2): + dependencies: + '@types/node': 26.1.2 + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.3 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + sql-escaper: 1.5.1 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + + napi-build-utils@2.0.0: {} + + negotiator@0.6.4: {} + + negotiator@1.0.0: {} + + netmask@2.1.1: {} + + node-abi@3.94.0: + dependencies: + semver: 7.8.5 + + node-addon-api@8.9.0: {} + + node-gyp-build@4.8.4: {} + + node-gyp@12.4.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.8.5 + tar: 7.5.22 + tinyglobby: 0.2.17 + undici: 6.28.0 + which: 6.0.1 + optional: true + + node-rsa@2.0.0: + dependencies: + '@noble/hashes': 2.2.0 + + nodemon@3.1.14: + dependencies: + chokidar: 3.6.0 + debug: 4.4.3(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 10.2.6 + pstree.remy: 1.1.8 + semver: 7.8.5 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + optional: true + + normalize-path@3.0.0: {} + + object-inspect@1.13.4: {} + + objection@3.1.5(knex@3.2.10(better-sqlite3@12.11.1)(mysql2@3.23.2(@types/node@26.1.2))(pg@8.22.0)(sqlite3@6.0.1)): + dependencies: + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + db-errors: 0.2.3 + knex: 3.2.10(better-sqlite3@12.11.1)(mysql2@3.23.2(@types/node@26.1.2))(pg@8.22.0)(sqlite3@6.0.1) + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openapi-types@12.1.3: {} + + otplib@13.4.1: + dependencies: + '@otplib/core': 13.4.1 + '@otplib/hotp': 13.4.1 + '@otplib/plugin-base32-scure': 13.4.1 + '@otplib/plugin-crypto-noble': 13.4.1 + '@otplib/totp': 13.4.1 + '@otplib/uri': 13.4.1 + + p-limit@1.3.0: + dependencies: + p-try: 1.0.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@2.0.0: + dependencies: + p-limit: 1.3.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@1.0.0: {} + + p-try@2.2.0: {} + + pac-proxy-agent@9.1.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@5.5.0) + get-uri: 8.0.1 + http-proxy-agent: 9.1.0 + https-proxy-agent: 9.1.0 + pac-resolver: 9.0.1(quickjs-wasi@2.2.0) + quickjs-wasi: 2.2.0 + socks-proxy-agent: 10.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + + pac-resolver@9.0.1(quickjs-wasi@2.2.0): + dependencies: + degenerator: 7.0.1(quickjs-wasi@2.2.0) + netmask: 2.1.1 + quickjs-wasi: 2.2.0 + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.4 + json-parse-better-errors: 1.0.2 + + parseurl@1.3.3: {} + + path-exists@3.0.0: {} + + path-exists@4.0.0: {} + + path-parse@1.0.7: {} + + path-to-regexp@8.4.2: {} + + path@0.12.7: + dependencies: + process: 0.11.10 + util: 0.10.4 + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-connection-string@2.6.2: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picomatch@2.3.2: {} + + picomatch@4.0.5: + optional: true + + pify@3.0.0: {} + + pkg-conf@2.1.0: + dependencies: + find-up: 2.1.0 + load-json-file: 4.0.0 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.94.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.5 + tunnel-agent: 0.6.0 + + proc-log@6.1.0: + optional: true + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-agent-negotiate@1.1.0: {} + + proxy-agent@8.0.2: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@5.5.0) + http-proxy-agent: 9.1.0 + https-proxy-agent: 9.1.0 + lru-cache: 7.18.3 + pac-proxy-agent: 9.1.0 + proxy-from-env: 2.1.0 + socks-proxy-agent: 10.1.0 + transitivePeerDependencies: + - kerberos + - supports-color + + proxy-from-env@2.1.0: {} + + pstree.remy@1.1.8: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + querystring@0.2.0: {} + + quickjs-wasi@2.2.0: {} + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@3.0.0: + dependencies: + minimatch: 10.2.6 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + rechoir@0.8.0: + dependencies: + resolve: 1.22.12 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-main-filename@2.0.0: {} + + resolve-from@5.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + router@2.2.0: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + semver@7.8.5: {} + + send@1.2.1: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signale@1.4.0: + dependencies: + chalk: 2.4.2 + figures: 2.0.0 + pkg-conf: 2.1.0 + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.8.5 + + smart-buffer@4.2.0: {} + + socks-proxy-agent@10.1.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3(supports-color@5.5.0) + socks: 2.8.9 + transitivePeerDependencies: + - supports-color + + socks@2.8.9: + dependencies: + ip-address: 10.3.1 + smart-buffer: 4.2.0 + + source-map@0.6.1: + optional: true + + split2@4.2.0: {} + + sql-escaper@1.5.1: {} + + sqlite3@6.0.1: + dependencies: + bindings: 1.5.0 + node-addon-api: 8.9.0 + prebuild-install: 7.1.3 + tar: 7.5.22 + optionalDependencies: + node-gyp: 12.4.0 + + statuses@2.0.2: {} + + streamsearch@1.1.0: {} + + streamx@2.28.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-json-comments@2.0.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.7.4 + fast-fifo: 1.3.2 + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + tarn@3.1.2: {} + + teex@1.0.1: + dependencies: + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + temp-dir@3.0.0: {} + + temp-write@6.0.1: + dependencies: + graceful-fs: 4.2.11 + is-stream: 4.0.1 + temp-dir: 3.0.0 + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + + tildify@2.0.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + optional: true + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + touch@3.1.1: {} + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + + undefsafe@2.0.5: {} + + undici-types@8.3.0: {} + + undici@6.28.0: {} + + unpipe@1.0.0: {} + + util-deprecate@1.0.2: {} + + util@0.10.4: + dependencies: + inherits: 2.0.3 + + vary@1.1.2: {} + + which-module@2.0.1: {} + + which@6.0.1: + dependencies: + isexe: 4.0.0 + optional: true + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + yallist@5.0.0: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + zip-stream@7.0.5: + dependencies: + compress-commons: 7.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 diff --git a/backend/routes/nginx/proxy_hosts.js b/backend/routes/nginx/proxy_hosts.js index 7045a195cc..609785addf 100644 --- a/backend/routes/nginx/proxy_hosts.js +++ b/backend/routes/nginx/proxy_hosts.js @@ -70,6 +70,42 @@ router } }); +/** + * Preview a Proxy Host candidate. This route is static so it must be declared + * before /:host_id and never writes the active nginx directory. + */ +router.options("/nginx-config/preview", (_, res) => res.sendStatus(204)); +router.post("/nginx-config/preview", jwtdecode(), async (req, res, next) => { + try { + const result = await internalProxyHost.previewNginxConfig(res.locals.access, req.body); + res.set({ "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }).status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } +}); + +/** + * Read deployed/failed artifacts after normal object visibility enforcement. + */ +router + .route("/:host_id/nginx-config") + .options((_, res) => res.sendStatus(204)) + .all(jwtdecode()) + .get(async (req, res, next) => { + try { + const hostId = Number.parseInt(req.params.host_id, 10); + if (!Number.isInteger(hostId)) throw new Error("Invalid Proxy Host id"); + const includeContent = + typeof req.query.include_content === "string" ? req.query.include_content.split(",") : []; + const result = await internalProxyHost.getNginxArtifacts(res.locals.access, hostId, includeContent); + res.set({ "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" }).status(200).send(result); + } catch (err) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`); + next(err); + } + }); + /** * Specific proxy-host * diff --git a/backend/schema/components/proxy-host-object.json b/backend/schema/components/proxy-host-object.json index 3ac6462136..3d54e5d8a2 100644 --- a/backend/schema/components/proxy-host-object.json +++ b/backend/schema/components/proxy-host-object.json @@ -1,178 +1,249 @@ -{ - "type": "object", - "description": "Proxy Host object", - "required": [ - "id", - "created_on", - "modified_on", - "owner_user_id", - "domain_names", - "forward_host", - "forward_port", - "access_list_id", - "certificate_id", - "ssl_forced", - "caching_enabled", - "block_exploits", - "advanced_config", - "meta", - "allow_websocket_upgrade", - "http2_support", - "forward_scheme", - "enabled", - "locations", - "hsts_enabled", - "hsts_subdomains", - "trust_forwarded_proto" - ], - "properties": { - "id": { - "$ref": "../common.json#/properties/id" - }, - "created_on": { - "$ref": "../common.json#/properties/created_on" - }, - "modified_on": { - "$ref": "../common.json#/properties/modified_on" - }, - "owner_user_id": { - "$ref": "../common.json#/properties/user_id" - }, - "domain_names": { - "$ref": "../common.json#/properties/domain_names" - }, - "forward_host": { - "type": "string", - "minLength": 1, - "maxLength": 255, - "example": "127.0.0.1" - }, - "forward_port": { - "type": "integer", - "minimum": 1, - "maximum": 65535, - "example": 8080 - }, - "access_list_id": { - "$ref": "../common.json#/properties/access_list_id" - }, - "certificate_id": { - "$ref": "../common.json#/properties/certificate_id" - }, - "ssl_forced": { - "$ref": "../common.json#/properties/ssl_forced" - }, - "caching_enabled": { - "$ref": "../common.json#/properties/caching_enabled" - }, - "block_exploits": { - "$ref": "../common.json#/properties/block_exploits" - }, - "advanced_config": { - "type": "string", - "example": "" - }, - "meta": { - "type": "object", - "example": { - "nginx_online": true, - "nginx_err": null - } - }, - "allow_websocket_upgrade": { - "description": "Allow Websocket Upgrade for all paths", - "type": "boolean", - "example": true - }, - "http2_support": { - "$ref": "../common.json#/properties/http2_support" - }, - "forward_scheme": { - "type": "string", - "enum": ["http", "https"], - "example": "http" - }, - "enabled": { - "$ref": "../common.json#/properties/enabled" - }, - "locations": { - "type": "array", - "minItems": 0, - "items": { - "type": "object", - "required": ["forward_scheme", "forward_host", "forward_port", "path"], - "additionalProperties": false, - "properties": { - "id": { - "type": ["integer", "null"] - }, - "path": { - "type": "string", - "minLength": 1 - }, - "forward_scheme": { - "$ref": "#/properties/forward_scheme" - }, - "forward_host": { - "$ref": "#/properties/forward_host" - }, - "forward_port": { - "$ref": "#/properties/forward_port" - }, - "forward_path": { - "type": "string" - }, - "advanced_config": { - "type": "string" - } - } - }, - "example": [ - { - "path": "/app", - "forward_scheme": "http", - "forward_host": "example.com", - "forward_port": 80 - } - ] - }, - "hsts_enabled": { - "$ref": "../common.json#/properties/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "../common.json#/properties/hsts_subdomains" - }, - "trust_forwarded_proto":{ - "type": "boolean", - "description": "Trust the forwarded headers", - "example": false - }, - "certificate": { - "oneOf": [ - { - "type": "null", - "example": null - }, - { - "$ref": "./certificate-object.json" - } - ], - "example": null - }, - "owner": { - "$ref": "./user-object.json" - }, - "access_list": { - "oneOf": [ - { - "type": "null", - "example": null - }, - { - "$ref": "./access-list-object.json" - } - ], - "example": null - } - } -} +{ + "type": "object", + "description": "Proxy Host object", + "required": [ + "id", + "created_on", + "modified_on", + "owner_user_id", + "domain_names", + "forward_host", + "forward_port", + "access_list_id", + "certificate_id", + "ssl_forced", + "caching_enabled", + "block_exploits", + "advanced_config", + "meta", + "allow_websocket_upgrade", + "http2_support", + "forward_scheme", + "enabled", + "locations", + "hsts_enabled", + "hsts_subdomains", + "trust_forwarded_proto" + ], + "properties": { + "id": { + "$ref": "../common.json#/properties/id" + }, + "created_on": { + "$ref": "../common.json#/properties/created_on" + }, + "modified_on": { + "$ref": "../common.json#/properties/modified_on" + }, + "owner_user_id": { + "$ref": "../common.json#/properties/user_id" + }, + "domain_names": { + "description": "Domain Names separated by a comma. May be empty when nginx_config.listener.mode is port.", + "type": "array", + "minItems": 0, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[^&| @!#%^();:/\\}{=+?<>,~`'\"]+$" + }, + "example": ["example.com", "www.example.com"] + }, + "forward_host": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "example": "127.0.0.1" + }, + "forward_port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "example": 8080 + }, + "access_list_id": { + "$ref": "../common.json#/properties/access_list_id" + }, + "certificate_id": { + "$ref": "../common.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../common.json#/properties/ssl_forced" + }, + "caching_enabled": { + "$ref": "../common.json#/properties/caching_enabled" + }, + "block_exploits": { + "$ref": "../common.json#/properties/block_exploits" + }, + "advanced_config": { + "type": "string", + "example": "" + }, + "meta": { + "type": "object", + "example": { + "nginx_online": true, + "nginx_err": null + } + }, + "allow_websocket_upgrade": { + "description": "Allow Websocket Upgrade for all paths", + "type": "boolean", + "example": true + }, + "http2_support": { + "$ref": "../common.json#/properties/http2_support" + }, + "forward_scheme": { + "type": "string", + "enum": [ + "http", + "https" + ], + "example": "http" + }, + "enabled": { + "$ref": "../common.json#/properties/enabled" + }, + "locations": { + "type": "array", + "minItems": 0, + "items": { + "type": "object", + "required": [ + "forward_scheme", + "forward_host", + "forward_port", + "path" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": [ + "integer", + "null" + ] + }, + "path": { + "type": "string", + "minLength": 1 + }, + "forward_scheme": { + "$ref": "#/properties/forward_scheme" + }, + "forward_host": { + "$ref": "#/properties/forward_host" + }, + "forward_port": { + "$ref": "#/properties/forward_port" + }, + "forward_path": { + "type": "string" + }, + "advanced_config": { + "type": "string" + } + } + }, + "example": [ + { + "path": "/app", + "forward_scheme": "http", + "forward_host": "example.com", + "forward_port": 80 + } + ] + }, + "hsts_enabled": { + "$ref": "../common.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../common.json#/properties/hsts_subdomains" + }, + "trust_forwarded_proto": { + "type": "boolean", + "description": "Trust the forwarded headers", + "example": false + }, + "certificate": { + "oneOf": [ + { + "type": "null", + "example": null + }, + { + "$ref": "./certificate-object.json" + } + ], + "example": null + }, + "owner": { + "$ref": "./user-object.json" + }, + "access_list": { + "oneOf": [ + { + "type": "null", + "example": null + }, + { + "$ref": "./access-list-object.json" + } + ], + "example": null + }, + "nginx_config": { + "type": "object", + "additionalProperties": true, + "description": "Structured Nginx configuration schema v1", + "example": { + "schema_version": 1, + "server": {} + } + }, + "nginx_config_revision": { + "type": "integer", + "minimum": 1, + "example": 1 + }, + "nginx_applied_revision": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "example": 1 + }, + "nginx_applied_enabled": { + "type": "boolean", + "example": true + }, + "nginx_applied_hash": { + "type": [ + "string", + "null" + ], + "pattern": "^sha256:[a-f0-9]{64}$" + }, + "nginx_deployment_status": { + "type": "string", + "enum": [ + "pending", + "online", + "disabled", + "degraded", + "error", + "recovering" + ] + }, + "nginx_checked_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json index fc3198456b..dd7bceca17 100644 --- a/backend/schema/paths/nginx/proxy-hosts/hostID/put.json +++ b/backend/schema/paths/nginx/proxy-hosts/hostID/put.json @@ -1,154 +1,168 @@ -{ - "operationId": "updateProxyHost", - "summary": "Update a Proxy Host", - "tags": ["proxy-hosts"], - "security": [ - { - "bearerAuth": [ - "proxy_hosts.manage" - ] - } - ], - "parameters": [ - { - "in": "path", - "name": "hostID", - "description": "The ID of the Proxy Host", - "schema": { - "type": "integer", - "minimum": 1 - }, - "required": true, - "example": 2 - } - ], - "requestBody": { - "description": "Proxy Host Payload", - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": false, - "minProperties": 1, - "properties": { - "domain_names": { - "$ref": "../../../../components/proxy-host-object.json#/properties/domain_names" - }, - "forward_scheme": { - "$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme" - }, - "forward_host": { - "$ref": "../../../../components/proxy-host-object.json#/properties/forward_host" - }, - "forward_port": { - "$ref": "../../../../components/proxy-host-object.json#/properties/forward_port" - }, - "certificate_id": { - "$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id" - }, - "ssl_forced": { - "$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced" - }, - "hsts_enabled": { - "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains" - }, - "trust_forwarded_proto": { - "$ref": "../../../../components/proxy-host-object.json#/properties/trust_forwarded_proto" - }, - "http2_support": { - "$ref": "../../../../components/proxy-host-object.json#/properties/http2_support" - }, - "block_exploits": { - "$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits" - }, - "caching_enabled": { - "$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled" - }, - "allow_websocket_upgrade": { - "$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" - }, - "access_list_id": { - "$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id" - }, - "advanced_config": { - "$ref": "../../../../components/proxy-host-object.json#/properties/advanced_config" - }, - "enabled": { - "$ref": "../../../../components/proxy-host-object.json#/properties/enabled" - }, - "meta": { - "$ref": "../../../../components/proxy-host-object.json#/properties/meta" - }, - "locations": { - "$ref": "../../../../components/proxy-host-object.json#/properties/locations" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "200 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 3, - "created_on": "2025-10-30T01:12:05.000Z", - "modified_on": "2025-10-30T01:17:06.000Z", - "owner_user_id": 1, - "domain_names": [ - "test.example.com" - ], - "forward_host": "127.0.0.1", - "forward_port": 8080, - "access_list_id": 0, - "certificate_id": 0, - "ssl_forced": false, - "caching_enabled": false, - "block_exploits": false, - "advanced_config": "", - "meta": { - "nginx_online": true, - "nginx_err": null - }, - "allow_websocket_upgrade": false, - "http2_support": false, - "forward_scheme": "http", - "enabled": true, - "locations": [], - "hsts_enabled": false, - "hsts_subdomains": false, - "trust_forwarded_proto": false, - "owner": { - "id": 1, - "created_on": "2025-10-28T00:50:24.000Z", - "modified_on": "2025-10-28T00:50:24.000Z", - "is_disabled": false, - "email": "jc@jc21.com", - "name": "jamiec", - "nickname": "jamiec", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - }, - "certificate": null, - "access_list": null - } - } - }, - "schema": { - "$ref": "../../../../components/proxy-host-object.json" - } - } - } - } - } -} +{ + "operationId": "updateProxyHost", + "summary": "Update a Proxy Host", + "tags": [ + "proxy-hosts" + ], + "security": [ + { + "bearerAuth": [ + "proxy_hosts.manage" + ] + } + ], + "parameters": [ + { + "in": "path", + "name": "hostID", + "description": "The ID of the Proxy Host", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "example": 2 + } + ], + "requestBody": { + "description": "Proxy Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "domain_names": { + "$ref": "../../../../components/proxy-host-object.json#/properties/domain_names" + }, + "forward_scheme": { + "$ref": "../../../../components/proxy-host-object.json#/properties/forward_scheme" + }, + "forward_host": { + "$ref": "../../../../components/proxy-host-object.json#/properties/forward_host" + }, + "forward_port": { + "$ref": "../../../../components/proxy-host-object.json#/properties/forward_port" + }, + "certificate_id": { + "$ref": "../../../../components/proxy-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../../components/proxy-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../../components/proxy-host-object.json#/properties/hsts_subdomains" + }, + "trust_forwarded_proto": { + "$ref": "../../../../components/proxy-host-object.json#/properties/trust_forwarded_proto" + }, + "http2_support": { + "$ref": "../../../../components/proxy-host-object.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../../../../components/proxy-host-object.json#/properties/block_exploits" + }, + "caching_enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/caching_enabled" + }, + "allow_websocket_upgrade": { + "$ref": "../../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" + }, + "access_list_id": { + "$ref": "../../../../components/proxy-host-object.json#/properties/access_list_id" + }, + "advanced_config": { + "$ref": "../../../../components/proxy-host-object.json#/properties/advanced_config" + }, + "enabled": { + "$ref": "../../../../components/proxy-host-object.json#/properties/enabled" + }, + "meta": { + "$ref": "../../../../components/proxy-host-object.json#/properties/meta" + }, + "locations": { + "$ref": "../../../../components/proxy-host-object.json#/properties/locations" + }, + "nginx_config": { + "$ref": "../../../../components/proxy-host-object.json#/properties/nginx_config" + }, + "base_revision": { + "type": "integer", + "minimum": 1 + }, + "preview_token": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 3, + "created_on": "2025-10-30T01:12:05.000Z", + "modified_on": "2025-10-30T01:17:06.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_host": "127.0.0.1", + "forward_port": 8080, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "advanced_config": "", + "meta": { + "nginx_online": true, + "nginx_err": null + }, + "allow_websocket_upgrade": false, + "http2_support": false, + "forward_scheme": "http", + "enabled": true, + "locations": [], + "hsts_enabled": false, + "hsts_subdomains": false, + "trust_forwarded_proto": false, + "owner": { + "id": 1, + "created_on": "2025-10-28T00:50:24.000Z", + "modified_on": "2025-10-28T00:50:24.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "jamiec", + "nickname": "jamiec", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": [ + "admin" + ] + }, + "certificate": null, + "access_list": null + } + } + }, + "schema": { + "$ref": "../../../../components/proxy-host-object.json" + } + } + } + } + } +} diff --git a/backend/schema/paths/nginx/proxy-hosts/post.json b/backend/schema/paths/nginx/proxy-hosts/post.json index 28ddad8fc2..60b110cb1f 100644 --- a/backend/schema/paths/nginx/proxy-hosts/post.json +++ b/backend/schema/paths/nginx/proxy-hosts/post.json @@ -1,151 +1,165 @@ -{ - "operationId": "createProxyHost", - "summary": "Create a Proxy Host", - "tags": ["proxy-hosts"], - "security": [ - { - "bearerAuth": [ - "proxy_hosts.manage" - ] - } - ], - "requestBody": { - "description": "Proxy Host Payload", - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": false, - "required": [ - "domain_names", - "forward_scheme", - "forward_host", - "forward_port" - ], - "properties": { - "domain_names": { - "$ref": "../../../components/proxy-host-object.json#/properties/domain_names" - }, - "forward_scheme": { - "$ref": "../../../components/proxy-host-object.json#/properties/forward_scheme" - }, - "forward_host": { - "$ref": "../../../components/proxy-host-object.json#/properties/forward_host" - }, - "forward_port": { - "$ref": "../../../components/proxy-host-object.json#/properties/forward_port" - }, - "certificate_id": { - "$ref": "../../../components/proxy-host-object.json#/properties/certificate_id" - }, - "ssl_forced": { - "$ref": "../../../components/proxy-host-object.json#/properties/ssl_forced" - }, - "hsts_enabled": { - "$ref": "../../../components/proxy-host-object.json#/properties/hsts_enabled" - }, - "hsts_subdomains": { - "$ref": "../../../components/proxy-host-object.json#/properties/hsts_subdomains" - }, - "trust_forwarded_proto": { - "$ref": "../../../components/proxy-host-object.json#/properties/trust_forwarded_proto" - }, - "http2_support": { - "$ref": "../../../components/proxy-host-object.json#/properties/http2_support" - }, - "block_exploits": { - "$ref": "../../../components/proxy-host-object.json#/properties/block_exploits" - }, - "caching_enabled": { - "$ref": "../../../components/proxy-host-object.json#/properties/caching_enabled" - }, - "allow_websocket_upgrade": { - "$ref": "../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" - }, - "access_list_id": { - "$ref": "../../../components/proxy-host-object.json#/properties/access_list_id" - }, - "advanced_config": { - "$ref": "../../../components/proxy-host-object.json#/properties/advanced_config" - }, - "enabled": { - "$ref": "../../../components/proxy-host-object.json#/properties/enabled" - }, - "meta": { - "$ref": "../../../components/proxy-host-object.json#/properties/meta" - }, - "locations": { - "$ref": "../../../components/proxy-host-object.json#/properties/locations" - } - } - }, - "example": { - "domain_names": [ - "test.example.com" - ], - "forward_scheme": "http", - "forward_host": "127.0.0.1", - "forward_port": 8080 - } - } - } - }, - "responses": { - "201": { - "description": "201 response", - "content": { - "application/json": { - "examples": { - "default": { - "value": { - "id": 3, - "created_on": "2025-10-30T01:12:05.000Z", - "modified_on": "2025-10-30T01:12:05.000Z", - "owner_user_id": 1, - "domain_names": [ - "test.example.com" - ], - "forward_host": "127.0.0.1", - "forward_port": 8080, - "access_list_id": 0, - "certificate_id": 0, - "ssl_forced": false, - "caching_enabled": false, - "block_exploits": false, - "advanced_config": "", - "meta": {}, - "allow_websocket_upgrade": false, - "http2_support": false, - "forward_scheme": "http", - "enabled": true, - "locations": [], - "hsts_enabled": false, - "hsts_subdomains": false, - "trust_forwarded_proto": false, - "certificate": null, - "owner": { - "id": 1, - "created_on": "2025-10-28T00:50:24.000Z", - "modified_on": "2025-10-28T00:50:24.000Z", - "is_disabled": false, - "email": "jc@jc21.com", - "name": "jamiec", - "nickname": "jamiec", - "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", - "roles": [ - "admin" - ] - }, - "access_list": null - } - } - }, - "schema": { - "$ref": "../../../components/proxy-host-object.json" - } - } - } - } - } -} +{ + "operationId": "createProxyHost", + "summary": "Create a Proxy Host", + "tags": [ + "proxy-hosts" + ], + "security": [ + { + "bearerAuth": [ + "proxy_hosts.manage" + ] + } + ], + "requestBody": { + "description": "Proxy Host Payload", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": [ + "domain_names", + "forward_scheme", + "forward_host", + "forward_port" + ], + "properties": { + "domain_names": { + "$ref": "../../../components/proxy-host-object.json#/properties/domain_names" + }, + "forward_scheme": { + "$ref": "../../../components/proxy-host-object.json#/properties/forward_scheme" + }, + "forward_host": { + "$ref": "../../../components/proxy-host-object.json#/properties/forward_host" + }, + "forward_port": { + "$ref": "../../../components/proxy-host-object.json#/properties/forward_port" + }, + "certificate_id": { + "$ref": "../../../components/proxy-host-object.json#/properties/certificate_id" + }, + "ssl_forced": { + "$ref": "../../../components/proxy-host-object.json#/properties/ssl_forced" + }, + "hsts_enabled": { + "$ref": "../../../components/proxy-host-object.json#/properties/hsts_enabled" + }, + "hsts_subdomains": { + "$ref": "../../../components/proxy-host-object.json#/properties/hsts_subdomains" + }, + "trust_forwarded_proto": { + "$ref": "../../../components/proxy-host-object.json#/properties/trust_forwarded_proto" + }, + "http2_support": { + "$ref": "../../../components/proxy-host-object.json#/properties/http2_support" + }, + "block_exploits": { + "$ref": "../../../components/proxy-host-object.json#/properties/block_exploits" + }, + "caching_enabled": { + "$ref": "../../../components/proxy-host-object.json#/properties/caching_enabled" + }, + "allow_websocket_upgrade": { + "$ref": "../../../components/proxy-host-object.json#/properties/allow_websocket_upgrade" + }, + "access_list_id": { + "$ref": "../../../components/proxy-host-object.json#/properties/access_list_id" + }, + "advanced_config": { + "$ref": "../../../components/proxy-host-object.json#/properties/advanced_config" + }, + "enabled": { + "$ref": "../../../components/proxy-host-object.json#/properties/enabled" + }, + "meta": { + "$ref": "../../../components/proxy-host-object.json#/properties/meta" + }, + "locations": { + "$ref": "../../../components/proxy-host-object.json#/properties/locations" + }, + "nginx_config": { + "$ref": "../../../components/proxy-host-object.json#/properties/nginx_config" + }, + "base_revision": { + "type": "integer", + "minimum": 1 + }, + "preview_token": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + } + }, + "example": { + "domain_names": [ + "test.example.com" + ], + "forward_scheme": "http", + "forward_host": "127.0.0.1", + "forward_port": 8080 + } + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "examples": { + "default": { + "value": { + "id": 3, + "created_on": "2025-10-30T01:12:05.000Z", + "modified_on": "2025-10-30T01:12:05.000Z", + "owner_user_id": 1, + "domain_names": [ + "test.example.com" + ], + "forward_host": "127.0.0.1", + "forward_port": 8080, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": false, + "advanced_config": "", + "meta": {}, + "allow_websocket_upgrade": false, + "http2_support": false, + "forward_scheme": "http", + "enabled": true, + "locations": [], + "hsts_enabled": false, + "hsts_subdomains": false, + "trust_forwarded_proto": false, + "certificate": null, + "owner": { + "id": 1, + "created_on": "2025-10-28T00:50:24.000Z", + "modified_on": "2025-10-28T00:50:24.000Z", + "is_disabled": false, + "email": "jc@jc21.com", + "name": "jamiec", + "nickname": "jamiec", + "avatar": "//www.gravatar.com/avatar/6193176330f8d38747f038c170ddb193?default=mm", + "roles": [ + "admin" + ] + }, + "access_list": null + } + } + }, + "schema": { + "$ref": "../../../components/proxy-host-object.json" + } + } + } + } + } +} diff --git a/backend/templates/proxy_host.conf b/backend/templates/proxy_host.conf index d23ca46fa2..bd91df9102 100644 --- a/backend/templates/proxy_host.conf +++ b/backend/templates/proxy_host.conf @@ -25,6 +25,10 @@ proxy_http_version 1.1; access_log /data/logs/proxy-host-{{ id }}_access.log proxy; error_log /data/logs/proxy-host-{{ id }}_error.log warn; +{% if nginx_options %} +{{ nginx_options }} +{% endif %} + {{ advanced_config }} {{ locations }} @@ -43,7 +47,9 @@ proxy_http_version 1.1; {% endif %} # Proxy! - include conf.d/include/proxy.conf; +{% if use_managed_request_headers %} add_header X-Served-By $host; + proxy_pass $forward_scheme://$server:$port$request_uri;{% else %} include conf.d/include/proxy.conf;{% endif %}{{ "\n" }} +{% if use_managed_request_headers %}{{ managed_nginx_location_options | prepend: " " | replace: "\n", "\n " }}{% elsif nginx_options %}{{ nginx_options | prepend: " " | replace: "\n", "\n " }}{% endif %}{{ "\n" }} } {% endif %} diff --git a/backend/test/nginx/config-renderer.test.js b/backend/test/nginx/config-renderer.test.js new file mode 100644 index 0000000000..933e04991b --- /dev/null +++ b/backend/test/nginx/config-renderer.test.js @@ -0,0 +1,536 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { scanAdvancedConfig } from "../../internal/nginx-config-diagnostics.js"; +import { canonicalize, hashCanonical } from "../../internal/nginx-config-hash.js"; +import { normalizeLocation, normalizeProxyHost } from "../../internal/nginx-config-normalizer.js"; +import { buildProxyHostCandidate } from "../../internal/nginx-config-renderer.js"; + +test("HASH-001 canonical JSON sorts object keys but retains array order", () => { + assert.equal(canonicalize({ z: [2, 1], a: { y: true, x: null } }), '{"a":{"x":null,"y":true},"z":[2,1]}'); + assert.equal(hashCanonical({ b: 1, a: 2 }), hashCanonical({ a: 2, b: 1 })); + assert.notEqual(hashCanonical([1, 2]), hashCanonical([2, 1])); + assert.throws(() => canonicalize({ value: undefined }), /undefined/); +}); + +test("PREVIEW-003 dependency hashing is stable across API foreign-key coercion", async () => { + const host = { + id: 42, + enabled: true, + domain_names: ["hash.example.com"], + forward_scheme: "http", + forward_host: "127.0.0.1", + forward_port: 8080, + access_list_id: 0, + certificate_id: 0, + locations: [], + nginx_config: { schema_version: 1, server: {} }, + }; + const preview = await buildProxyHostCandidate({ host }); + const save = await buildProxyHostCandidate({ host: { ...host, certificate_id: "0" } }); + assert.equal(save.dependencyHash, preview.dependencyHash); + assert.equal(save.payloadHash, preview.payloadHash); +}); + +test("LOC truth table validates match/path combinations", () => { + assert.equal( + normalizeLocation({ + path: "/api/", + forward_scheme: "http", + forward_host: "example.com", + forward_port: 80, + match_type: "prefix", + path_mode: "strip_prefix", + }).path_mode, + "strip_prefix", + ); + assert.throws( + () => + normalizeLocation({ + path: "/api", + forward_scheme: "http", + forward_host: "example.com", + forward_port: 80, + match_type: "prefix", + path_mode: "strip_prefix", + }), + /slash-terminated/, + ); + assert.throws( + () => + normalizeLocation({ + path: "/a", + forward_scheme: "http", + forward_host: "example.com", + forward_port: 80, + match_type: "exact", + path_mode: "replace_prefix", + forward_path: "/", + }), + /only support preserve_uri/, + ); + assert.equal( + normalizeLocation({ + path: "^/a", + forward_scheme: "http", + forward_host: "example.com", + forward_port: 80, + match_type: "regex", + path_mode: "preserve_uri", + }).match_type, + "regex", + ); +}); + +test("HDR duplicate and protected headers are rejected", () => { + assert.throws( + () => + normalizeProxyHost({ + domain_names: ["headers.example.com"], + nginx_config: { + schema_version: 1, + server: { + request_headers: [ + { name: "X-Test", value: "a" }, + { name: "x-test", value: "b" }, + ], + }, + }, + }), + /duplicate/, + ); + assert.throws( + () => + normalizeProxyHost({ + domain_names: ["headers.example.com"], + nginx_config: { + schema_version: 1, + server: { request_headers: [{ name: "Host", operation: "remove" }] }, + }, + }), + /managed by the system/, + ); +}); + +test("HDR-001 managed Host override emits a single upstream Host header", async () => { + const candidate = await buildProxyHostCandidate({ + host: { + id: 24, + enabled: true, + domain_names: ["headers.example.com"], + forward_scheme: "http", + forward_host: "127.0.0.1", + forward_port: 8080, + locations: [], + nginx_config: { + schema_version: 1, + server: { + request_headers: [{ name: "Host", operation: "set", value: "localhost" }], + }, + }, + }, + }); + + const defaultLocation = candidate.config.match(/location \/ \{([\s\S]*?)\n {2}\}/)?.[1] ?? ""; + assert.doesNotMatch(defaultLocation, /include conf\.d\/include\/proxy\.conf;/); + assert.match(defaultLocation, /proxy_pass \$forward_scheme:\/\/\$server:\$port\$request_uri;/); + assert.equal((defaultLocation.match(/proxy_set_header Host /g) ?? []).length, 1); + assert.match(defaultLocation, /proxy_set_header Host "localhost";/); +}); + +test("OPT-001 all structured proxy options normalize and render with nginx-compatible durations", async () => { + const host = { + id: 14, + enabled: true, + domain_names: ["all-options.example.com"], + forward_scheme: "http", + forward_host: "127.0.0.1", + forward_port: 8080, + access_list_id: 0, + certificate_id: 0, + ssl_forced: false, + caching_enabled: false, + block_exploits: false, + allow_websocket_upgrade: true, + http2_support: false, + hsts_enabled: false, + hsts_subdomains: false, + trust_forwarded_proto: false, + advanced_config: "", + locations: [ + { + path: "/api/", + forward_scheme: "https", + forward_host: "api.example.com", + forward_port: 8443, + match_type: "priority_prefix", + path_mode: "replace_prefix", + forward_path: "/v1/", + nginx_config: { + proxy_connect_timeout: "10s", + proxy_buffering: false, + proxy_request_buffering: false, + proxy_ssl_server_name: false, + }, + }, + ], + nginx_config: { + schema_version: 1, + server: { + default_location_enabled: true, + proxy_connect_timeout: "60", + proxy_send_timeout: "2m", + proxy_read_timeout: "500ms", + client_max_body_size: "64m", + proxy_buffer_size: "8k", + proxy_busy_buffers_size: "32k", + proxy_buffers: [8, "16k"], + proxy_buffering: true, + proxy_request_buffering: true, + proxy_ssl_server_name: true, + request_headers: [ + { name: "X-Request-Id", operation: "set", value_mode: "variable", value: "$request_id" }, + { name: "X-Debug", operation: "add", value: "enabled" }, + { name: "X-Remove-Me", operation: "remove" }, + ], + response_headers: [ + { name: "X-Frame-Options", operation: "set", value: "DENY" }, + { name: "X-Trace", operation: "add", value_mode: "variable", value: "$request_id" }, + ], + hide_response_headers: ["X-Powered-By", "X-AspNet-Version"], + }, + }, + }; + + const normalized = normalizeProxyHost(host); + assert.equal(normalized.nginx_options.proxy_connect_timeout, "60"); + assert.equal(normalized.locations[0].nginx_config.proxy_connect_timeout, "10s"); + assert.throws( + () => + normalizeProxyHost({ + ...host, + nginx_config: { schema_version: 1, server: { proxy_connect_timeout: "1.5s" } }, + }), + /whole nginx duration/, + ); + + const candidate = await buildProxyHostCandidate({ host }); + const inheritedLocationCandidate = await buildProxyHostCandidate({ + host: { + ...host, + locations: [{ ...host.locations[0], nginx_config: {} }], + }, + }); + assert.match( + inheritedLocationCandidate.config, + /location \^~ \/api\/ \{[\s\S]*?proxy_connect_timeout 60;[\s\S]*?proxy_buffering on;/, + ); + + for (const directive of [ + "client_max_body_size 64m;", + "proxy_connect_timeout 60;", + "proxy_send_timeout 2m;", + "proxy_read_timeout 500ms;", + "proxy_buffer_size 8k;", + "proxy_busy_buffers_size 32k;", + "proxy_buffers 8 16k;", + "proxy_buffering on;", + "proxy_request_buffering on;", + "proxy_ssl_server_name on;", + 'proxy_set_header X-Debug "enabled";', + "proxy_set_header X-Request-Id $request_id;", + 'proxy_set_header X-Remove-Me "";', + 'add_header X-Frame-Options "DENY" always;', + "add_header X-Trace $request_id always;", + "proxy_hide_header x-aspnet-version;", + "proxy_hide_header x-powered-by;", + "proxy_connect_timeout 10s;", + "proxy_buffering off;", + "proxy_request_buffering off;", + "proxy_ssl_server_name off;", + ]) { + assert.match(candidate.config, new RegExp(directive.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + } +}); + +test("OPT-002 expanded proxy module controls normalize and render", async () => { + const host = { + id: 15, + enabled: true, + domain_names: ["proxy-module.example.com"], + forward_scheme: "https", + forward_host: "origin.example.com", + forward_port: 443, + access_list_id: 0, + certificate_id: 0, + ssl_forced: false, + caching_enabled: false, + block_exploits: false, + allow_websocket_upgrade: false, + http2_support: false, + hsts_enabled: false, + hsts_subdomains: false, + trust_forwarded_proto: false, + advanced_config: "", + locations: [], + nginx_config: { + schema_version: 1, + server: { + proxy_http_version: "1.1", + proxy_method: "POST", + proxy_pass_request_headers: false, + proxy_pass_request_body: false, + proxy_pass_trailers: true, + proxy_ignore_client_abort: true, + proxy_socket_keepalive: true, + proxy_bind: "192.0.2.10", + proxy_next_upstream: ["error", "timeout", "http_502", "http_429", "non_idempotent"], + proxy_next_upstream_timeout: "15s", + proxy_next_upstream_tries: 3, + proxy_max_temp_file_size: "256m", + proxy_temp_file_write_size: "32k", + proxy_limit_rate: "128k", + proxy_headers_hash_bucket_size: 64, + proxy_headers_hash_max_size: 512, + proxy_intercept_errors: true, + proxy_force_ranges: true, + proxy_redirect: "off", + proxy_cookie_domain: [{ from: "origin.example.com", to: "public.example.com" }], + proxy_cookie_path: [{ from: "/app", to: "/" }], + proxy_pass_headers: ["Date", "Server"], + proxy_ignore_headers: ["X-Accel-Redirect", "Set-Cookie"], + proxy_ssl_server_name: true, + proxy_ssl_name: "origin.example.com", + proxy_ssl_verify: true, + proxy_ssl_verify_depth: 2, + proxy_ssl_session_reuse: false, + proxy_ssl_protocols: ["TLSv1.2", "TLSv1.3"], + proxy_ssl_ciphers: "HIGH:!aNULL", + }, + }, + }; + + const candidate = await buildProxyHostCandidate({ host }); + for (const directive of [ + "proxy_http_version 1.1;", + "proxy_method POST;", + "proxy_pass_request_headers off;", + "proxy_pass_request_body off;", + "proxy_pass_trailers on;", + "proxy_ignore_client_abort on;", + "proxy_socket_keepalive on;", + "proxy_bind 192.0.2.10;", + "proxy_next_upstream error timeout http_502 http_429 non_idempotent;", + "proxy_next_upstream_timeout 15s;", + "proxy_next_upstream_tries 3;", + "proxy_max_temp_file_size 256m;", + "proxy_temp_file_write_size 32k;", + "proxy_limit_rate 128k;", + "proxy_headers_hash_bucket_size 64;", + "proxy_headers_hash_max_size 512;", + "proxy_intercept_errors on;", + "proxy_force_ranges on;", + "proxy_redirect off;", + "proxy_cookie_domain origin.example.com public.example.com;", + "proxy_cookie_path /app /;", + "proxy_pass_header Date;", + "proxy_pass_header Server;", + "proxy_ignore_headers X-Accel-Redirect Set-Cookie;", + "proxy_ssl_name origin.example.com;", + "proxy_ssl_server_name on;", + "proxy_ssl_verify on;", + "proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;", + "proxy_ssl_verify_depth 2;", + "proxy_ssl_session_reuse off;", + "proxy_ssl_protocols TLSv1.2 TLSv1.3;", + "proxy_ssl_ciphers HIGH:!aNULL;", + ]) { + assert.match(candidate.config, new RegExp(directive.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + } + assert.throws( + () => + normalizeProxyHost({ + ...host, + nginx_config: { schema_version: 1, server: { proxy_next_upstream: ["off", "timeout"] } }, + }), + /only off/, + ); + assert.throws( + () => normalizeProxyHost({ ...host, nginx_config: { schema_version: 1, server: { proxy_bind: "not an ip" } } }), + /IP address/, + ); + assert.throws( + () => + normalizeProxyHost({ + ...host, + nginx_config: { schema_version: 1, server: { proxy_headers_hash_bucket_size: 0 } }, + }), + /at least 1/, + ); +}); +test("ADV lexer ignores comments and strings but diagnoses managed directives", () => { + assert.equal( + scanAdvancedConfig('# proxy_pass http://ignored;\nset $x "location / {";').filter( + (item) => item.severity === "error", + ).length, + 0, + ); + assert.equal(scanAdvancedConfig("proxy_pass http://example.com;").at(0).code, "ADVANCED_MANAGED_DIRECTIVE"); + for (const directive of [ + "proxy_http_version 1.1;", + "proxy_pass_request_body off;", + "proxy_buffer_size 8k;", + "proxy_redirect off;", + "proxy_cookie_domain backend.example public.example;", + "proxy_ssl_verify on;", + ]) { + const diagnostic = scanAdvancedConfig(directive).at(0); + assert.equal(diagnostic.code, "ADVANCED_STRUCTURED_CONFLICT", directive); + assert.equal(diagnostic.severity, "warning", directive); + } +}); + +test("REN-001/002 renderer is immutable and deterministic", async () => { + const host = { + id: 12, + enabled: true, + domain_names: ["example.com"], + forward_scheme: "http", + forward_host: "127.0.0.1", + forward_port: 3000, + access_list_id: 0, + certificate_id: 0, + ssl_forced: false, + caching_enabled: false, + block_exploits: true, + allow_websocket_upgrade: false, + http2_support: false, + hsts_enabled: false, + hsts_subdomains: false, + trust_forwarded_proto: false, + advanced_config: "", + locations: [ + { + path: "/api/", + forward_scheme: "http", + forward_host: "example.com", + forward_port: 8080, + match_type: "priority_prefix", + path_mode: "replace_prefix", + forward_path: "/v1/", + }, + ], + nginx_config: { schema_version: 1, server: { proxy_read_timeout: "30s" } }, + }; + const before = structuredClone(host); + const first = await buildProxyHostCandidate({ host }); + const second = await buildProxyHostCandidate({ host }); + assert.deepEqual(host, before); + assert.equal(first.config, second.config); + assert.equal(first.configHash, second.configHash); + assert.match(first.config, /location \^~ \/api\//); + assert.match(first.config, /proxy_pass http:\/\/example\.com:8080\/v1\//); +}); + +test("renderer can omit the managed default location", async () => { + const host = { + id: 13, + enabled: true, + domain_names: ["locations-only.example.com"], + forward_scheme: "http", + forward_host: "127.0.0.1", + forward_port: 80, + access_list_id: 0, + certificate_id: 0, + ssl_forced: false, + caching_enabled: false, + block_exploits: false, + allow_websocket_upgrade: false, + http2_support: false, + hsts_enabled: false, + hsts_subdomains: false, + trust_forwarded_proto: false, + advanced_config: "", + locations: [ + { + path: "/api/", + forward_scheme: "http", + forward_host: "api.example.com", + forward_port: 8080, + match_type: "priority_prefix", + path_mode: "preserve_uri", + }, + ], + nginx_config: { schema_version: 1, server: { default_location_enabled: false } }, + }; + + const candidate = await buildProxyHostCandidate({ host }); + + assert.doesNotMatch(candidate.config, /^\s*location \/\s*\{/m); + assert.equal(candidate.config.match(/\bproxy_pass\b/g)?.length, 1); + assert.match(candidate.config, /proxy_pass http:\/\/api\.example\.com:8080;/); + assert.match(candidate.config, /proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;/); +}); + +test("listener validation keeps domain semantics and protects reserved NPM ports", () => { + const baseHost = { + domain_names: ["app.example.com"], + forward_scheme: "http", + forward_host: "127.0.0.1", + forward_port: 3000, + nginx_config: { schema_version: 1, server: {} }, + }; + assert.throws( + () => normalizeProxyHost({ ...baseHost, domain_names: [] }), + /domain listener requires at least one domain_name/, + ); + assert.throws( + () => + normalizeProxyHost({ + ...baseHost, + domain_names: [], + nginx_config: { schema_version: 1, listener: { mode: "port", port: 80 } }, + }), + /reserved by Nginx Proxy Manager/, + ); + assert.throws( + () => + normalizeProxyHost({ + ...baseHost, + nginx_config: { schema_version: 1, listener: { mode: "port", port: 18080 } }, + }), + /port listener cannot include domain_names/, + ); +}); + +test("port-only listener renders a standalone HTTP default server", async () => { + const candidate = await buildProxyHostCandidate({ + host: { + id: 14, + enabled: true, + domain_names: [], + forward_scheme: "http", + forward_host: "127.0.0.1", + forward_port: 9000, + access_list_id: 0, + certificate_id: 0, + ssl_forced: false, + caching_enabled: false, + block_exploits: false, + allow_websocket_upgrade: false, + http2_support: false, + hsts_enabled: false, + hsts_subdomains: false, + trust_forwarded_proto: false, + advanced_config: "", + locations: [], + nginx_config: { schema_version: 1, listener: { mode: "port", port: 18080 }, server: {} }, + }, + }); + assert.match(candidate.config, /^\s*listen 18080 default_server;/m); + assert.match(candidate.config, /^\s*server_name _;/m); + assert.doesNotMatch(candidate.config, /^\s*listen 80;/m); + assert.doesNotMatch(candidate.config, /^\s*listen 443 ssl;/m); + assert.match(candidate.config, /set \$forward_scheme http;/); + assert.match(candidate.config, /set \$server {9}"127\.0\.0\.1";/); + assert.match(candidate.config, /set \$port {11}9000;/); +}); diff --git a/backend/test/nginx/config-validator.test.js b/backend/test/nginx/config-validator.test.js new file mode 100644 index 0000000000..f8017e5125 --- /dev/null +++ b/backend/test/nginx/config-validator.test.js @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { candidateArtifactPath } from "../../internal/nginx-config-artifacts.js"; +import { validateInMirror } from "../../internal/nginx-config-validator.js"; + +const temporaryRoot = async () => fs.mkdtemp(join(os.tmpdir(), "npm-nginx-validator-")); + +test("validation mirror is created outside nginxRoot and receives the candidate artifact", async (context) => { + const nginxRoot = await temporaryRoot(); + context.after(() => fs.rm(nginxRoot, { recursive: true, force: true })); + const targetPath = join(nginxRoot, "proxy_host", "2.conf"); + const candidatePath = candidateArtifactPath("proxy_host", 2, "operation-2", nginxRoot); + const nginxPrefix = join(nginxRoot, "nginx-prefix"); + const nginxConfigPath = join(nginxRoot, "test-nginx.conf"); + await fs.mkdir(join(nginxPrefix, "conf.d", "include"), { recursive: true }); + await fs.writeFile(join(nginxPrefix, "conf.d", "include", "shared.conf"), "shared config\n"); + await fs.mkdir(join(targetPath, ".."), { recursive: true }); + await fs.mkdir(join(candidatePath, ".."), { recursive: true }); + await fs.writeFile(targetPath, "old config\n"); + await fs.writeFile(candidatePath, "candidate config\n"); + await fs.writeFile(nginxConfigPath, `include ${nginxRoot}/proxy_host/*.conf;\n`); + + let generatedMaster = null; + const result = await validateInMirror({ + nginxRoot, + nginxConfigPath, + nginxPrefix, + operationId: "operation-2", + candidatePath, + targetPath, + commandRunner: async (_command, args) => { + assert.equal(args[args.indexOf("-g") + 1], "error_log /dev/null crit;"); + generatedMaster = args[args.indexOf("-c") + 1]; + const mirror = join(generatedMaster, ".."); + assert.equal( + await fs.readFile(join(mirror, "nginx", "proxy_host", "2.conf"), "utf8"), + "candidate config\n", + ); + assert.match(await fs.readFile(generatedMaster, "utf8"), /\.nginx-validation-/); + return { stdout: "", stderr: "" }; + }, + }); + + assert.equal(result.valid, true); + assert.ok(generatedMaster); + await assert.rejects(() => fs.access(generatedMaster), { code: "ENOENT" }); +}); diff --git a/backend/test/nginx/deployment-coordinator.test.js b/backend/test/nginx/deployment-coordinator.test.js new file mode 100644 index 0000000000..d1fe398804 --- /dev/null +++ b/backend/test/nginx/deployment-coordinator.test.js @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { NginxDeploymentCoordinator, deriveDeploymentStatus } from "../../internal/nginx-deployment-coordinator.js"; +import { activeArtifactPath, readArtifact } from "../../internal/nginx-config-artifacts.js"; + +const rendered = (config) => ({ config, configHash: `sha256:${config}`, payloadHash: "sha256:payload", dependencyHash: "sha256:deps", templateVersion: "test", templateHash: "sha256:template", capabilityHash: "sha256:cap", diagnostics: [], snapshot: {} }); + +const temporaryRoot = async () => fs.mkdtemp(join(os.tmpdir(), "npm-nginx-coordinator-")); + +test("DEP-007 rollback preserves a prior active artifact when reload fails", async (context) => { + const root = await temporaryRoot(); + context.after(() => fs.rm(root, { recursive: true, force: true })); + const active = activeArtifactPath("proxy_host", 7, root); + await fs.mkdir(join(active, ".."), { recursive: true }); + await fs.writeFile(active, "old config\n"); + let reloads = 0; + const coordinator = new NginxDeploymentCoordinator({ + nginxRoot: root, + renderCandidate: async () => rendered("new config\n"), + validator: async () => ({ valid: true, stdout: "", stderr: "", validation_scope: "full" }), + commandRunner: async (_, args) => { + if (args.includes("reload")) throw new Error("reload failed"); + reloads += 1; + return { stdout: "", stderr: "" }; + }, + }); + await assert.rejects(() => coordinator.deploy({ hostType: "proxy_host", host: { id: 7 } }), /reload failed/); + assert.equal(await readArtifact(active), "old config\n"); + assert.ok(reloads >= 2, "rollback must validate/reload the restored active config"); +}); + +test("DEP-003 validation failure never swaps the active artifact", async (context) => { + const root = await temporaryRoot(); + context.after(() => fs.rm(root, { recursive: true, force: true })); + const active = activeArtifactPath("proxy_host", 8, root); + await fs.mkdir(join(active, ".."), { recursive: true }); + await fs.writeFile(active, "old config\n"); + const coordinator = new NginxDeploymentCoordinator({ nginxRoot: root, renderCandidate: async () => rendered("new config\n"), validator: async () => ({ valid: false, stderr: "invalid" }), commandRunner: async () => ({ stdout: "", stderr: "" }) }); + await assert.rejects(() => coordinator.deploy({ hostType: "proxy_host", host: { id: 8 } }), /invalid/); + assert.equal(await readArtifact(active), "old config\n"); +}); + +test("deployment status has one derivation order", () => { + assert.equal(deriveDeploymentStatus({ enabled: true, nginx_applied_enabled: true, nginx_config_revision: 2, nginx_applied_revision: 2, nginx_applied_hash: "sha256:a", active_hash: "sha256:a" }), "online"); + assert.equal(deriveDeploymentStatus({ enabled: true, nginx_applied_enabled: true, nginx_config_revision: 2, nginx_applied_revision: 1, nginx_applied_hash: "sha256:a", active_hash: "sha256:a" }), "degraded"); + assert.equal(deriveDeploymentStatus({ enabled: false, nginx_applied_enabled: false, nginx_config_revision: 2, nginx_applied_revision: 2 }), "disabled"); +}); + +test("DEP-006 removal validates before swap and restores active config when reload fails", async (context) => { + const root = await temporaryRoot(); + context.after(() => fs.rm(root, { recursive: true, force: true })); + const active = activeArtifactPath("proxy_host", 9, root); + await fs.mkdir(join(active, ".."), { recursive: true }); + await fs.writeFile(active, "old config\n"); + const coordinator = new NginxDeploymentCoordinator({ + nginxRoot: root, + validator: async () => ({ valid: true, stdout: "", stderr: "", validation_scope: "full" }), + commandRunner: async (_, args) => { + if (args.includes("reload")) throw new Error("reload failed"); + return { stdout: "", stderr: "" }; + }, + }); + await assert.rejects(() => coordinator.remove({ hostType: "proxy_host", host: { id: 9 } }), /reload failed/); + assert.equal(await readArtifact(active), "old config\n"); +}); + +test("DEP-008 recovery restores an interrupted swapped artifact", async (context) => { + const root = await temporaryRoot(); + context.after(() => fs.rm(root, { recursive: true, force: true })); + const active = activeArtifactPath("proxy_host", 10, root); + const backup = join(root, ".deploy", "backups", "operation-10", "proxy_host", "10.conf"); + await fs.mkdir(join(backup, ".."), { recursive: true }); + await fs.writeFile(backup, "old config\n"); + await fs.mkdir(join(root, ".deploy", "journal"), { recursive: true }); + await fs.writeFile(join(root, ".deploy", "journal", "operation-10.json"), JSON.stringify({ operation_id: "operation-10", phase: "swapped", target: active, backup, had_active: true })); + const coordinator = new NginxDeploymentCoordinator({ nginxRoot: root, commandRunner: async () => ({ stdout: "", stderr: "" }) }); + const recovered = await coordinator.recover(); + assert.deepEqual(recovered, [{ operationId: "operation-10", state: "recovered_rollback" }]); + assert.equal(await readArtifact(active), "old config\n"); +}); diff --git a/backend/test/nginx/fixtures/legacy-default-proxy-host.conf b/backend/test/nginx/fixtures/legacy-default-proxy-host.conf new file mode 100644 index 0000000000..577ee74d4b --- /dev/null +++ b/backend/test/nginx/fixtures/legacy-default-proxy-host.conf @@ -0,0 +1,77 @@ +# ------------------------------------------------------------ +# legacy.example.test +# ------------------------------------------------------------ + + + +map $scheme $hsts_header { + https "max-age=63072000; preload"; +} + +server { + set $forward_scheme http; + set $server "127.0.0.1"; + set $port 8080; + + listen 80; +listen [::]:80; + + + server_name legacy.example.test; +http2 off; + + + + + + + # Block Exploits + include conf.d/include/block-exploits.conf; + + + + + + + + access_log /data/logs/proxy-host-101_access.log proxy; + error_log /data/logs/proxy-host-101_error.log warn; + + +proxy_set_header Host $host; +proxy_set_header X-Forwarded-For $remote_addr; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Scheme $scheme; +proxy_set_header X-Real-IP $remote_addr; + + + + + + + + + location / { + + + + + + + + + # Proxy! + include conf.d/include/proxy.conf; + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Real-IP $remote_addr; + + } + + + # Custom + include /data/nginx/custom/server_proxy[.]conf; +} diff --git a/backend/test/nginx/fixtures/legacy-default-proxy-host.json b/backend/test/nginx/fixtures/legacy-default-proxy-host.json new file mode 100644 index 0000000000..af3a809011 --- /dev/null +++ b/backend/test/nginx/fixtures/legacy-default-proxy-host.json @@ -0,0 +1,21 @@ +{ + "id": 101, + "enabled": true, + "domain_names": ["legacy.example.test"], + "forward_scheme": "http", + "forward_host": "127.0.0.1", + "forward_port": 8080, + "access_list_id": 0, + "certificate_id": 0, + "ssl_forced": false, + "caching_enabled": false, + "block_exploits": true, + "allow_websocket_upgrade": false, + "http2_support": false, + "hsts_enabled": false, + "hsts_subdomains": false, + "trust_forwarded_proto": false, + "advanced_config": "", + "locations": [], + "nginx_config": { "schema_version": 1 } +} diff --git a/backend/test/nginx/legacy-golden.test.js b/backend/test/nginx/legacy-golden.test.js new file mode 100644 index 0000000000..5dd6234e7b --- /dev/null +++ b/backend/test/nginx/legacy-golden.test.js @@ -0,0 +1,14 @@ +import { readFile } from "node:fs/promises"; +import assert from "node:assert/strict"; +import test from "node:test"; +import { buildProxyHostCandidate } from "../../internal/nginx-config-renderer.js"; + +const fixture = new URL("./fixtures/legacy-default-proxy-host.json", import.meta.url); +const golden = new URL("./fixtures/legacy-default-proxy-host.conf", import.meta.url); + +test("GOLDEN-001 untouched legacy-compatible proxy host renders the reviewed baseline", async () => { + const host = JSON.parse(await readFile(fixture, "utf8")); + const expected = await readFile(golden, "utf8"); + const actual = await buildProxyHostCandidate({ host }); + assert.equal(actual.config, expected); +}); diff --git a/backend/test/nginx/migrations.test.js b/backend/test/nginx/migrations.test.js new file mode 100644 index 0000000000..112a3652e2 --- /dev/null +++ b/backend/test/nginx/migrations.test.js @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import knex from "knex"; +import test from "node:test"; +import { down as downDeployment, up as upDeployment } from "../../migrations/20260731120100_nginx_deployment.js"; +import { down as downProxyHost, up as upProxyHost } from "../../migrations/20260731120000_proxy_host_nginx_desired_applied.js"; + +const externalClient = process.env.NPM_MIGRATION_TEST_CLIENT; +const connection = externalClient === "mysql2" + ? { host: process.env.NPM_MIGRATION_MYSQL_HOST || "127.0.0.1", port: Number(process.env.NPM_MIGRATION_MYSQL_PORT || 3306), user: process.env.NPM_MIGRATION_MYSQL_USER || "npm", password: process.env.NPM_MIGRATION_MYSQL_PASSWORD || "npmpass", database: process.env.NPM_MIGRATION_MYSQL_DATABASE || "npm" } + : externalClient === "pg" + ? { host: process.env.NPM_MIGRATION_POSTGRES_HOST || "127.0.0.1", port: Number(process.env.NPM_MIGRATION_POSTGRES_PORT || 5432), user: process.env.NPM_MIGRATION_POSTGRES_USER || "npm", password: process.env.NPM_MIGRATION_POSTGRES_PASSWORD || "npmpass", database: process.env.NPM_MIGRATION_POSTGRES_DATABASE || "npm" } + : null; + +const client = externalClient || "better-sqlite3"; +let sqliteBindingAvailable = true; +if (!externalClient) { + try { + const BetterSqlite = (await import("better-sqlite3")).default; + const probe = new BetterSqlite(":memory:"); + probe.close(); + } catch { + sqliteBindingAvailable = false; + } +} + +const setupDatabase = async () => { + const directory = connection ? null : await mkdtemp(join(tmpdir(), "npm-nginx-migration-")); + const database = knex({ client, connection: connection || { filename: join(directory, "migration.sqlite") }, useNullAsDefault: !connection }); + await database.schema.dropTableIfExists("nginx_deployment"); + await database.schema.dropTableIfExists("proxy_host"); + await database.schema.createTable("proxy_host", (table) => { + table.increments("id").primary(); + table.boolean("enabled").notNullable().defaultTo(true); + }); + await database("proxy_host").insert({ enabled: true }); + return { database, directory }; +}; + +test(`MIG-001 desired/applied schema migrates and rolls back on ${client}`, { skip: !sqliteBindingAvailable && !externalClient ? "better-sqlite3 native binding is unavailable in this host runtime" : false }, async () => { + const { database, directory } = await setupDatabase(); + try { + await upProxyHost(database); + await upDeployment(database); + const proxyColumns = await database("proxy_host").columnInfo(); + for (const name of ["nginx_config", "nginx_config_revision", "nginx_applied_revision", "nginx_applied_enabled", "nginx_applied_hash", "nginx_deployment_status", "nginx_checked_at", "nginx_last_error", "nginx_applied_snapshot", "nginx_last_deployment_id"]) assert.ok(proxyColumns[name], `${name} exists`); + const row = await database("proxy_host").first(); + assert.equal(row.nginx_config_revision, 1); + assert.equal(row.nginx_deployment_status, "pending"); + assert.ok(await database.schema.hasTable("nginx_deployment")); + await downDeployment(database); + await downProxyHost(database); + assert.equal(await database.schema.hasTable("nginx_deployment"), false); + const rolledBack = await database("proxy_host").columnInfo(); + assert.equal(rolledBack.nginx_config, undefined); + } finally { + await database.destroy(); + if (directory) await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/backend/test/nginx/preview-token.test.js b/backend/test/nginx/preview-token.test.js new file mode 100644 index 0000000000..c955328fbe --- /dev/null +++ b/backend/test/nginx/preview-token.test.js @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { issuePreviewToken, verifyPreviewToken } from "../../internal/nginx-preview-token.js"; + +test("PREVIEW-001 token binds host, base revision, and rendered input hashes", () => { + const token = issuePreviewToken({ + hostId: 42, + baseRevision: 7, + payloadHash: "payload", + dependencyHash: "dependencies", + templateHash: "template", + capabilityHash: "capability", + }); + const verified = verifyPreviewToken(token, { host_id: 42, base_revision: 7 }); + assert.equal(verified.valid, true); + assert.equal(verified.data.payload_hash, "payload"); + assert.equal(verifyPreviewToken(token, { host_id: 43 }).reason, "host_id"); +}); + +test("PREVIEW-002 token rejects a tampered signature and expired body", () => { + const token = issuePreviewToken({ hostId: 1, baseRevision: 1, payloadHash: "a", dependencyHash: "b", templateHash: "c", capabilityHash: "d" }); + assert.equal(verifyPreviewToken(`${token}x`).valid, false); + const expired = issuePreviewToken({ hostId: 1, baseRevision: 1, payloadHash: "a", dependencyHash: "b", templateHash: "c", capabilityHash: "d", expiresInSeconds: -1 }); + assert.equal(verifyPreviewToken(expired).reason, "expired"); +}); diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 56875fd849..d3c1ccc0c0 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -1,4 +1,5 @@ FROM nginxproxymanager/testca AS testca +FROM shinsenter/s6-overlay:v3.2.3.0 AS s6-overlay FROM nginxproxymanager/nginx-full:certbot-node LABEL maintainer="Jamie Curnow " @@ -22,13 +23,18 @@ RUN echo "fs.file-max = 65535" > /etc/sysctl.conf \ WORKDIR /usr RUN curl -sL 'https://taskfile.dev/install.sh' | sh +# The S6 overlay is copied from a Docker Hub build-stage image rather than downloaded +# from GitHub during every local Docker build. This keeps the development stack +# buildable when Docker Desktop cannot reach GitHub directly. +COPY --from=s6-overlay /init /init +COPY --from=s6-overlay /command /command +COPY --from=s6-overlay /package /package +COPY --from=s6-overlay /etc/s6-overlay /etc/s6-overlay + COPY rootfs / -COPY scripts/install-s6 /tmp/install-s6 RUN rm -f /etc/nginx/conf.d/production.conf.template \ && chmod 644 /etc/logrotate.d/nginx-proxy-manager \ - && /tmp/install-s6 "${TARGETPLATFORM}" \ - && rm -f /tmp/install-s6 \ && chmod 644 -R /root/.cache # Copy test CA cert for use in container and add to trusted certs from a self-signed CA diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index d6b07ec012..801082dd5e 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -47,7 +47,9 @@ services: - le_data:/etc/letsencrypt - "./dev/resolv.conf:/etc/resolv.conf:ro" - ../backend:/app + - backend_node_modules:/app/node_modules - ../frontend:/frontend + - frontend_node_modules:/frontend/node_modules - "/etc/localtime:/etc/localtime:ro" healthcheck: test: ["CMD", "/usr/bin/check-health"] @@ -254,6 +256,10 @@ services: - authentik volumes: + backend_node_modules: + name: npm2dev_backend_node_modules + frontend_node_modules: + name: npm2dev_frontend_node_modules npm_data: name: npm2dev_core_data le_data: diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run index b140257b75..0d8b2ce9d2 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/backend/run @@ -10,6 +10,10 @@ cd /app || exit 1 log_info 'Starting backend ...' if [ "${DEVELOPMENT:-}" = 'true' ]; then + # Keep container dependencies isolated from host node_modules. This is + # especially important when the host is Windows and the container is Linux. + mkdir -p /app/node_modules + chown -R "$PUID:$PGID" /app/node_modules s6-setuidgid "$PUID:$PGID" yarn install exec s6-setuidgid "$PUID:$PGID" bash -c "export HOME=$NPMHOME;export CERTBOT_VERSION=$CERTBOT_VERSION;node --max_old_space_size=250 --abort_on_uncaught_exception node_modules/nodemon/bin/nodemon.js" else diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run index 91ed3fa805..b999cd0f59 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/frontend/run @@ -10,8 +10,8 @@ if [ "$DEVELOPMENT" = 'true' ]; then cd /frontend || exit 1 HOME=$NPMHOME export HOME - mkdir -p /frontend/dist - chown -R "$PUID:$PGID" /frontend/dist + mkdir -p /frontend/dist /frontend/node_modules + chown -R "$PUID:$PGID" /frontend/dist /frontend/node_modules log_info 'Starting frontend ...' s6-setuidgid "$PUID:$PGID" yarn install diff --git a/docker/scripts/install-s6 b/docker/scripts/install-s6 index 55513fe1e7..54cc5e017e 100755 --- a/docker/scripts/install-s6 +++ b/docker/scripts/install-s6 @@ -24,8 +24,13 @@ esac echo -e "${BLUE}❯ ${CYAN}Installing S6-overlay v${S6_OVERLAY_VERSION} for ${YELLOW}${TARGETPLATFORM} (${S6_ARCH})${RESET}" -curl -L -o '/tmp/s6-overlay-noarch.tar.xz' "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" -curl -L -o "/tmp/s6-overlay-${S6_ARCH}.tar.xz" "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" +if [ ! -s '/tmp/s6-overlay-noarch.tar.xz' ]; then + curl --fail --location --retry 5 --retry-all-errors --retry-delay 3 -o '/tmp/s6-overlay-noarch.tar.xz' "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" +fi + +if [ ! -s "/tmp/s6-overlay-${S6_ARCH}.tar.xz" ]; then + curl --fail --location --retry 5 --retry-all-errors --retry-delay 3 -o "/tmp/s6-overlay-${S6_ARCH}.tar.xz" "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" +fi tar -C / -Jxpf '/tmp/s6-overlay-noarch.tar.xz' tar -C / -Jxpf "/tmp/s6-overlay-${S6_ARCH}.tar.xz" diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000000..64c659901b --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,3 @@ +auto-install-peers=true +registry=https://registry.npmmirror.com +strict-ssl=false diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000000..0de9595909 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,3779 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tabler/core': + specifier: ^1.4.0 + version: 1.4.0 + '@tabler/icons-react': + specifier: ^3.44.0 + version: 3.46.0(react@19.2.8) + '@tanstack/react-query': + specifier: ^5.101.0 + version: 5.101.4(react@19.2.8) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@uiw/react-textarea-code-editor': + specifier: ^3.1.1 + version: 3.1.1(@babel/runtime@7.29.7)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + classnames: + specifier: ^2.5.1 + version: 2.5.1 + country-flag-icons: + specifier: ^1.6.17 + version: 1.6.20 + date-fns: + specifier: ^4.4.0 + version: 4.4.0 + ez-modal-react: + specifier: ^1.0.5 + version: 1.0.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3) + formik: + specifier: ^2.4.9 + version: 2.4.9(@types/react@19.2.18)(react@19.2.8) + generate-password-browser: + specifier: ^1.1.0 + version: 1.1.0 + humps: + specifier: ^2.0.1 + version: 2.0.1 + query-string: + specifier: ^9.4.0 + version: 9.4.1 + react: + specifier: ^19.2.7 + version: 19.2.8 + react-bootstrap: + specifier: ^2.10.10 + version: 2.10.10(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-dom: + specifier: ^19.2.7 + version: 19.2.8(react@19.2.8) + react-intl: + specifier: ^10.1.11 + version: 10.1.18(@types/react@19.2.18)(react@19.2.8) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.18)(react@19.2.8) + react-router-dom: + specifier: ^7.16.0 + version: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-select: + specifier: ^5.10.2 + version: 5.10.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-toastify: + specifier: ^11.1.0 + version: 11.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + rooks: + specifier: ^9.8.0 + version: 9.8.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + devDependencies: + '@biomejs/biome': + specifier: ^2.4.15 + version: 2.5.6 + '@formatjs/cli': + specifier: ^6.16.6 + version: 6.16.15 + '@tanstack/react-query-devtools': + specifier: ^5.100.14 + version: 5.101.4(@tanstack/react-query@5.101.4(react@19.2.8))(react@19.2.8) + '@testing-library/dom': + specifier: ^10.4.1 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.10.0(@testing-library/dom@10.4.1) + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@types/country-flag-icons': + specifier: ^1.2.2 + version: 1.2.2 + '@types/humps': + specifier: ^2.0.6 + version: 2.0.6 + '@types/node': + specifier: ^25.9.1 + version: 25.9.5 + '@types/react': + specifier: ^19.2.16 + version: 19.2.18 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.4(@types/react@19.2.18) + '@types/react-table': + specifier: ^7.7.20 + version: 7.7.20 + '@vitejs/plugin-react': + specifier: ^6.0.2 + version: 6.0.5(vite@8.2.0(@types/node@25.9.5)(sass@1.102.0)) + happy-dom: + specifier: ^20.9.0 + version: 20.11.1 + postcss: + specifier: ^8.5.15 + version: 8.5.25 + postcss-simple-vars: + specifier: ^7.0.1 + version: 7.0.1(postcss@8.5.25) + sass: + specifier: ^1.100.0 + version: 1.102.0 + tmp: + specifier: ^0.2.7 + version: 0.2.7 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vite: + specifier: ^8.0.16 + version: 8.2.0(@types/node@25.9.5)(sass@1.102.0) + vite-plugin-checker: + specifier: ^0.14.1 + version: 0.14.5(@biomejs/biome@2.5.6)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(sass@1.102.0)) + vitest: + specifier: ^4.1.8 + version: 4.1.10(@types/node@25.9.5)(happy-dom@20.11.1)(vite@8.2.0(@types/node@25.9.5)(sass@1.102.0)) + +packages: + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.5.6': + resolution: {integrity: sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.6': + resolution: {integrity: sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.6': + resolution: {integrity: sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.5.6': + resolution: {integrity: sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.5.6': + resolution: {integrity: sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.5.6': + resolution: {integrity: sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.6': + resolution: {integrity: sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@emnapi/core@2.0.0-alpha.3': + resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} + + '@emnapi/runtime@2.0.0-alpha.3': + resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} + + '@emnapi/wasi-threads@2.0.1': + resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@formatjs/cli-native-darwin-arm64@1.1.9': + resolution: {integrity: sha512-vvCefAgtnwzwIGZO33ZNy6vED6iWE2+cVLjmbvhwV7kxXvFcFZtThn9F0BduzUPUt3yJSaN/wQ+CqQeAzP6qQw==} + cpu: [arm64] + os: [darwin] + + '@formatjs/cli-native-linux-arm64-musl@1.0.7': + resolution: {integrity: sha512-XjCGSu7nrB29HEOQeMXb5IWgvjMGeqVOavm3i4qR3z/YHt7XZ1MMJalrob5dqWEgTnqyOP4Z1nDPpfYXhlkBcw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@formatjs/cli-native-linux-arm64@1.2.9': + resolution: {integrity: sha512-tGA6sm2yMSurUzwzo/W0mQX0eSLPgaH28nu9UmIEpvxJg4Rd6OO7LxYbrOT6Q+JoosvPVPLqTCFY72uSKZmLPQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@formatjs/cli-native-linux-x64-musl@1.0.7': + resolution: {integrity: sha512-p1TRiXL+Pq03pBarYwjGzvnLNY2W3Hz7/7hVEnvBeCU08szwE2KrELBQPS3Y1YJN7D05d5rT661057t2uui+eg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@formatjs/cli-native-linux-x64@1.1.9': + resolution: {integrity: sha512-IdVE05YGhaPzvo/7/v16aSSnAd3NtIZa8Mo/LzvKpBRijFtMPiw9Zh/APpHz5wUO+UK/9s+Rrui7b+ou8tccSA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@formatjs/cli-native-win32-x64@1.1.10': + resolution: {integrity: sha512-yhuT+0Wh941yToJRMqY72v2xyZ782FvWyCny1FAi0TQH+jQgsyT3nJIT1cYDxBLxg7XKZIUBcOnheShptBH9sw==} + cpu: [x64] + os: [win32] + + '@formatjs/cli@6.16.15': + resolution: {integrity: sha512-JDsQQc2eonUmnd/zkz6rzALsTlVxZjGdjauvhrHXsoxgZqmEglADCKMaL2+NisjxMyTG/hFF6Ocut7YGszmW4A==} + engines: {node: '>= 20.12.0'} + hasBin: true + peerDependencies: + '@glimmer/env': '*' + '@glimmer/reference': '*' + '@glimmer/syntax': ^0.84.3 || ^0.95.0 + '@glimmer/validator': '*' + '@vue/compiler-core': '3' + content-tag: '4' + vue: '3' + peerDependenciesMeta: + '@glimmer/env': + optional: true + '@glimmer/reference': + optional: true + '@glimmer/syntax': + optional: true + '@glimmer/validator': + optional: true + '@vue/compiler-core': + optional: true + content-tag: + optional: true + vue: + optional: true + + '@formatjs/fast-memoize@3.1.7': + resolution: {integrity: sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==} + + '@formatjs/icu-messageformat-parser@3.5.15': + resolution: {integrity: sha512-5o4grXKotAB3JqQuisLApHG43g17N+paoRTa92Jiz35Zvfemq0cVf4EDvuxyHAzmsJji7igaEowicLO/VmfJ8Q==} + + '@formatjs/icu-skeleton-parser@2.1.11': + resolution: {integrity: sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==} + + '@formatjs/intl@4.1.17': + resolution: {integrity: sha512-IxluRkzOZH8ahaDip5g8YH9kJi1munUZi9i1BJs1wVFQIY2pROoCSiOlybZegyiPsZlMaYfADkD99zP2xmBfjQ==} + + '@internationalized/date@3.12.2': + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} + + '@internationalized/number@3.6.7': + resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} + + '@internationalized/string@3.2.9': + resolution: {integrity: sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@js-temporal/polyfill@0.5.1': + resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} + engines: {node: '>=12'} + + '@napi-rs/wasm-runtime@1.2.1': + resolution: {integrity: sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + + '@oxc-project/types@0.142.0': + resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} + + '@parcel/watcher-android-arm64@2.6.0': + resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} + engines: {node: '>= 10.0.0'} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@react-aria/ssr@3.10.1': + resolution: {integrity: sha512-jn038/ZYmu6DpfXJ6r2U9zFFppjbc9wnApPJSCxao2RZVEqep4YyoniHSy8qv6V21/xyS4IV7W9a+X2jOjSuag==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.36.0': + resolution: {integrity: sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@restart/hooks@0.4.16': + resolution: {integrity: sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==} + peerDependencies: + react: '>=16.8.0' + + '@restart/hooks@0.5.1': + resolution: {integrity: sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==} + peerDependencies: + react: '>=16.8.0' + + '@restart/ui@1.9.4': + resolution: {integrity: sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==} + peerDependencies: + react: '>=16.14.0' + react-dom: '>=16.14.0' + + '@rolldown/binding-android-arm64@1.2.1': + resolution: {integrity: sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.1': + resolution: {integrity: sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.1': + resolution: {integrity: sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.1': + resolution: {integrity: sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': + resolution: {integrity: sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.1': + resolution: {integrity: sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.1': + resolution: {integrity: sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.1': + resolution: {integrity: sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.1': + resolution: {integrity: sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.1': + resolution: {integrity: sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.1': + resolution: {integrity: sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.1': + resolution: {integrity: sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.2.1': + resolution: {integrity: sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + + '@rolldown/binding-win32-arm64-msvc@1.2.1': + resolution: {integrity: sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.1': + resolution: {integrity: sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@tabler/core@1.4.0': + resolution: {integrity: sha512-5BigzOlbOH9N0Is4u0rYNRCiwtnUXWO57K9zwuscygcicAa8UV9MGaS4zTgQsZEtZ9tsNANhN/YD8gCBGKYCiw==} + engines: {node: '>=20'} + + '@tabler/icons-react@3.46.0': + resolution: {integrity: sha512-CCm7xJWhDT2PH4ZIFkP6AgYKtVhq0gpYkjUN+GVh1AzmIQaa77OW0bQPBPQiTE0PsXMR9oSxFqA3qBglzPyrVQ==} + peerDependencies: + react: '>= 16' + + '@tabler/icons@3.46.0': + resolution: {integrity: sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A==} + + '@tanstack/query-core@5.101.4': + resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==} + + '@tanstack/query-devtools@5.101.4': + resolution: {integrity: sha512-z5IPHnDX3aUWeTWlRKLyooBQekaCAw4xRpZqPQ390RiWTDBcTynjpPT221BArw0u2+pnQMdGvPQI9YNNubBcmA==} + + '@tanstack/react-query-devtools@5.101.4': + resolution: {integrity: sha512-VeK2gtmfj7kvRBjtxS7TKxt/6qKhn8VzabY4UiYMr7NV9CddjSRYRgeYyld+NpjAkgMV9dd+2Qdr8ah5I03NeA==} + peerDependencies: + '@tanstack/react-query': ^5.101.4 + react: ^18 || ^19 + + '@tanstack/react-query@5.101.4': + resolution: {integrity: sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.10.0': + resolution: {integrity: sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/country-flag-icons@1.2.2': + resolution: {integrity: sha512-CefEn/J336TBDp7NX8JqzlDtCBOsm8M3r1Li0gEOt0HOMHF1XemNyrx9lSHjsafcb1yYWybU0N8ZAXuyCaND0w==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hast@2.3.10': + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + + '@types/humps@2.0.6': + resolution: {integrity: sha512-Fagm1/a/1J9gDKzGdtlPmmTN5eSw/aaTzHtj740oSfo+MODsSY2WglxMmhTdOglC8nxqUhGGQ+5HfVtBvxo3Kg==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@25.9.5': + resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prismjs@1.26.6': + resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@19.2.4': + resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react-table@7.7.20': + resolution: {integrity: sha512-ahMp4pmjVlnExxNwxyaDrFgmKxSbPwU23sGQw2gJK4EhCvnvmib2s/O/+y1dfV57dXOwpr2plfyBol+vEHbi2w==} + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/warning@3.0.4': + resolution: {integrity: sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg==} + + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@uiw/react-textarea-code-editor@3.1.1': + resolution: {integrity: sha512-AERRbp/d85vWR+UPgsB5hEgerNXuyszdmhWl2fV2H2jN63jgOobwEnjIpb76Vwy8SaGa/AdehaoJX2XZgNXtJA==} + peerDependencies: + '@babel/runtime': '>=7.10.0' + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + + '@vitejs/plugin-react@6.0.5': + resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bootstrap@5.3.7: + resolution: {integrity: sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==} + peerDependencies: + '@popperjs/core': ^2.11.8 + + buffer-image-size@0.6.4: + resolution: {integrity: sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==} + engines: {node: '>=4.0'} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + country-flag-icons@1.6.20: + resolution: {integrity: sha512-py8JiEKzjhYw6HPJ0L7SxLgCYim36UPRTZX43/kqGueUCZLSvnrqAiwW8HtQibur7mdkFQUkjOgdK+o/9FBtaw==} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + decode-uri-component@0.4.1: + resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} + engines: {node: '>=14.16'} + + deepmerge@2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + ez-modal-react@1.0.5: + resolution: {integrity: sha512-/A8yLK54tpmWCMkW8Pwqc2xxspmimGOOw/m+1Y+tNtUIheuDHhLynHP1Q0utciJEGDAK849aQcd+6DrJ88hggQ==} + peerDependencies: + react: '>16.8.0' + react-dom: '>16.8.0' + typescript: '>4.0.0' + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + formik@2.4.9: + resolution: {integrity: sha512-5nI94BMnlFDdQRBY4Sz39WkhxajZJ57Fzs8wVbtsQlm5ScKIR1QLYqv/ultBnobObtlUyxpxoLodpixrsf36Og==} + peerDependencies: + react: '>=16.8.0' + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + generate-password-browser@1.1.0: + resolution: {integrity: sha512-qsQve0rVbCqGqAfKgZwjxKUfI1d1nyd22dz+kE8gn1iw1LxGkR+Slsl79XXfm2wxuK27IkopTs5KXcOEQnhg0w==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + happy-dom@20.11.1: + resolution: {integrity: sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==} + engines: {node: '>=20.0.0'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@3.1.1: + resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@7.2.0: + resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + humps@2.0.1: + resolution: {integrity: sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immutable@5.1.9: + resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + intl-messageformat@11.2.12: + resolution: {integrity: sha512-KW70Xxfcvy7vV3qODfvShWkFDPMqKDAa4N+hSyVBWGNtVhTUFYaqlD/l88DaYPKiVcPP4rPQ3qnH7i5K82Mg7g==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsbi@4.3.2: + resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-numeric-range@1.3.0: + resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss-simple-vars@7.0.1: + resolution: {integrity: sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==} + engines: {node: '>=14.0'} + peerDependencies: + postcss: ^8.2.1 + + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + prop-types-extra@1.1.1: + resolution: {integrity: sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==} + peerDependencies: + react: '>=0.14.0' + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + + query-string@9.4.1: + resolution: {integrity: sha512-lSyJeN3RuaG7DZGWThtYRhk96+kEyZ/+doZpERuWbjeFL+Ok3vEat/swU498rAI0NcVt5/RJp8UDuLz7FckxrA==} + engines: {node: '>=18'} + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + react-aria@3.50.0: + resolution: {integrity: sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-bootstrap@2.10.10: + resolution: {integrity: sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==} + peerDependencies: + '@types/react': '>=16.14.8' + react: '>=16.14.0' + react-dom: '>=16.14.0' + peerDependenciesMeta: + '@types/react': + optional: true + + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + + react-fast-compare@2.0.4: + resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} + + react-intl@10.1.18: + resolution: {integrity: sha512-4JMOYakqVn/IajJt8Ad341Iwd6RDtiP1WQZiHRcFpHDBhVW4E9T0vkypSOk69Adg3dEbIYOE4HKTehH/1sSwnw==} + peerDependencies: + '@types/react': '>=18.0.0' + react: '>=18.0.0' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-router-dom@7.18.2: + resolution: {integrity: sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.2: + resolution: {integrity: sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-select@5.10.2: + resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-stately@3.48.0: + resolution: {integrity: sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-toastify@11.1.0: + resolution: {integrity: sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg==} + peerDependencies: + react: ^18 || ^19 + react-dom: ^18 || ^19 + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + refractor@4.9.0: + resolution: {integrity: sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-prism-plus@2.0.0: + resolution: {integrity: sha512-FeM/9V2N7EvDZVdR2dqhAzlw5YI49m9Tgn7ZrYJeYHIahM6gcXpH0K1y2gNnKanZCydOMluJvX2cB9z3lhY8XQ==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + rehype@13.0.2: + resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + rolldown@1.2.1: + resolution: {integrity: sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rooks@9.8.0: + resolution: {integrity: sha512-S6FqnmERx5zgl8ZUEcnyTe1jgjwE5xeFCgOV4bzgQHKp26P7YA7uPnzzOgacojtoX6E7pQTcewSGqN83wVyz+g==} + engines: {node: '>=v10.24.1'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + sass@1.102.0: + resolution: {integrity: sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==} + engines: {node: '>=20.19.0'} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + split-on-first@3.0.0: + resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} + engines: {node: '>=12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + uncontrollable@7.2.1: + resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} + peerDependencies: + react: '>=15.0.0' + + uncontrollable@8.0.4: + resolution: {integrity: sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==} + peerDependencies: + react: '>=16.14.0' + + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-filter@5.0.1: + resolution: {integrity: sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-plugin-checker@0.14.5: + resolution: {integrity: sha512-c9lQ92eisUO+F7Fd93aelojmiOS+NQpPgQ1XR2LTQHox1/laZf4yAoQj+L3RA9Vgh10e2nFd9b8r2LLyYZsbpA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@biomejs/biome': '>=2.4.12' + eslint: '>=9.39.4' + meow: ^13.2.0 || ^14.0.0 + optionator: ^0.9.4 + oxlint: '>=1' + stylelint: '>=16.26.1' + typescript: '*' + vite: '>=5.4.21' + vue-tsc: ~2.2.10 || ^3.0.0 + peerDependenciesMeta: + '@biomejs/biome': + optional: true + eslint: + optional: true + meow: + optional: true + optionator: + optional: true + oxlint: + optional: true + stylelint: + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@adobe/css-tools@4.5.0': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@biomejs/biome@2.5.6': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.6 + '@biomejs/cli-darwin-x64': 2.5.6 + '@biomejs/cli-linux-arm64': 2.5.6 + '@biomejs/cli-linux-arm64-musl': 2.5.6 + '@biomejs/cli-linux-x64': 2.5.6 + '@biomejs/cli-linux-x64-musl': 2.5.6 + '@biomejs/cli-win32-arm64': 2.5.6 + '@biomejs/cli-win32-x64': 2.5.6 + + '@biomejs/cli-darwin-arm64@2.5.6': + optional: true + + '@biomejs/cli-darwin-x64@2.5.6': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.6': + optional: true + + '@biomejs/cli-linux-arm64@2.5.6': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.6': + optional: true + + '@biomejs/cli-linux-x64@2.5.6': + optional: true + + '@biomejs/cli-win32-arm64@2.5.6': + optional: true + + '@biomejs/cli-win32-x64@2.5.6': + optional: true + + '@emnapi/core@2.0.0-alpha.3': + dependencies: + '@emnapi/wasi-threads': 2.0.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@2.0.0-alpha.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@2.0.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/runtime': 7.29.7 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.2.18)(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.8) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.8)': + dependencies: + react: 19.2.8 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + + '@formatjs/cli-native-darwin-arm64@1.1.9': + optional: true + + '@formatjs/cli-native-linux-arm64-musl@1.0.7': + optional: true + + '@formatjs/cli-native-linux-arm64@1.2.9': + optional: true + + '@formatjs/cli-native-linux-x64-musl@1.0.7': + optional: true + + '@formatjs/cli-native-linux-x64@1.1.9': + optional: true + + '@formatjs/cli-native-win32-x64@1.1.10': + optional: true + + '@formatjs/cli@6.16.15': + optionalDependencies: + '@formatjs/cli-native-darwin-arm64': 1.1.9 + '@formatjs/cli-native-linux-arm64': 1.2.9 + '@formatjs/cli-native-linux-arm64-musl': 1.0.7 + '@formatjs/cli-native-linux-x64': 1.1.9 + '@formatjs/cli-native-linux-x64-musl': 1.0.7 + '@formatjs/cli-native-win32-x64': 1.1.10 + + '@formatjs/fast-memoize@3.1.7': {} + + '@formatjs/icu-messageformat-parser@3.5.15': + dependencies: + '@formatjs/icu-skeleton-parser': 2.1.11 + + '@formatjs/icu-skeleton-parser@2.1.11': {} + + '@formatjs/intl@4.1.17': + dependencies: + '@formatjs/fast-memoize': 3.1.7 + '@formatjs/icu-messageformat-parser': 3.5.15 + intl-messageformat: 11.2.12 + + '@internationalized/date@3.12.2': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/number@3.6.7': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/string@3.2.9': + dependencies: + '@swc/helpers': 0.5.23 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@js-temporal/polyfill@0.5.1': + dependencies: + jsbi: 4.3.2 + optional: true + + '@napi-rs/wasm-runtime@1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + dependencies: + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.142.0': {} + + '@parcel/watcher-android-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-x64@2.6.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.6.0': + optional: true + + '@parcel/watcher-win32-arm64@2.6.0': + optional: true + + '@parcel/watcher-win32-x64@2.6.0': + optional: true + + '@parcel/watcher@2.6.0': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.5 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.6.0 + '@parcel/watcher-darwin-arm64': 2.6.0 + '@parcel/watcher-darwin-x64': 2.6.0 + '@parcel/watcher-freebsd-x64': 2.6.0 + '@parcel/watcher-linux-arm-glibc': 2.6.0 + '@parcel/watcher-linux-arm-musl': 2.6.0 + '@parcel/watcher-linux-arm64-glibc': 2.6.0 + '@parcel/watcher-linux-arm64-musl': 2.6.0 + '@parcel/watcher-linux-x64-glibc': 2.6.0 + '@parcel/watcher-linux-x64-musl': 2.6.0 + '@parcel/watcher-win32-arm64': 2.6.0 + '@parcel/watcher-win32-x64': 2.6.0 + optional: true + + '@popperjs/core@2.11.8': {} + + '@react-aria/ssr@3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@swc/helpers': 0.5.23 + react: 19.2.8 + react-aria: 3.50.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-dom: 19.2.8(react@19.2.8) + + '@react-types/shared@3.36.0(react@19.2.8)': + dependencies: + react: 19.2.8 + + '@restart/hooks@0.4.16(react@19.2.8)': + dependencies: + dequal: 2.0.3 + react: 19.2.8 + + '@restart/hooks@0.5.1(react@19.2.8)': + dependencies: + dequal: 2.0.3 + react: 19.2.8 + + '@restart/ui@1.9.4(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@popperjs/core': 2.11.8 + '@react-aria/ssr': 3.10.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@restart/hooks': 0.5.1(react@19.2.8) + '@types/warning': 3.0.4 + dequal: 2.0.3 + dom-helpers: 5.2.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + uncontrollable: 8.0.4(react@19.2.8) + warning: 4.0.3 + + '@rolldown/binding-android-arm64@1.2.1': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.1': + optional: true + + '@rolldown/binding-darwin-x64@1.2.1': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.1': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.1': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.1': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.1': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.1': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.1': + optional: true + + '@rolldown/binding-wasm32-wasi@1.2.1': + dependencies: + '@emnapi/core': 2.0.0-alpha.3 + '@emnapi/runtime': 2.0.0-alpha.3 + '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.1': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.1': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@tabler/core@1.4.0': + dependencies: + '@popperjs/core': 2.11.8 + bootstrap: 5.3.7(@popperjs/core@2.11.8) + + '@tabler/icons-react@3.46.0(react@19.2.8)': + dependencies: + '@tabler/icons': 3.46.0 + react: 19.2.8 + + '@tabler/icons@3.46.0': {} + + '@tanstack/query-core@5.101.4': {} + + '@tanstack/query-devtools@5.101.4': {} + + '@tanstack/react-query-devtools@5.101.4(@tanstack/react-query@5.101.4(react@19.2.8))(react@19.2.8)': + dependencies: + '@tanstack/query-devtools': 5.101.4 + '@tanstack/react-query': 5.101.4(react@19.2.8) + react: 19.2.8 + + '@tanstack/react-query@5.101.4(react@19.2.8)': + dependencies: + '@tanstack/query-core': 5.101.4 + react: 19.2.8 + + '@tanstack/react-table@8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + '@tanstack/table-core@8.21.3': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.10.0(@testing-library/dom@10.4.1)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/country-flag-icons@1.2.2': {} + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + + '@types/estree@1.0.9': {} + + '@types/hast@2.3.10': + dependencies: + '@types/unist': 2.0.11 + + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + hoist-non-react-statics: 3.3.2 + + '@types/humps@2.0.6': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/node@25.9.5': + dependencies: + undici-types: 7.24.6 + + '@types/parse-json@4.0.2': {} + + '@types/prismjs@1.26.6': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@19.2.4(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + + '@types/react-table@7.7.20': + dependencies: + '@types/react': 19.2.18 + + '@types/react-transition-group@4.4.12(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + + '@types/react@19.2.18': + dependencies: + csstype: 3.2.3 + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/warning@3.0.4': {} + + '@types/whatwg-mimetype@3.0.2': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.9.5 + + '@uiw/react-textarea-code-editor@3.1.1(@babel/runtime@7.29.7)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + rehype: 13.0.2 + rehype-prism-plus: 2.0.0 + + '@ungap/structured-clone@1.3.3': {} + + '@vitejs/plugin-react@6.0.5(vite@8.2.0(@types/node@25.9.5)(sass@1.102.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.0(@types/node@25.9.5)(sass@1.102.0) + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@25.9.5)(sass@1.102.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.0(@types/node@25.9.5)(sass@1.102.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.7 + cosmiconfig: 7.1.0 + resolve: 1.22.12 + + bail@2.0.2: {} + + base64-js@1.5.1: {} + + bootstrap@5.3.7(@popperjs/core@2.11.8): + dependencies: + '@popperjs/core': 2.11.8 + + buffer-image-size@0.6.4: + dependencies: + '@types/node': 25.9.5 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + callsites@3.1.0: {} + + ccount@2.0.1: {} + + chai@6.2.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + classnames@2.5.1: {} + + clsx@2.1.1: {} + + comma-separated-tokens@2.0.3: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + + country-flag-icons@1.6.20: {} + + css.escape@1.5.1: {} + + csstype@3.2.3: {} + + date-fns@4.4.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + decode-uri-component@0.4.1: {} + + deepmerge@2.2.1: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.7 + csstype: 3.2.3 + + entities@6.0.1: {} + + entities@7.0.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-errors@1.3.0: {} + + es-module-lexer@2.3.1: {} + + escape-string-regexp@4.0.0: {} + + estree-util-is-identifier-name@3.0.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + extend@3.0.2: {} + + ez-modal-react@1.0.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3): + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + typescript: 6.0.3 + + fast-deep-equal@3.1.3: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + filter-obj@5.1.0: {} + + find-root@1.1.0: {} + + formik@2.4.9(@types/react@19.2.18)(react@19.2.8): + dependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.18) + deepmerge: 2.2.1 + hoist-non-react-statics: 3.3.2 + lodash: 4.18.1 + lodash-es: 4.18.1 + react: 19.2.8 + react-fast-compare: 2.0.4 + tiny-warning: 1.0.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@types/react' + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + generate-password-browser@1.1.0: + dependencies: + buffer: 6.0.3 + randombytes: 2.1.0 + + graceful-fs@4.2.11: {} + + happy-dom@20.11.1: + dependencies: + '@types/node': 25.9.5 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + buffer-image-size: 0.6.4 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.21.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.5 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.2.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-parse-selector@3.1.1: + dependencies: + '@types/hast': 2.3.10 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.5 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.5 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hastscript@7.2.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 3.1.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-url-attributes@3.0.1: {} + + html-void-elements@3.0.0: {} + + humps@2.0.1: {} + + ieee754@1.2.1: {} + + immutable@5.1.9: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + indent-string@4.0.0: {} + + inline-style-parser@0.2.7: {} + + intl-messageformat@11.2.12: + dependencies: + '@formatjs/fast-memoize': 3.1.7 + '@formatjs/icu-messageformat-parser': 3.5.15 + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arrayish@0.2.1: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-decimal@2.0.1: {} + + is-extglob@2.1.1: + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + optional: true + + is-hexadecimal@2.0.1: {} + + is-plain-obj@4.1.0: {} + + js-tokens@4.0.0: {} + + jsbi@4.3.2: + optional: true + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lines-and-columns@1.2.4: {} + + lodash-es@4.18.1: {} + + lodash.debounce@4.0.8: {} + + lodash@4.18.1: {} + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + memoize-one@6.0.0: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + min-indent@1.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + node-addon-api@7.1.1: + optional: true + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + object-assign@4.1.1: {} + + obug@2.1.4: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-numeric-range@1.3.0: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + performance-now@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + postcss-simple-vars@7.0.1(postcss@8.5.25): + dependencies: + postcss: 8.5.25 + + postcss@8.5.25: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + prop-types-extra@1.1.1(react@19.2.8): + dependencies: + react: 19.2.8 + react-is: 16.13.1 + warning: 4.0.3 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + property-information@6.5.0: {} + + property-information@7.2.0: {} + + query-string@9.4.1: + dependencies: + decode-uri-component: 0.4.1 + filter-obj: 5.1.0 + split-on-first: 3.0.0 + + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + react-aria@3.50.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.36.0(react@19.2.8) + '@swc/helpers': 0.5.23 + aria-hidden: 1.2.6 + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-stately: 3.48.0(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) + + react-bootstrap@2.10.10(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@babel/runtime': 7.29.7 + '@restart/hooks': 0.4.16(react@19.2.8) + '@restart/ui': 1.9.4(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@types/prop-types': 15.7.15 + '@types/react-transition-group': 4.4.12(@types/react@19.2.18) + classnames: 2.5.1 + dom-helpers: 5.2.1 + invariant: 2.2.4 + prop-types: 15.8.1 + prop-types-extra: 1.1.1(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-transition-group: 4.4.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + uncontrollable: 7.2.1(react@19.2.8) + warning: 4.0.3 + optionalDependencies: + '@types/react': 19.2.18 + + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + + react-fast-compare@2.0.4: {} + + react-intl@10.1.18(@types/react@19.2.18)(react@19.2.8): + dependencies: + '@formatjs/icu-messageformat-parser': 3.5.15 + '@formatjs/intl': 4.1.17 + '@types/react': 19.2.18 + intl-messageformat: 11.2.12 + react: 19.2.8 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-lifecycles-compat@3.0.4: {} + + react-markdown@10.1.0(@types/react@19.2.18)(react@19.2.8): + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/react': 19.2.18 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.8 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-router-dom@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-router: 7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + + react-router@7.18.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + cookie: 1.1.1 + react: 19.2.8 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + + react-select@5.10.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@babel/runtime': 7.29.7 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@19.2.18)(react@19.2.8) + '@floating-ui/dom': 1.8.0 + '@types/react-transition-group': 4.4.12(@types/react@19.2.18) + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-transition-group: 4.4.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.18)(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-stately@3.48.0(react@19.2.8): + dependencies: + '@internationalized/date': 3.12.2 + '@internationalized/number': 3.6.7 + '@internationalized/string': 3.2.9 + '@react-types/shared': 3.36.0(react@19.2.8) + '@swc/helpers': 0.5.23 + react: 19.2.8 + use-sync-external-store: 1.6.0(react@19.2.8) + + react-toastify@11.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + clsx: 2.1.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + react-transition-group@4.4.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + '@babel/runtime': 7.29.7 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + + react@19.2.8: {} + + readdirp@5.0.0: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + refractor@4.9.0: + dependencies: + '@types/hast': 2.3.10 + '@types/prismjs': 1.26.6 + hastscript: 7.2.0 + parse-entities: 4.0.2 + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.5 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-prism-plus@2.0.0: + dependencies: + hast-util-to-string: 3.0.1 + parse-numeric-range: 1.3.0 + refractor: 4.9.0 + rehype-parse: 9.0.1 + unist-util-filter: 5.0.1 + unist-util-visit: 5.1.0 + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + rehype@13.0.2: + dependencies: + '@types/hast': 3.0.5 + rehype-parse: 9.0.1 + rehype-stringify: 10.0.1 + unified: 11.0.5 + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + resolve-from@4.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + retry@0.12.0: {} + + rolldown@1.2.1: + dependencies: + '@oxc-project/types': 0.142.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.1 + '@rolldown/binding-darwin-arm64': 1.2.1 + '@rolldown/binding-darwin-x64': 1.2.1 + '@rolldown/binding-freebsd-x64': 1.2.1 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.1 + '@rolldown/binding-linux-arm64-gnu': 1.2.1 + '@rolldown/binding-linux-arm64-musl': 1.2.1 + '@rolldown/binding-linux-ppc64-gnu': 1.2.1 + '@rolldown/binding-linux-s390x-gnu': 1.2.1 + '@rolldown/binding-linux-x64-gnu': 1.2.1 + '@rolldown/binding-linux-x64-musl': 1.2.1 + '@rolldown/binding-openharmony-arm64': 1.2.1 + '@rolldown/binding-wasm32-wasi': 1.2.1 + '@rolldown/binding-win32-arm64-msvc': 1.2.1 + '@rolldown/binding-win32-x64-msvc': 1.2.1 + + rooks@9.8.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + fast-deep-equal: 3.1.3 + lodash.debounce: 4.0.8 + raf: 3.4.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) + optionalDependencies: + '@js-temporal/polyfill': 0.5.1 + + safe-buffer@5.2.1: {} + + sass@1.102.0: + dependencies: + chokidar: 5.0.0 + immutable: 5.1.9 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.6.0 + + scheduler@0.27.0: {} + + set-cookie-parser@2.7.2: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + source-map-js@1.2.1: {} + + source-map@0.5.7: {} + + space-separated-tokens@2.0.2: {} + + split-on-first@3.0.0: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + stylis@4.2.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.1: {} + + tmp@0.2.7: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tslib@2.8.1: {} + + typescript@6.0.3: {} + + uncontrollable@7.2.1(react@19.2.8): + dependencies: + '@babel/runtime': 7.29.7 + '@types/react': 19.2.18 + invariant: 2.2.4 + react: 19.2.8 + react-lifecycles-compat: 3.0.4 + + uncontrollable@8.0.4(react@19.2.8): + dependencies: + react: 19.2.8 + + undici-types@7.24.6: {} + + unicorn-magic@0.3.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-filter@5.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.18)(react@19.2.8): + dependencies: + react: 19.2.8 + optionalDependencies: + '@types/react': 19.2.18 + + use-sync-external-store@1.6.0(react@19.2.8): + dependencies: + react: 19.2.8 + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite-plugin-checker@0.14.5(@biomejs/biome@2.5.6)(typescript@6.0.3)(vite@8.2.0(@types/node@25.9.5)(sass@1.102.0)): + dependencies: + '@babel/code-frame': 7.29.7 + chokidar: 5.0.0 + npm-run-path: 6.0.0 + picocolors: 1.1.1 + picomatch: 4.0.5 + proper-lockfile: 4.1.2 + tiny-invariant: 1.3.3 + vite: 8.2.0(@types/node@25.9.5)(sass@1.102.0) + optionalDependencies: + '@biomejs/biome': 2.5.6 + typescript: 6.0.3 + + vite@8.2.0(@types/node@25.9.5)(sass@1.102.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.2.1 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 25.9.5 + fsevents: 2.3.3 + sass: 1.102.0 + + vitest@4.1.10(@types/node@25.9.5)(happy-dom@20.11.1)(vite@8.2.0(@types/node@25.9.5)(sass@1.102.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@25.9.5)(sass@1.102.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.0(@types/node@25.9.5)(sass@1.102.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.5 + happy-dom: 20.11.1 + transitivePeerDependencies: + - msw + + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + + web-namespaces@2.0.1: {} + + whatwg-mimetype@3.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.1: {} + + yaml@1.10.3: {} + + zwitch@2.0.4: {} diff --git a/frontend/src/api/backend/getProxyHostNginxConfig.ts b/frontend/src/api/backend/getProxyHostNginxConfig.ts new file mode 100644 index 0000000000..3bfaad0592 --- /dev/null +++ b/frontend/src/api/backend/getProxyHostNginxConfig.ts @@ -0,0 +1,5 @@ +import * as api from "./base"; +import type { NginxConfigArtifactResponse } from "./models"; + +export const getProxyHostNginxConfig = async (id: number, includeContent = ["deployed", "candidate"]): Promise => + api.get({ url: `/nginx/proxy-hosts/${id}/nginx-config`, params: { include_content: includeContent.join(",") } }); diff --git a/frontend/src/api/backend/index.ts b/frontend/src/api/backend/index.ts index 40cb4142fc..7680702942 100644 --- a/frontend/src/api/backend/index.ts +++ b/frontend/src/api/backend/index.ts @@ -27,6 +27,8 @@ export * from "./getDeadHosts"; export * from "./getHealth"; export * from "./getHostsReport"; export * from "./getProxyHost"; +export * from "./getProxyHostNginxConfig"; +export * from "./previewProxyHostNginxConfig"; export * from "./getProxyHosts"; export * from "./getRedirectionHost"; export * from "./getRedirectionHosts"; diff --git a/frontend/src/api/backend/models.ts b/frontend/src/api/backend/models.ts index 2ae0b08348..c95695f240 100644 --- a/frontend/src/api/backend/models.ts +++ b/frontend/src/api/backend/models.ts @@ -1,210 +1,324 @@ -export interface AppVersion { - major: number; - minor: number; - revision: number; -} - -export interface UserPermissions { - id?: number; - createdOn?: string; - modifiedOn?: string; - userId?: number; - visibility: string; - proxyHosts: string; - redirectionHosts: string; - deadHosts: string; - streams: string; - accessLists: string; - certificates: string; -} - -export interface User { - id: number; - createdOn: string; - modifiedOn: string; - isDisabled: boolean; - email: string; - name: string; - nickname: string; - avatar: string; - roles: string[]; - permissions?: UserPermissions; -} - -export interface AuditLog { - id: number; - createdOn: string; - modifiedOn: string; - userId: number; - objectType: string; - objectId: number; - action: string; - meta: Record; - // Expansions: - user?: User; -} - -export interface AccessList { - id?: number; - createdOn?: string; - modifiedOn?: string; - ownerUserId: number; - name: string; - meta: Record; - satisfyAny: boolean; - passAuth: boolean; - proxyHostCount?: number; - // Expansions: - owner?: User; - items?: AccessListItem[]; - clients?: AccessListClient[]; -} - -export interface AccessListItem { - id?: number; - createdOn?: string; - modifiedOn?: string; - accessListId?: number; - username: string; - password: string; - meta?: Record; - hint?: string; -} - -export type AccessListClient = { - id?: number; - createdOn?: string; - modifiedOn?: string; - accessListId?: number; - address: string; - directive: "allow" | "deny"; - meta?: Record; -}; - -export interface Certificate { - id: number; - createdOn: string; - modifiedOn: string; - ownerUserId: number; - provider: string; - niceName: string; - domainNames: string[]; - expiresOn: string; - meta: Record; - owner?: User; - proxyHosts?: ProxyHost[]; - deadHosts?: DeadHost[]; - redirectionHosts?: RedirectionHost[]; -} - -export interface ProxyLocation { - path: string; - advancedConfig: string; - forwardScheme: string; - forwardHost: string; - forwardPort: number; -} - -export interface ProxyHost { - id: number; - createdOn: string; - modifiedOn: string; - ownerUserId: number; - domainNames: string[]; - forwardScheme: string; - forwardHost: string; - forwardPort: number; - accessListId: number; - certificateId: number; - sslForced: boolean; - cachingEnabled: boolean; - blockExploits: boolean; - advancedConfig: string; - meta: Record; - allowWebsocketUpgrade: boolean; - http2Support: boolean; - enabled: boolean; - locations?: ProxyLocation[]; - hstsEnabled: boolean; - hstsSubdomains: boolean; - trustForwardedProto: boolean; - // Expansions: - owner?: User; - accessList?: AccessList; - certificate?: Certificate; -} - -export interface DeadHost { - id: number; - createdOn: string; - modifiedOn: string; - ownerUserId: number; - domainNames: string[]; - certificateId: number; - sslForced: boolean; - advancedConfig: string; - meta: Record; - http2Support: boolean; - enabled: boolean; - hstsEnabled: boolean; - hstsSubdomains: boolean; - // Expansions: - owner?: User; - certificate?: Certificate; -} - -export interface RedirectionHost { - id: number; - createdOn: string; - modifiedOn: string; - ownerUserId: number; - domainNames: string[]; - forwardDomainName: string; - preservePath: boolean; - certificateId: number; - sslForced: boolean; - blockExploits: boolean; - advancedConfig: string; - meta: Record; - http2Support: boolean; - forwardScheme: string; - forwardHttpCode: number; - enabled: boolean; - hstsEnabled: boolean; - hstsSubdomains: boolean; - // Expansions: - owner?: User; - certificate?: Certificate; -} - -export interface Stream { - id: number; - createdOn: string; - modifiedOn: string; - ownerUserId: number; - incomingPort: number; - forwardingHost: string; - forwardingPort: number; - tcpForwarding: boolean; - udpForwarding: boolean; - meta: Record; - enabled: boolean; - certificateId: number; - // Expansions: - owner?: User; - certificate?: Certificate; -} - -export interface Setting { - id: string; - name?: string; - description?: string; - value: string; - meta?: Record; -} - -export interface DNSProvider { - id: string; - name: string; - credentials: string; -} +export interface AppVersion { + major: number; + minor: number; + revision: number; +} + +export interface UserPermissions { + id?: number; + createdOn?: string; + modifiedOn?: string; + userId?: number; + visibility: string; + proxyHosts: string; + redirectionHosts: string; + deadHosts: string; + streams: string; + accessLists: string; + certificates: string; +} + +export interface User { + id: number; + createdOn: string; + modifiedOn: string; + isDisabled: boolean; + email: string; + name: string; + nickname: string; + avatar: string; + roles: string[]; + permissions?: UserPermissions; +} + +export interface AuditLog { + id: number; + createdOn: string; + modifiedOn: string; + userId: number; + objectType: string; + objectId: number; + action: string; + meta: Record; + // Expansions: + user?: User; +} + +export interface AccessList { + id?: number; + createdOn?: string; + modifiedOn?: string; + ownerUserId: number; + name: string; + meta: Record; + satisfyAny: boolean; + passAuth: boolean; + proxyHostCount?: number; + // Expansions: + owner?: User; + items?: AccessListItem[]; + clients?: AccessListClient[]; +} + +export interface AccessListItem { + id?: number; + createdOn?: string; + modifiedOn?: string; + accessListId?: number; + username: string; + password: string; + meta?: Record; + hint?: string; +} + +export type AccessListClient = { + id?: number; + createdOn?: string; + modifiedOn?: string; + accessListId?: number; + address: string; + directive: "allow" | "deny"; + meta?: Record; +}; + +export interface Certificate { + id: number; + createdOn: string; + modifiedOn: string; + ownerUserId: number; + provider: string; + niceName: string; + domainNames: string[]; + expiresOn: string; + meta: Record; + owner?: User; + proxyHosts?: ProxyHost[]; + deadHosts?: DeadHost[]; + redirectionHosts?: RedirectionHost[]; +} + +export type NginxMatchType = "prefix" | "priority_prefix" | "exact" | "regex" | "regex_i"; +export type NginxPathMode = "preserve_uri" | "strip_prefix" | "replace_prefix"; +export type NginxDeploymentStatus = "pending" | "online" | "disabled" | "degraded" | "error" | "recovering"; + +export interface NginxHeaderOperation { + name: string; + operation?: "set" | "add" | "remove"; + value?: string; + valueMode?: "literal" | "variable"; +} + +export interface NginxCookieRewrite { + from: string; + to: string; +} + +export interface NginxOptions { + defaultLocationEnabled?: boolean; + clientMaxBodySize?: string; + proxyHttpVersion?: "1.0" | "1.1"; + proxyMethod?: string; + proxyConnectTimeout?: string; + proxySendTimeout?: string; + proxyReadTimeout?: string; + proxyNextUpstream?: string[]; + proxyNextUpstreamTimeout?: string; + proxyNextUpstreamTries?: number; + proxyIgnoreClientAbort?: boolean; + proxySocketKeepalive?: boolean; + proxyBind?: string; + proxyPassRequestHeaders?: boolean; + proxyPassRequestBody?: boolean; + proxyPassTrailers?: boolean; + proxyRequestBuffering?: boolean; + proxyBuffering?: boolean; + proxyBufferSize?: string; + proxyBuffers?: [number, string]; + proxyBusyBuffersSize?: string; + proxyMaxTempFileSize?: string; + proxyTempFileWriteSize?: string; + proxyLimitRate?: string; + proxyHeadersHashBucketSize?: number; + proxyHeadersHashMaxSize?: number; + proxyInterceptErrors?: boolean; + proxyForceRanges?: boolean; + proxyRedirect?: "default" | "off"; + proxyCookieDomain?: NginxCookieRewrite[]; + proxyCookiePath?: NginxCookieRewrite[]; + proxySslServerName?: boolean; + proxySslName?: string; + proxySslVerify?: boolean; + proxySslVerifyDepth?: number; + proxySslSessionReuse?: boolean; + proxySslProtocols?: string[]; + proxySslCiphers?: string; + requestHeaders?: NginxHeaderOperation[]; + responseHeaders?: NginxHeaderOperation[]; + hideResponseHeaders?: string[]; + proxyPassHeaders?: string[]; + proxyIgnoreHeaders?: string[]; +} + +export interface ProxyLocation { + path: string; + advancedConfig: string; + forwardScheme: string; + forwardHost: string; + forwardPort: number; + forwardPath?: string; + matchType?: NginxMatchType; + pathMode?: NginxPathMode; + nginxConfig?: NginxOptions; +} + +export interface NginxListener { + mode: "domain" | "port"; + port?: number; +} + +export interface ProxyHost { + id: number; + createdOn: string; + modifiedOn: string; + ownerUserId: number; + domainNames: string[]; + forwardScheme: string; + forwardHost: string; + forwardPort: number; + accessListId: number; + certificateId: number; + sslForced: boolean; + cachingEnabled: boolean; + blockExploits: boolean; + advancedConfig: string; + meta: Record; + allowWebsocketUpgrade: boolean; + http2Support: boolean; + enabled: boolean; + locations?: ProxyLocation[]; + hstsEnabled: boolean; + hstsSubdomains: boolean; + trustForwardedProto: boolean; + nginxConfig?: { schemaVersion: 1; server?: NginxOptions; listener?: NginxListener }; + nginxConfigRevision?: number; + nginxAppliedRevision?: number | null; + nginxAppliedEnabled?: boolean; + nginxAppliedHash?: string | null; + nginxDeploymentStatus?: NginxDeploymentStatus; + nginxCheckedAt?: string | null; + nginxLastError?: { + operationId?: string; + code?: string; + message: string; + diagnostics?: Array<{ severity: string; code: string; message: string }> | null; + } | null; + // Expansions: + owner?: User; + accessList?: AccessList; + certificate?: Certificate; +} + +export interface DeadHost { + id: number; + createdOn: string; + modifiedOn: string; + ownerUserId: number; + domainNames: string[]; + certificateId: number; + sslForced: boolean; + advancedConfig: string; + meta: Record; + http2Support: boolean; + enabled: boolean; + hstsEnabled: boolean; + hstsSubdomains: boolean; + // Expansions: + owner?: User; + certificate?: Certificate; +} + +export interface RedirectionHost { + id: number; + createdOn: string; + modifiedOn: string; + ownerUserId: number; + domainNames: string[]; + forwardDomainName: string; + preservePath: boolean; + certificateId: number; + sslForced: boolean; + blockExploits: boolean; + advancedConfig: string; + meta: Record; + http2Support: boolean; + forwardScheme: string; + forwardHttpCode: number; + enabled: boolean; + hstsEnabled: boolean; + hstsSubdomains: boolean; + // Expansions: + owner?: User; + certificate?: Certificate; +} + +export interface Stream { + id: number; + createdOn: string; + modifiedOn: string; + ownerUserId: number; + incomingPort: number; + forwardingHost: string; + forwardingPort: number; + tcpForwarding: boolean; + udpForwarding: boolean; + meta: Record; + enabled: boolean; + certificateId: number; + // Expansions: + owner?: User; + certificate?: Certificate; +} + +export interface Setting { + id: string; + name?: string; + description?: string; + value: string; + meta?: Record; +} + +export interface DNSProvider { + id: string; + name: string; + credentials: string; +} + +export interface NginxConfigArtifactResponse { + hostId: number; + status: NginxDeploymentStatus; + desiredRevision: number; + appliedRevision: number | null; + deployed: { logicalPath: string; hash: string; config?: string } | null; + candidate: { logicalPath: string; hash: string; config?: string } | null; + lastError: Record | null; + lastCheckedAt: string | null; +} + +export interface ProxyHostPreview { + valid: boolean; + config: string; + payloadHash: string; + hash: string; + previewToken?: string | null; + baseRevision?: number | null; + validationScope?: "full" | "partial" | "not_applicable"; + unresolvedDependencies?: Array<{ code: string; message: string }>; + diagnostics: Array<{ + severity: "error" | "warning" | "info"; + code: string; + message: string; + line?: number; + field?: string; + locationId?: string; + }>; +} diff --git a/frontend/src/api/backend/previewProxyHostNginxConfig.ts b/frontend/src/api/backend/previewProxyHostNginxConfig.ts new file mode 100644 index 0000000000..cc7a04262c --- /dev/null +++ b/frontend/src/api/backend/previewProxyHostNginxConfig.ts @@ -0,0 +1,5 @@ +import * as api from "./base"; +import type { ProxyHost, ProxyHostPreview } from "./models"; + +export const previewProxyHostNginxConfig = async (payload: Partial): Promise => + api.post({ url: "/nginx/proxy-hosts/nginx-config/preview", data: payload }); diff --git a/frontend/src/components/Form/AccessClientFields.tsx b/frontend/src/components/Form/AccessClientFields.tsx index 9dda8c3de0..cd4c2c56d7 100644 --- a/frontend/src/components/Form/AccessClientFields.tsx +++ b/frontend/src/components/Form/AccessClientFields.tsx @@ -1,131 +1,128 @@ -import { IconX } from "@tabler/icons-react"; -import cn from "classnames"; -import { useFormikContext } from "formik"; -import { useState } from "react"; -import type { AccessListClient } from "src/api/backend"; -import { intl, T } from "src/locale"; - -interface Props { - initialValues: AccessListClient[]; - name?: string; -} -export function AccessClientFields({ initialValues, name = "clients" }: Props) { - const [values, setValues] = useState(initialValues || []); - const { setFieldValue } = useFormikContext(); - - const blankClient: AccessListClient = { directive: "allow", address: "" }; - - if (values?.length === 0) { - setValues([blankClient]); - } - - const handleAdd = () => { - setValues([...values, blankClient]); - }; - - const handleRemove = (idx: number) => { - const newValues = values.filter((_: AccessListClient, i: number) => i !== idx); - if (newValues.length === 0) { - newValues.push(blankClient); - } - setValues(newValues); - setFormField(newValues); - }; - - const handleChange = (idx: number, field: string, fieldValue: string) => { - const newValues = values.map((v: AccessListClient, i: number) => - i === idx ? { ...v, [field]: fieldValue } : v, - ); - setValues(newValues); - setFormField(newValues); - }; - - const setFormField = (newValues: AccessListClient[]) => { - const filtered = newValues.filter((v: AccessListClient) => v?.address?.trim() !== ""); - setFieldValue(name, filtered); - }; - - return ( - <> -

- -

- {values.map((client: AccessListClient, idx: number) => ( -
-
-
- - - - handleChange(idx, "address", e.target.value)} - placeholder={intl.formatMessage({ id: "access-list.rule-source.placeholder" })} - /> -
-
- -
- ))} -
- -
-
-

- -

-
-
- - - - -
-
-
- - ); -} +import { IconX } from "@tabler/icons-react"; +import cn from "classnames"; +import { useFormikContext } from "formik"; +import { useState } from "react"; +import type { AccessListClient } from "src/api/backend"; +import { intl, T } from "src/locale"; + +interface Props { + initialValues: AccessListClient[]; + name?: string; +} +export function AccessClientFields({ initialValues, name = "clients" }: Props) { + const [values, setValues] = useState(initialValues || []); + const { setFieldValue } = useFormikContext(); + + const blankClient: AccessListClient = { directive: "allow", address: "" }; + + if (values?.length === 0) { + setValues([blankClient]); + } + + const handleAdd = () => { + setValues([...values, blankClient]); + }; + + const handleRemove = (idx: number) => { + const newValues = values.filter((_: AccessListClient, i: number) => i !== idx); + if (newValues.length === 0) { + newValues.push(blankClient); + } + setValues(newValues); + setFormField(newValues); + }; + + const handleChange = (idx: number, field: string, fieldValue: string) => { + const newValues = values.map((v: AccessListClient, i: number) => + i === idx ? { ...v, [field]: fieldValue } : v, + ); + setValues(newValues); + setFormField(newValues); + }; + + const setFormField = (newValues: AccessListClient[]) => { + const filtered = newValues.filter((v: AccessListClient) => v?.address?.trim() !== ""); + setFieldValue(name, filtered); + }; + + return ( + <> +

+ +

+ {values.map((client: AccessListClient, idx: number) => ( +
+
+
+ + + + handleChange(idx, "address", e.target.value)} + placeholder={intl.formatMessage({ id: "access-list.rule-source.placeholder" })} + /> +
+
+
+ +
+
+ ))} +
+ +
+
+

+ +

+
+
+ + + + +
+
+
+ + ); +} diff --git a/frontend/src/components/Form/AccessField.tsx b/frontend/src/components/Form/AccessField.tsx index afcbd0cf7d..ab4685e1b8 100644 --- a/frontend/src/components/Form/AccessField.tsx +++ b/frontend/src/components/Form/AccessField.tsx @@ -71,6 +71,9 @@ export function AccessField({ name = "accessListId", label = "access-list", id = +
+ +
{isLoading ?
: null} {isError ?
{`${error}`}
: null} {!isLoading && !isError ? ( diff --git a/frontend/src/components/Form/BasicAuthFields.tsx b/frontend/src/components/Form/BasicAuthFields.tsx index c47d96da42..d25938808c 100644 --- a/frontend/src/components/Form/BasicAuthFields.tsx +++ b/frontend/src/components/Form/BasicAuthFields.tsx @@ -1,105 +1,102 @@ -import { IconX } from "@tabler/icons-react"; -import { useFormikContext } from "formik"; -import { useState } from "react"; -import type { AccessListItem } from "src/api/backend"; -import { T } from "src/locale"; - -interface Props { - initialValues: AccessListItem[]; - name?: string; -} -export function BasicAuthFields({ initialValues, name = "items" }: Props) { - const [values, setValues] = useState(initialValues || []); - const { setFieldValue } = useFormikContext(); - - const blankItem: AccessListItem = { username: "", password: "" }; - - if (values?.length === 0) { - setValues([blankItem]); - } - - const handleAdd = () => { - setValues([...values, blankItem]); - }; - - const handleRemove = (idx: number) => { - const newValues = values.filter((_: AccessListItem, i: number) => i !== idx); - if (newValues.length === 0) { - newValues.push(blankItem); - } - setValues(newValues); - setFormField(newValues); - }; - - const handleChange = (idx: number, field: string, fieldValue: string) => { - const newValues = values.map((v: AccessListItem, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v)); - setValues(newValues); - setFormField(newValues); - }; - - const setFormField = (newValues: AccessListItem[]) => { - const filtered = newValues.filter((v: AccessListItem) => v?.username?.trim() !== ""); - setFieldValue(name, filtered); - }; - - return ( - <> -
-
- -
-
- -
-
- {values.map((item: AccessListItem, idx: number) => ( -
-
- handleChange(idx, "username", e.target.value)} - /> -
-
- iv.username === item.username).length > 0 - ? "••••••••" - : "" - } - onChange={(e) => handleChange(idx, "password", e.target.value)} - /> -
- -
- ))} -
- -
- - ); -} +import { IconX } from "@tabler/icons-react"; +import { useFormikContext } from "formik"; +import { useState } from "react"; +import type { AccessListItem } from "src/api/backend"; +import { T } from "src/locale"; + +interface Props { + initialValues: AccessListItem[]; + name?: string; +} +export function BasicAuthFields({ initialValues, name = "items" }: Props) { + const [values, setValues] = useState(initialValues || []); + const { setFieldValue } = useFormikContext(); + + const blankItem: AccessListItem = { username: "", password: "" }; + + if (values?.length === 0) { + setValues([blankItem]); + } + + const handleAdd = () => { + setValues([...values, blankItem]); + }; + + const handleRemove = (idx: number) => { + const newValues = values.filter((_: AccessListItem, i: number) => i !== idx); + if (newValues.length === 0) { + newValues.push(blankItem); + } + setValues(newValues); + setFormField(newValues); + }; + + const handleChange = (idx: number, field: string, fieldValue: string) => { + const newValues = values.map((v: AccessListItem, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v)); + setValues(newValues); + setFormField(newValues); + }; + + const setFormField = (newValues: AccessListItem[]) => { + const filtered = newValues.filter((v: AccessListItem) => v?.username?.trim() !== ""); + setFieldValue(name, filtered); + }; + + return ( + <> +
+
+ +
+
+ +
+
+ {values.map((item: AccessListItem, idx: number) => ( +
+
+ handleChange(idx, "username", e.target.value)} + /> +
+
+ iv.username === item.username).length > 0 + ? "••••••••" + : "" + } + onChange={(e) => handleChange(idx, "password", e.target.value)} + /> +
+
+ +
+
+ ))} +
+ +
+ + ); +} diff --git a/frontend/src/components/Form/DomainNamesField.tsx b/frontend/src/components/Form/DomainNamesField.tsx index bdba2f2e23..a61a764b8c 100644 --- a/frontend/src/components/Form/DomainNamesField.tsx +++ b/frontend/src/components/Form/DomainNamesField.tsx @@ -56,6 +56,9 @@ export function DomainNamesField({ +
+ +
{form.errors[field.name] && form.touched[field.name] ? ( {form.errors[field.name]} - ) : helperTexts.length ? ( - helperTexts.map((i, idx) => ( - - {i} - - )) + ) : null} + {helperTexts.length ? ( +
+ {helperTexts.map((i, idx) => ( + + {i} + + ))} +
) : null}
)} diff --git a/frontend/src/components/Form/LocationsFields.tsx b/frontend/src/components/Form/LocationsFields.tsx index 4240b1f986..d38346560e 100644 --- a/frontend/src/components/Form/LocationsFields.tsx +++ b/frontend/src/components/Form/LocationsFields.tsx @@ -1,185 +1,413 @@ -import { IconSettings } from "@tabler/icons-react"; +import { IconChevronDown, IconChevronUp, IconTrash } from "@tabler/icons-react"; import CodeEditor from "@uiw/react-textarea-code-editor"; import cn from "classnames"; -import { useFormikContext } from "formik"; +import { getIn, useFormikContext } from "formik"; import { useState } from "react"; -import type { ProxyLocation } from "src/api/backend"; +import type { NginxMatchType, NginxPathMode, ProxyLocation } from "src/api/backend"; import { intl, T } from "src/locale"; import styles from "./LocationsFields.module.css"; +import { ProxyDirectivesFields } from "./ProxyDirectivesFields"; interface Props { initialValues: ProxyLocation[]; name?: string; + defaultLocationEnabled?: boolean; } -export function LocationsFields({ initialValues, name = "locations" }: Props) { - const [values, setValues] = useState(initialValues || []); - const { setFieldValue } = useFormikContext(); - const [advVisible, setAdvVisible] = useState([]); - - const blankItem: ProxyLocation = { - path: "", - advancedConfig: "", - forwardScheme: "http", - forwardHost: "", - forwardPort: 80, - }; - const toggleAdvVisible = (idx: number) => { - setAdvVisible(advVisible.includes(idx) ? advVisible.filter((i) => i !== idx) : [...advVisible, idx]); +const matchOperators: Record = { + prefix: "", + priority_prefix: "^~", + exact: "=", + regex: "~", + regex_i: "~*", +}; + +const blankItem = (): ProxyLocation => ({ + path: "", + advancedConfig: "", + forwardScheme: "http", + forwardHost: "", + forwardPort: 80, + matchType: "prefix", + pathMode: "preserve_uri", + nginxConfig: {}, +}); + +export function LocationsFields({ initialValues, name = "locations", defaultLocationEnabled = true }: Props) { + const { values: formValues, setFieldValue } = useFormikContext(); + const [values, setValues] = useState(() => getIn(formValues, name) || initialValues || []); + const [advancedVisible, setAdvancedVisible] = useState([]); + const [proxyDirectivesVisible, setProxyDirectivesVisible] = useState(() => + (getIn(formValues, name) || initialValues || []).flatMap((item: ProxyLocation, index: number) => + Object.keys(item.nginxConfig || {}).length > 0 ? [index] : [], + ), + ); + + const currentValues = () => getIn(formValues, name) || values; + + const updateValues = (newValues: ProxyLocation[]) => { + setValues(newValues); + setFieldValue(name, newValues); }; - const handleAdd = () => { - setValues([...values, blankItem]); + const rebaseVisibleIndexes = (current: number[], removedIndex: number) => + current + .filter((itemIndex) => itemIndex !== removedIndex) + .map((itemIndex) => (itemIndex > removedIndex ? itemIndex - 1 : itemIndex)); + + const handleAdd = () => updateValues([...currentValues(), blankItem()]); + + const handleRemove = (index: number) => { + updateValues(currentValues().filter((_: ProxyLocation, itemIndex: number) => itemIndex !== index)); + setAdvancedVisible((current) => rebaseVisibleIndexes(current, index)); + setProxyDirectivesVisible((current) => rebaseVisibleIndexes(current, index)); }; - const handleRemove = (idx: number) => { - const newValues = values.filter((_: ProxyLocation, i: number) => i !== idx); - setValues(newValues); - setFormField(newValues); + const handleChange = (index: number, field: keyof ProxyLocation, fieldValue: unknown) => { + const newValues = currentValues().map((value: ProxyLocation, itemIndex: number) => { + if (itemIndex !== index) return value; + const next = { ...value, [field]: fieldValue }; + if (field === "matchType" && ["exact", "regex", "regex_i"].includes(String(fieldValue))) { + next.pathMode = "preserve_uri"; + delete next.forwardPath; + } + if (field === "pathMode" && fieldValue !== "replace_prefix") delete next.forwardPath; + return next; + }); + updateValues(newValues); }; - const handleChange = (idx: number, field: string, fieldValue: string) => { - const newValues = values.map((v: ProxyLocation, i: number) => (i === idx ? { ...v, [field]: fieldValue } : v)); - setValues(newValues); - setFormField(newValues); + const toggleAdvanced = (index: number) => { + setAdvancedVisible((current) => + current.includes(index) ? current.filter((itemIndex) => itemIndex !== index) : [...current, index], + ); }; - const setFormField = (newValues: ProxyLocation[]) => { - const filtered = newValues.filter((v: ProxyLocation) => v?.path?.trim() !== ""); - setFieldValue(name, filtered); + const toggleProxyDirectives = (index: number) => { + const visible = proxyDirectivesVisible.includes(index); + const configured = Object.keys(currentValues()[index]?.nginxConfig || {}).length > 0; + if (visible || configured) { + handleChange(index, "nginxConfig", {}); + setProxyDirectivesVisible((current) => current.filter((itemIndex) => itemIndex !== index)); + return; + } + + const serverOptions = { ...(getIn(formValues, "nginxConfig.server") || {}) }; + delete serverOptions.defaultLocationEnabled; + handleChange(index, "nginxConfig", JSON.parse(JSON.stringify(serverOptions))); + setProxyDirectivesVisible((current) => [...current, index]); }; if (values.length === 0) { return ( -
- +
+
+

+ +

+

+ +

+ +
); } return ( <> - {values.map((item: ProxyLocation, idx: number) => ( -
-
-
-
-
- Location + {values.map((item, index) => { + const matchType = item.matchType || "prefix"; + const pathMode = item.pathMode || "preserve_uri"; + const pathModeLocked = ["exact", "regex", "regex_i"].includes(matchType); + const locationSyntax = `location ${matchOperators[matchType]} ${item.path || "/path"}`.replace( + " ", + " ", + ); + return ( +
+
+
+
+ +
+ + {locationSyntax} {"{"} + +
+ +
+
+
+
+ +
+ +
+ +
+
+ +
+ +
handleChange(idx, "path", e.target.value)} + onChange={(event) => handleChange(index, "path", event.target.value)} />
-
- -
-
-
-
-
-