From 674316aa46014179e358cc39a9dc6b7cb81e7cb6 Mon Sep 17 00:00:00 2001 From: Matthias Palmetshofer Date: Wed, 9 Apr 2025 23:42:50 +0200 Subject: [PATCH 01/53] add option to set TLS Server Name --- server/db/schemas/schema.ts | 3 +- server/lib/schemas.ts | 7 +++ server/routers/resource/listResources.ts | 6 ++- server/routers/resource/updateResource.ts | 14 +++++- server/routers/traefik/getTraefikConfig.ts | 19 +++++++- .../resources/[resourceId]/general/page.tsx | 46 +++++++++++++++++-- 6 files changed, 84 insertions(+), 11 deletions(-) diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index a8627553..2fe5ac2b 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -77,7 +77,8 @@ export const resources = sqliteTable("resources", { applyRules: integer("applyRules", { mode: "boolean" }) .notNull() .default(false), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + tlsServerName: text("tlsServerName").notNull().default("") }); export const targets = sqliteTable("targets", { diff --git a/server/lib/schemas.ts b/server/lib/schemas.ts index f4b7daf3..cf1b40c8 100644 --- a/server/lib/schemas.ts +++ b/server/lib/schemas.ts @@ -9,3 +9,10 @@ export const subdomainSchema = z .min(1, "Subdomain must be at least 1 character long") .transform((val) => val.toLowerCase()); +export const tlsNameSchema = z + .string() + .regex( + /^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$|^$/, + "Invalid subdomain format" + ) + .transform((val) => val.toLowerCase()); \ No newline at end of file diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 1dba4119..56df9128 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -68,7 +68,8 @@ function queryResources( http: resources.http, protocol: resources.protocol, proxyPort: resources.proxyPort, - enabled: resources.enabled + enabled: resources.enabled, + tlsServerName: resources.tlsServerName }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) @@ -102,7 +103,8 @@ function queryResources( http: resources.http, protocol: resources.protocol, proxyPort: resources.proxyPort, - enabled: resources.enabled + enabled: resources.enabled, + tlsServerName: resources.tlsServerName }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 121b34ed..54802ccc 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -16,7 +16,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import config from "@server/lib/config"; -import { subdomainSchema } from "@server/lib/schemas"; +import { subdomainSchema, tlsNameSchema } from "@server/lib/schemas"; const updateResourceParamsSchema = z .object({ @@ -40,7 +40,8 @@ const updateHttpResourceBodySchema = z isBaseDomain: z.boolean().optional(), applyRules: z.boolean().optional(), domainId: z.string().optional(), - enabled: z.boolean().optional() + enabled: z.boolean().optional(), + tlsServerName: z.string().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -67,6 +68,15 @@ const updateHttpResourceBodySchema = z { message: "Base domain resources are not allowed" } + ) + .refine( + (data) => { + if (data.tlsServerName) { + return tlsNameSchema.safeParse(data.tlsServerName).success; + } + return true; + }, + { message: "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name." } ); export type UpdateResourceResponse = Resource; diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 17e385ed..42a47940 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -40,7 +40,8 @@ export async function traefikConfigProvider( org: { orgId: orgs.orgId }, - enabled: resources.enabled + enabled: resources.enabled, + tlsServerName: resources.tlsServerName }) .from(resources) .innerJoin(sites, eq(sites.siteId, resources.siteId)) @@ -139,6 +140,7 @@ export async function traefikConfigProvider( const routerName = `${resource.resourceId}-router`; const serviceName = `${resource.resourceId}-service`; const fullDomain = `${resource.fullDomain}`; + const transportName = `${resource.resourceId}-transport`; if (!resource.enabled) { continue; @@ -278,6 +280,21 @@ export async function traefikConfigProvider( }) } }; + + // Add the serversTransport if TLS server name is provided + if (resource.tlsServerName) { + if (!config_output.http.serversTransports) { + config_output.http.serversTransports = {}; + } + config_output.http.serversTransports![transportName] = { + serverName: resource.tlsServerName, + //unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings + // if defined in the static config and here. if not set, self-signed certs won't work + insecureSkipVerify: true + }; + config_output.http.services![serviceName].loadBalancer.serversTransport = transportName; + } + } else { // Non-HTTP (TCP/UDP) configuration const protocol = resource.protocol.toLowerCase(); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index 5d6cc81e..a3fccf26 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -48,7 +48,7 @@ import { useOrgContext } from "@app/hooks/useOrgContext"; import CustomDomainInput from "../CustomDomainInput"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { subdomainSchema } from "@server/lib/schemas"; +import { subdomainSchema, tlsNameSchema } from "@server/lib/schemas"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; import { Label } from "@app/components/ui/label"; @@ -73,7 +73,8 @@ const GeneralFormSchema = z proxyPort: z.number().optional(), http: z.boolean(), isBaseDomain: z.boolean().optional(), - domainId: z.string().optional() + domainId: z.string().optional(), + tlsServerName: z.string().optional() }) .refine( (data) => { @@ -103,6 +104,18 @@ const GeneralFormSchema = z message: "Invalid subdomain", path: ["subdomain"] } + ) + .refine( + (data) => { + if (data.tlsServerName) { + return tlsNameSchema.safeParse(data.tlsServerName).success; + } + return true; + }, + { + message: "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", + path: ["tlsServerName"] + } ); const TransferFormSchema = z.object({ @@ -146,7 +159,8 @@ export default function GeneralForm() { proxyPort: resource.proxyPort ? resource.proxyPort : undefined, http: resource.http, isBaseDomain: resource.isBaseDomain ? true : false, - domainId: resource.domainId || undefined + domainId: resource.domainId || undefined, + tlsServerName: resource.http ? resource.tlsServerName || "" : undefined }, mode: "onChange" }); @@ -210,7 +224,8 @@ export default function GeneralForm() { subdomain: data.http ? data.subdomain : undefined, proxyPort: data.proxyPort, isBaseDomain: data.http ? data.isBaseDomain : undefined, - domainId: data.http ? data.domainId : undefined + domainId: data.http ? data.domainId : undefined, + tlsServerName: data.http ? data.tlsServerName : undefined } ) .catch((e) => { @@ -237,7 +252,8 @@ export default function GeneralForm() { subdomain: data.subdomain, proxyPort: data.proxyPort, isBaseDomain: data.isBaseDomain, - fullDomain: resource.fullDomain + fullDomain: resource.fullDomain, + tlsServerName: data.tlsServerName }); router.refresh(); @@ -545,7 +561,27 @@ export default function GeneralForm() { )} /> )} + {/* New TLS Server Name Field */} +
+ + TLS Server Name + + ( + + + + + + + )} + /> +
)} From 517bc7f6325436ef443757dcc87de7b967e5bd31 Mon Sep 17 00:00:00 2001 From: Matthias Palmetshofer Date: Thu, 10 Apr 2025 00:36:34 +0200 Subject: [PATCH 02/53] added table change to new migration script --- server/setup/migrations.ts | 4 +++- server/setup/scripts/1.3.0.ts | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 server/setup/scripts/1.3.0.ts diff --git a/server/setup/migrations.ts b/server/setup/migrations.ts index 77248f62..dbeaeea2 100644 --- a/server/setup/migrations.ts +++ b/server/setup/migrations.ts @@ -19,6 +19,7 @@ import m15 from "./scripts/1.0.0-beta15"; import m16 from "./scripts/1.0.0"; import m17 from "./scripts/1.1.0"; import m18 from "./scripts/1.2.0"; +import m19 from "./scripts/1.3.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -37,7 +38,8 @@ const migrations = [ { version: "1.0.0-beta.15", run: m15 }, { version: "1.0.0", run: m16 }, { version: "1.1.0", run: m17 }, - { version: "1.2.0", run: m18 } + { version: "1.2.0", run: m18 }, + { version: "1.3.0", run: m19 } // Add new migrations here as they are created ] as const; diff --git a/server/setup/scripts/1.3.0.ts b/server/setup/scripts/1.3.0.ts new file mode 100644 index 00000000..d9a8e959 --- /dev/null +++ b/server/setup/scripts/1.3.0.ts @@ -0,0 +1,23 @@ +import db from "@server/db"; +import { sql } from "drizzle-orm"; + +const version = "1.1.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + db.transaction((trx) => { + trx.run( + sql`ALTER TABLE 'resources' ADD 'tlsServerName' integer DEFAULT '' NOT NULL;` + ); + }); + + console.log(`Migrated database schema`); + } catch (e) { + console.log("Unable to migrate database schema"); + throw e; + } + + console.log(`${version} migration complete`); +} From 64a2cc23c6629616ae968f07fdbbf05d8e04b926 Mon Sep 17 00:00:00 2001 From: Matthias Palmetshofer Date: Fri, 11 Apr 2025 09:52:34 +0200 Subject: [PATCH 03/53] adjusting field description; fix migration script; trying to resolve conflict in updateResource.ts --- server/routers/resource/updateResource.ts | 3 ++- server/setup/scripts/1.3.0.ts | 4 ++-- .../[orgId]/settings/resources/[resourceId]/general/page.tsx | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 54802ccc..23dea616 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -16,7 +16,8 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import config from "@server/lib/config"; -import { subdomainSchema, tlsNameSchema } from "@server/lib/schemas"; +import { tlsNameSchema } from "@server/lib/schemas"; +import { subdomainSchema } from "@server/lib/schemas"; const updateResourceParamsSchema = z .object({ diff --git a/server/setup/scripts/1.3.0.ts b/server/setup/scripts/1.3.0.ts index d9a8e959..f0b481fc 100644 --- a/server/setup/scripts/1.3.0.ts +++ b/server/setup/scripts/1.3.0.ts @@ -1,7 +1,7 @@ import db from "@server/db"; import { sql } from "drizzle-orm"; -const version = "1.1.0"; +const version = "1.3.0"; export default async function migration() { console.log(`Running setup script ${version}...`); @@ -9,7 +9,7 @@ export default async function migration() { try { db.transaction((trx) => { trx.run( - sql`ALTER TABLE 'resources' ADD 'tlsServerName' integer DEFAULT '' NOT NULL;` + sql`ALTER TABLE 'resources' ADD 'tlsServerName' text DEFAULT '' NOT NULL;` ); }); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index a3fccf26..529b5b97 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -565,7 +565,7 @@ export default function GeneralForm() {
- TLS Server Name + TLS Server Name (optional) Date: Fri, 11 Apr 2025 03:00:43 -0400 Subject: [PATCH 04/53] Add ability for sticky sessions to backend resource. --- server/db/schemas/schema.ts | 5 ++- server/routers/resource/updateResource.ts | 3 +- server/routers/traefik/getTraefikConfig.ts | 29 +++++++++++-- server/setup/migrations.ts | 5 ++- server/setup/scripts/1.3.0.ts | 23 ++++++++++ .../[resourceId]/connectivity/page.tsx | 43 ++++++++++++++++++- 6 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 server/setup/scripts/1.3.0.ts diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index a8627553..a72df539 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -77,7 +77,10 @@ export const resources = sqliteTable("resources", { applyRules: integer("applyRules", { mode: "boolean" }) .notNull() .default(false), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + stickySession: integer("stickySession", { mode: "boolean" }) + .notNull() + .default(false) }); export const targets = sqliteTable("targets", { diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index df4a41e7..df04adea 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -42,7 +42,8 @@ const updateHttpResourceBodySchema = z isBaseDomain: z.boolean().optional(), applyRules: z.boolean().optional(), domainId: z.string().optional(), - enabled: z.boolean().optional() + enabled: z.boolean().optional(), + stickySession: z.boolean().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 17e385ed..aee5e2a9 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -40,7 +40,8 @@ export async function traefikConfigProvider( org: { orgId: orgs.orgId }, - enabled: resources.enabled + enabled: resources.enabled, + stickySession: resources.stickySession }) .from(resources) .innerJoin(sites, eq(sites.siteId, resources.siteId)) @@ -275,7 +276,18 @@ export async function traefikConfigProvider( url: `${target.method}://${ip}:${target.internalPort}` }; } - }) + }), + ...(resource.stickySession + ? { + sticky: { + cookie: { + name: "pangolin_sticky", + secure: resource.ssl, + httpOnly: true + } + } + } + : {}) } }; } else { @@ -335,7 +347,18 @@ export async function traefikConfigProvider( address: `${ip}:${target.internalPort}` }; } - }) + }), + ...(resource.stickySession + ? { + sticky: { + cookie: { + name: "pangolin_sticky", + secure: resource.ssl, + httpOnly: true + } + } + } + : {}) } }; } diff --git a/server/setup/migrations.ts b/server/setup/migrations.ts index 77248f62..1a2c1d22 100644 --- a/server/setup/migrations.ts +++ b/server/setup/migrations.ts @@ -19,7 +19,7 @@ import m15 from "./scripts/1.0.0-beta15"; import m16 from "./scripts/1.0.0"; import m17 from "./scripts/1.1.0"; import m18 from "./scripts/1.2.0"; - +import m19 from "./scripts/1.3.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -37,7 +37,8 @@ const migrations = [ { version: "1.0.0-beta.15", run: m15 }, { version: "1.0.0", run: m16 }, { version: "1.1.0", run: m17 }, - { version: "1.2.0", run: m18 } + { version: "1.2.0", run: m18 }, + { version: "1.3.0", run: m19 } // Add new migrations here as they are created ] as const; diff --git a/server/setup/scripts/1.3.0.ts b/server/setup/scripts/1.3.0.ts new file mode 100644 index 00000000..a3a16e1c --- /dev/null +++ b/server/setup/scripts/1.3.0.ts @@ -0,0 +1,23 @@ +import db from "@server/db"; +import { sql } from "drizzle-orm"; + +const version = "1.3.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + db.transaction((trx) => { + trx.run( + sql`ALTER TABLE resources ADD stickySession integer DEFAULT false NOT NULL;` + ); + }); + + console.log(`Added new column: stickySession`); + } catch (e) { + console.log("Unable to add new column: stickySession"); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx index f0dd8978..bb238b68 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx @@ -94,6 +94,7 @@ export default function ReverseProxyTargets(props: { const [site, setSite] = useState(); const [targetsToRemove, setTargetsToRemove] = useState([]); const [sslEnabled, setSslEnabled] = useState(resource.ssl); + const [stickySession, setStickySession] = useState(resource.stickySession); const [loading, setLoading] = useState(false); @@ -317,6 +318,35 @@ export default function ReverseProxyTargets(props: { } } + async function saveStickySession(val: boolean) { + const res = await api + .post(`/resource/${params.resourceId}`, { + stickySession: val + }) + .catch((err) => { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to update sticky session configuration", + description: formatAxiosError( + err, + "An error occurred while updating the sticky session configuration" + ) + }); + }); + + if (res && res.status === 200) { + setStickySession(val); + updateResource({ stickySession: val }); + + toast({ + title: "Sticky Session Configuration", + description: "Sticky session configuration updated successfully" + }); + router.refresh(); + } + } + const columns: ColumnDef[] = [ { accessorKey: "ip", @@ -456,13 +486,22 @@ export default function ReverseProxyTargets(props: { - SSL Configuration + Advanced Configuration - Set up SSL to secure your connections with certificates + Configure advanced settings for your resource + { + await saveStickySession(val); + }} + /> Date: Sun, 13 Apr 2025 15:29:53 -0400 Subject: [PATCH 05/53] Update sticky session to show only with 2+ targets and use IP strategy for TCP --- server/routers/traefik/getTraefikConfig.ts | 12 ++++++----- .../[resourceId]/connectivity/page.tsx | 20 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index aee5e2a9..02dba188 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -103,7 +103,10 @@ export async function traefikConfigProvider( [badgerMiddlewareName]: { apiBaseUrl: new URL( "/api/v1", - `http://${config.getRawConfig().server.internal_hostname}:${ + `http://${ + config.getRawConfig().server + .internal_hostname + }:${ config.getRawConfig().server .internal_port }` @@ -351,10 +354,9 @@ export async function traefikConfigProvider( ...(resource.stickySession ? { sticky: { - cookie: { - name: "pangolin_sticky", - secure: resource.ssl, - httpOnly: true + ipStrategy: { + depth: 0, + sourcePort: true } } } diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx index bb238b68..18bb8f34 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx @@ -493,15 +493,17 @@ export default function ReverseProxyTargets(props: { - { - await saveStickySession(val); - }} - /> + {targets.length >= 2 && ( + { + await saveStickySession(val); + }} + /> + )} Date: Tue, 15 Apr 2025 13:17:46 +0200 Subject: [PATCH 06/53] added advanced section to general page & custom host header field --- server/db/schemas/schema.ts | 3 +- server/routers/resource/listResources.ts | 6 +- server/routers/resource/updateResource.ts | 12 +- server/routers/traefik/getTraefikConfig.ts | 26 ++- server/setup/scripts/1.3.0.ts | 3 + .../resources/[resourceId]/general/page.tsx | 187 ++++++++++++++---- 6 files changed, 199 insertions(+), 38 deletions(-) diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index 2fe5ac2b..5263161b 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -78,7 +78,8 @@ export const resources = sqliteTable("resources", { .notNull() .default(false), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), - tlsServerName: text("tlsServerName").notNull().default("") + tlsServerName: text("tlsServerName").notNull().default(""), + setHostHeader: text("setHostHeader").notNull().default("") }); export const targets = sqliteTable("targets", { diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 56df9128..72788bf2 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -69,7 +69,8 @@ function queryResources( protocol: resources.protocol, proxyPort: resources.proxyPort, enabled: resources.enabled, - tlsServerName: resources.tlsServerName + tlsServerName: resources.tlsServerName, + setHostHeader: resources.setHostHeader }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) @@ -104,7 +105,8 @@ function queryResources( protocol: resources.protocol, proxyPort: resources.proxyPort, enabled: resources.enabled, - tlsServerName: resources.tlsServerName + tlsServerName: resources.tlsServerName, + setHostHeader: resources.setHostHeader }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 23dea616..7ceb5657 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -42,7 +42,8 @@ const updateHttpResourceBodySchema = z applyRules: z.boolean().optional(), domainId: z.string().optional(), enabled: z.boolean().optional(), - tlsServerName: z.string().optional() + tlsServerName: z.string().optional(), + setHostHeader: z.string().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -78,6 +79,15 @@ const updateHttpResourceBodySchema = z return true; }, { message: "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name." } + ) + .refine( + (data) => { + if (data.setHostHeader) { + return tlsNameSchema.safeParse(data.setHostHeader).success; + } + return true; + }, + { message: "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header." } ); export type UpdateResourceResponse = Resource; diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 42a47940..8a952546 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -41,7 +41,8 @@ export async function traefikConfigProvider( orgId: orgs.orgId }, enabled: resources.enabled, - tlsServerName: resources.tlsServerName + tlsServerName: resources.tlsServerName, + setHostHeader: resources.setHostHeader }) .from(resources) .innerJoin(sites, eq(sites.siteId, resources.siteId)) @@ -141,6 +142,7 @@ export async function traefikConfigProvider( const serviceName = `${resource.resourceId}-service`; const fullDomain = `${resource.fullDomain}`; const transportName = `${resource.resourceId}-transport`; + const hostHeaderMiddlewareName = `${resource.resourceId}-host-header-middleware`; if (!resource.enabled) { continue; @@ -295,6 +297,28 @@ export async function traefikConfigProvider( config_output.http.services![serviceName].loadBalancer.serversTransport = transportName; } + // Add the host header middleware + if (resource.setHostHeader) { + if (!config_output.http.middlewares) { + config_output.http.middlewares = {}; + } + config_output.http.middlewares[hostHeaderMiddlewareName] = + { + headers: { + customRequestHeaders: { + Host: resource.setHostHeader + } + } + }; + if (!config_output.http.routers![routerName].middlewares) { + config_output.http.routers![routerName].middlewares = []; + } + config_output.http.routers![routerName].middlewares = [ + ...config_output.http.routers![routerName].middlewares, + hostHeaderMiddlewareName + ]; + } + } else { // Non-HTTP (TCP/UDP) configuration const protocol = resource.protocol.toLowerCase(); diff --git a/server/setup/scripts/1.3.0.ts b/server/setup/scripts/1.3.0.ts index f0b481fc..692dacb4 100644 --- a/server/setup/scripts/1.3.0.ts +++ b/server/setup/scripts/1.3.0.ts @@ -11,6 +11,9 @@ export default async function migration() { trx.run( sql`ALTER TABLE 'resources' ADD 'tlsServerName' text DEFAULT '' NOT NULL;` ); + trx.run( + sql`ALTER TABLE 'resources' ADD 'setHostHeader' text DEFAULT '' NOT NULL;` + ); }); console.log(`Migrated database schema`); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index 529b5b97..05d263e6 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -73,8 +73,7 @@ const GeneralFormSchema = z proxyPort: z.number().optional(), http: z.boolean(), isBaseDomain: z.boolean().optional(), - domainId: z.string().optional(), - tlsServerName: z.string().optional() + domainId: z.string().optional() }) .refine( (data) => { @@ -104,7 +103,18 @@ const GeneralFormSchema = z message: "Invalid subdomain", path: ["subdomain"] } - ) + ); + +const TransferFormSchema = z.object({ + siteId: z.number() +}); + +const AdvancedFormSchema = z + .object({ + http: z.boolean(), + tlsServerName: z.string().optional(), + setHostHeader: z.string().optional() + }) .refine( (data) => { if (data.tlsServerName) { @@ -116,14 +126,23 @@ const GeneralFormSchema = z message: "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", path: ["tlsServerName"] } + ) + .refine( + (data) => { + if (data.setHostHeader) { + return tlsNameSchema.safeParse(data.setHostHeader).success; + } + return true; + }, + { + message: "Invalid custom Host Header value. Use domain name format, or save empty to unset the custom Host Header", + path: ["tlsServerName"] + } ); -const TransferFormSchema = z.object({ - siteId: z.number() -}); - type GeneralFormValues = z.infer; type TransferFormValues = z.infer; +type AdvancedFormValues = z.infer; export default function GeneralForm() { const [formKey, setFormKey] = useState(0); @@ -159,8 +178,17 @@ export default function GeneralForm() { proxyPort: resource.proxyPort ? resource.proxyPort : undefined, http: resource.http, isBaseDomain: resource.isBaseDomain ? true : false, - domainId: resource.domainId || undefined, - tlsServerName: resource.http ? resource.tlsServerName || "" : undefined + domainId: resource.domainId || undefined + }, + mode: "onChange" + }); + + const advancedForm = useForm({ + resolver: zodResolver(AdvancedFormSchema), + defaultValues: { + http: resource.http, + tlsServerName: resource.http ? resource.tlsServerName || "" : undefined, + setHostHeader: resource.http ? resource.setHostHeader || "" : undefined }, mode: "onChange" }); @@ -224,8 +252,7 @@ export default function GeneralForm() { subdomain: data.http ? data.subdomain : undefined, proxyPort: data.proxyPort, isBaseDomain: data.http ? data.isBaseDomain : undefined, - domainId: data.http ? data.domainId : undefined, - tlsServerName: data.http ? data.tlsServerName : undefined + domainId: data.http ? data.domainId : undefined } ) .catch((e) => { @@ -252,8 +279,7 @@ export default function GeneralForm() { subdomain: data.subdomain, proxyPort: data.proxyPort, isBaseDomain: data.isBaseDomain, - fullDomain: resource.fullDomain, - tlsServerName: data.tlsServerName + fullDomain: resource.fullDomain }); router.refresh(); @@ -295,6 +321,46 @@ export default function GeneralForm() { setTransferLoading(false); } + async function onSubmitAdvanced(data: AdvancedFormValues) { + setSaveLoading(true); + + const res = await api + .post>( + `resource/${resource?.resourceId}`, + { + tlsServerName: data.http ? data.tlsServerName : undefined, + setHostHeader: data.http ? data.setHostHeader : undefined + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to update resource", + description: formatAxiosError( + e, + "An error occurred while updating the resource" + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: "Resource updated", + description: "The resource has been updated successfully" + }); + + const resource = res.data.data; + + updateResource({ + tlsServerName: data.tlsServerName, + setHostHeader: data.setHostHeader + }); + + router.refresh(); + } + setSaveLoading(false); + } + async function toggleResourceEnabled(val: boolean) { const res = await api .post>( @@ -561,27 +627,7 @@ export default function GeneralForm() { )} /> )} - {/* New TLS Server Name Field */}
-
- - TLS Server Name (optional) - - ( - - - - - - - )} - /> -
)} @@ -637,6 +683,81 @@ export default function GeneralForm() { + {resource.http && ( + <> + + + Advanced + + Adjust advanced settings for the resource, like customize the Host Header or set a TLS Server Name for SNI based routing. + + + + +
+ + {/* New TLS Server Name Field */} +
+ + TLS Server Name (optional) + + ( + + + + + + + )} + /> +
+ {/* New Custom Host Header Field */} +
+ + Custom Host Header (optional) + + ( + + + + + + + )} + /> +
+
+ +
+
+ + + + +
+ + )} From b59c6e377a4bf4cf43d71322b0d4c5b235a3198e Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:27:48 +0200 Subject: [PATCH 07/53] German translation of section Authentication size --- internationalization/de.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internationalization/de.md b/internationalization/de.md index 1acd5b12..c84249f7 100644 --- a/internationalization/de.md +++ b/internationalization/de.md @@ -1,3 +1,23 @@ +## Authentication Site + +| EN | DE | Notes | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- | +| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Bereitgestellt von [Pangolin](https://github.com/fosrl/pangolin) | | +| Authentication Required | Authentifizierung erforderlich | | +| Choose your preferred method to access {resource} | Wählen Sie Ihre bevorzugte Methode, um auf {resource} zuzugreifen | | +| PIN | PIN | | +| User | Benutzer | | +| 6-digit PIN Code | 6-stelliger PIN-Code | pin login | +| Login in with PIN | Mit PIN anmelden | pin login | +| Email | E-Mail | user login | +| Enter your email | Geben Sie Ihre E-Mail-Adresse ein | user login | +| Password | Passwort | user login | +| Enter your password | Geben Sie Ihr Passwort ein | user login | +| Forgot your password? | Passwort vergessen? | user login | +| Log in | Anmelden | user login | + +--- + ## Login site | EN | DE | Notes | From eb9675c6cf658a930b6d6a72d1f806b74e04b4b2 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:30:45 +0200 Subject: [PATCH 08/53] Turkish translation of section authentication site --- internationalization/tr.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 internationalization/tr.md diff --git a/internationalization/tr.md b/internationalization/tr.md new file mode 100644 index 00000000..f3df8dc6 --- /dev/null +++ b/internationalization/tr.md @@ -0,0 +1,17 @@ +## Authentication Site + +| EN | TR | Notes | +| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- | +| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Pangolin Tarafından Destekleniyor | | +| Authentication Required | Kimlik Doğrulaması Gerekli | | +| Choose your preferred method to access {resource} | {resource}'a erişmek için tercih ettiğiniz yöntemi seçin | | +| PIN | PIN | | +| User | Kullanıcı | | +| 6-digit PIN Code | 6 haneli PIN Kodu | pin login | +| Login in with PIN | PIN ile Giriş Yap | pin login | +| Email | E-posta | user login | +| Enter your email | E-postanızı girin | user login | +| Password | Şifre | user login | +| Enter your password | Şifrenizi girin | user login | +| Forgot your password? | Şifrenizi mi unuttunuz? | user login | +| Log in | Giriş Yap | user login | From 57b96adcd064aac327f073ea85b1559f6e8e7649 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:31:36 +0200 Subject: [PATCH 09/53] turkish translation of section login site --- internationalization/tr.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index f3df8dc6..f972460f 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -15,3 +15,18 @@ | Enter your password | Şifrenizi girin | user login | | Forgot your password? | Şifrenizi mi unuttunuz? | user login | | Log in | Giriş Yap | user login | + +--- + +## Login site + +| EN | TR | Notes | +| --------------------- | ------------------------------------------------------ | ----------- | +| Welcome to Pangolin | Pangolin'e Hoşgeldiniz | | +| Log in to get started | Başlamak için giriş yapın | | +| Email | E-posta | | +| Enter your email | E-posta adresinizi girin | placeholder | +| Password | Şifre | | +| Enter your password | Şifrenizi girin | placeholder | +| Forgot your password? | Şifrenizi mi unuttunuz? | | +| Log in | Giriş Yap | | From 499f75edd1d166acc33b8a7447c6fb7edf287dfb Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:32:26 +0200 Subject: [PATCH 10/53] turkish translation of section organization site after login --- internationalization/tr.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index f972460f..e6ad4ce8 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -30,3 +30,12 @@ | Enter your password | Şifrenizi girin | placeholder | | Forgot your password? | Şifrenizi mi unuttunuz? | | | Log in | Giriş Yap | | + +--- + +# Organization site after successful login + +| EN | TR | Notes | +| ----------------------------------------- | ------------------------------------------------------------------- | ----- | +| Welcome to Pangolin | Pangolin'e Hoşgeldiniz | | +| You're a member of {number} organization. | {number} organizasyonunun üyesiniz. | | From 4707722e6e8ccbef9bd78a42cec5ef6c169bb24a Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:33:06 +0200 Subject: [PATCH 11/53] turkish translation of shared header --- internationalization/tr.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index e6ad4ce8..6e77887d 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -39,3 +39,15 @@ | ----------------------------------------- | ------------------------------------------------------------------- | ----- | | Welcome to Pangolin | Pangolin'e Hoşgeldiniz | | | You're a member of {number} organization. | {number} organizasyonunun üyesiniz. | | + +--- + +## Shared Header, Navbar and Footer + +##### Header + +| EN | TR | Notes | +| ------------------- | -------------------------- | ----- | +| Documentation | Dokümantasyon | | +| Support | Destek | | +| Organization {name} | Organizasyon {name} | | From 285ad45a0e07dc9f7d7ec7ca7481b0fb08530561 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:33:41 +0200 Subject: [PATCH 12/53] turkish translation of organization selector --- internationalization/tr.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 6e77887d..91aeaf02 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -51,3 +51,12 @@ | Documentation | Dokümantasyon | | | Support | Destek | | | Organization {name} | Organizasyon {name} | | + +##### Organization selector + +| EN | TR | Notes | +| ---------------- | ---------------------- | ----- | +| Search… | Ara… | | +| Create | Oluştur | | +| New Organization | Yeni Organizasyon | | +| Organizations | Organizasyonlar | | From 51ac815b23429fd3a57e61020fc42c5b617a02f3 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:34:49 +0200 Subject: [PATCH 13/53] turkish translation of navbar --- internationalization/tr.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 91aeaf02..8a8bf735 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -60,3 +60,13 @@ | Create | Oluştur | | | New Organization | Yeni Organizasyon | | | Organizations | Organizasyonlar | | + +##### Navbar + +| EN | TR | Notes | +| --------------- | ------------------------------- | ----- | +| Sites | Siteler | | +| Resources | Kaynaklar | | +| User & Roles | Kullanıcılar ve Roller | | +| Shareable Links | Paylaşılabilir Linkler | | +| General | Genel | | From 01da3b3225c2b612102c59d734b7e3a93d36edf7 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:36:18 +0200 Subject: [PATCH 14/53] turkish translation of footer --- internationalization/tr.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 8a8bf735..83f1cc5b 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -70,3 +70,15 @@ | User & Roles | Kullanıcılar ve Roller | | | Shareable Links | Paylaşılabilir Linkler | | | General | Genel | | + +##### Footer + +| EN | TR | Notes | +| ------------------------- | ------------------------------------------------ | -------------------- | +| Page {number} of {number} | Sayfa {number} / {number} | | +| Rows per page | Sayfa başına satırlar | | +| Pangolin | Pangolin | Footer'da yer alır | +| Built by Fossorial | Fossorial tarafından oluşturuldu | Footer'da yer alır | +| Open Source | Açık Kaynak | Footer'da yer alır | +| Documentation | Dokümantasyon | Footer'da yer alır | +| {version} | {version} | Footer'da yer alır | From 442775ac90da5412871b114e13cb7363e1253c2c Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:37:56 +0200 Subject: [PATCH 15/53] turkish translation of hero section --- internationalization/tr.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 83f1cc5b..1f1831a8 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -82,3 +82,20 @@ | Open Source | Açık Kaynak | Footer'da yer alır | | Documentation | Dokümantasyon | Footer'da yer alır | | {version} | {version} | Footer'da yer alır | + +--- + +## Main “Sites” + +##### “Hero” section + +| EN | TR | Notes | +| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ----- | +| Newt (Recommended) | Newt (Tavsiye Edilen) | | +| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | En iyi kullanıcı deneyimi için Newt'i kullanın. Newt, arka planda WireGuard kullanır ve Pangolin kontrol paneli üzerinden özel ağınızdaki kaynaklarınıza LAN adresleriyle erişmenizi sağlar. | | +| Runs in Docker | Docker üzerinde çalışır | | +| Runs in shell on macOS, Linux, and Windows | macOS, Linux ve Windows’ta komut satırında çalışır | | +| Install Newt | Newt'i Yükle | | +| Basic WireGuard
| Temel WireGuard
| | +| Compatible with all WireGuard clients
| Tüm WireGuard istemcileriyle uyumlu
| | +| Manual configuration required | Manuel yapılandırma gereklidir | | From 12f627711c5a6e659bb75283799f3ce4470c954a Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:38:44 +0200 Subject: [PATCH 16/53] turkish translation of content --- internationalization/tr.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 1f1831a8..86966f1a 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -99,3 +99,23 @@ | Basic WireGuard
| Temel WireGuard
| | | Compatible with all WireGuard clients
| Tüm WireGuard istemcileriyle uyumlu
| | | Manual configuration required | Manuel yapılandırma gereklidir | | + +##### Content + +| EN | TR | Notes | +| --------------------------------------------------------- | --------------------------------------------------------------------------- | ------------ | +| Manage Sites | Siteleri Yönet | | +| Allow connectivity to your network through secure tunnels | Güvenli tüneller aracılığıyla ağınıza bağlantı sağlayın | | +| Search sites | Siteleri ara | placeholder | +| Add Site | Site Ekle | | +| Name | Ad | Table Header | +| Online | Çevrimiçi | Table Header | +| Site | Site | Table Header | +| Data In | Gelen Veri | Table Header | +| Data Out | Giden Veri | Table Header | +| Connection Type | Bağlantı Türü | Table Header | +| Online | Çevrimiçi | Site state | +| Offline | Çevrimdışı | Site state | +| Edit → | Düzenle → | | +| View settings | Ayarları Görüntüle | Popup | +| Delete | Sil | Popup | From 45a75d0bee090f363457613a44d47acf70d3cd43 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:39:42 +0200 Subject: [PATCH 17/53] turkish translation of Add Site Popup --- internationalization/tr.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 86966f1a..2ba19baf 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -119,3 +119,23 @@ | Edit → | Düzenle → | | | View settings | Ayarları Görüntüle | Popup | | Delete | Sil | Popup | + +##### Add Site Popup + +| EN | TR | Notes | +| ------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ----------- | +| Create Site | Site Oluştur | | +| Create a new site to start connection for this site | Bu site için bağlantıyı başlatmak amacıyla yeni bir site oluşturun | | +| Name | Ad | | +| Site name | Site adı | placeholder | +| This is the name that will be displayed for this site. | Bu, site için görüntülenecek addır. | desc | +| Method | Yöntem | | +| Local | Yerel | | +| Newt | Newt | | +| WireGuard | WireGuard | | +| This is how you will expose connections. | Bağlantılarınızı bu şekilde açığa çıkaracaksınız. | | +| You will only be able to see the configuration once. | Yapılandırmayı yalnızca bir kez görüntüleyebilirsiniz. | | +| Learn how to install Newt on your system | Sisteminizde Newt'in nasıl kurulacağını öğrenin | | +| I have copied the config | Yapılandırmayı kopyaladım | | +| Create Site | Site Oluştur | | +| Close | Kapat | | From 6b5674a107c12170e58fa67f19d9b50c1b97476a Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:42:30 +0200 Subject: [PATCH 18/53] turkish translation of main ressources / hero --- internationalization/tr.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 2ba19baf..df2dfa4a 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -139,3 +139,17 @@ | I have copied the config | Yapılandırmayı kopyaladım | | | Create Site | Site Oluştur | | | Close | Kapat | | + +--- + +## Main “Resources” + +##### “Hero” section + +| EN | TR | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----- | +| Resources | Kaynaklar | | +| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | Kaynaklar, özel ağınızda çalışan uygulamalar için proxy sunucularıdır. Özel ağınızdaki her HTTP veya HTTPS uygulaması için bir kaynak oluşturun. Her kaynağın, şifrelenmiş WireGuard tüneli üzerinden özel ve güvenli bağlantı sağlamak üzere bir siteyle ilişkili olması gerekir. | | +| Secure connectivity with WireGuard encryption | WireGuard şifrelemesiyle güvenli bağlantı | | +| Configure multiple authentication methods | Birden çok kimlik doğrulama yöntemini yapılandırın | | +| User and role-based access control | Kullanıcı ve role dayalı erişim kontrolü | | From 0454f0938322751c0843668f42799f2a7da8ae2e Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:43:12 +0200 Subject: [PATCH 19/53] turkish translation of main->content --- internationalization/tr.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index df2dfa4a..21dd5a44 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -153,3 +153,19 @@ | Secure connectivity with WireGuard encryption | WireGuard şifrelemesiyle güvenli bağlantı | | | Configure multiple authentication methods | Birden çok kimlik doğrulama yöntemini yapılandırın | | | User and role-based access control | Kullanıcı ve role dayalı erişim kontrolü | | + +##### Content + +| EN | TR | Notes | +| -------------------------------------------------- | ------------------------------------------------------------- | -------------------- | +| Manage Resources | Kaynakları Yönet | | +| Create secure proxies to your private applications | Özel uygulamalarınız için güvenli proxy’ler oluşturun | | +| Search resources | Kaynakları ara | placeholder | +| Name | Ad | | +| Site | Site | | +| Full URL | Tam URL | | +| Authentication | Kimlik Doğrulama | | +| Not Protected | Korunmayan | authentication state | +| Protected | Korunan | authentication state | +| Edit → | Düzenle → | | +| Add Resource | Kaynak Ekle | | From 976aaca287988e5961d0eab7bd536118cc674d00 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:43:56 +0200 Subject: [PATCH 20/53] turkish translation of Add Resource Popup --- internationalization/tr.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 21dd5a44..464ce77f 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -169,3 +169,21 @@ | Protected | Korunan | authentication state | | Edit → | Düzenle → | | | Add Resource | Kaynak Ekle | | + +##### Add Resource Popup + +| EN | TR | Notes | +| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------- | +| Create Resource | Kaynak Oluştur | | +| Create a new resource to proxy request to your app | Uygulamanıza gelen istekleri yönlendirmek için yeni bir kaynak oluşturun | | +| Name | Ad | | +| My Resource | Kaynağım | name placeholder | +| This is the name that will be displayed for this resource. | Bu, kaynağın görüntülenecek adıdır. | | +| Subdomain | Alt alan adı | | +| Enter subdomain | Alt alan adını girin | | +| This is the fully qualified domain name that will be used to access the resource. | Kaynağa erişmek için kullanılacak tam nitelikli alan adıdır. | | +| Site | Site | | +| Search site… | Site ara… | Site selector popup | +| This is the site that will be used in the dashboard. | Kontrol panelinde kullanılacak sitedir. | | +| Create Resource | Kaynak Oluştur | | +| Close | Kapat | | From 35daf42a556dc777e881dea45bb70c247cfff5bf Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:45:14 +0200 Subject: [PATCH 21/53] =?UTF-8?q?turkish=20translation=20of=20Main=20?= =?UTF-8?q?=E2=80=9CUser=20&=20Roles=E2=80=9D=20->=20Content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internationalization/tr.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 464ce77f..32fd2fd0 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -187,3 +187,36 @@ | This is the site that will be used in the dashboard. | Kontrol panelinde kullanılacak sitedir. | | | Create Resource | Kaynak Oluştur | | | Close | Kapat | | + +--- + +## Main “User & Roles” + +##### Content + +| EN | TR | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ----------------------------- | +| Manage User & Roles | Kullanıcılar ve Rolleri Yönet | | +| Invite users and add them to roles to manage access to your organization | Organizasyonunuza erişimi yönetmek için kullanıcıları davet edin ve rollere atayın | | +| Users | Kullanıcılar | sidebar item | +| Roles | Roller | sidebar item | +| **User tab** | **Kullanıcı Sekmesi** | | +| Search users | Kullanıcıları ara | placeholder | +| Invite User | Kullanıcı Davet Et | addbutton | +| Email | E-posta | table header | +| Status | Durum | table header | +| Role | Rol | table header | +| Confirmed | Onaylandı | account status | +| Not confirmed (?) | Onaylanmadı (?) | account status | +| Owner | Sahip | role | +| Admin | Yönetici | role | +| Member | Üye | role | +| **Roles Tab** | **Roller Sekmesi** | | +| Search roles | Rolleri ara | placeholder | +| Add Role | Rol Ekle | addbutton | +| Name | Ad | table header | +| Description | Açıklama | table header | +| Admin | Yönetici | role | +| Member | Üye | role | +| Admin role with the most permissions | En fazla yetkiye sahip yönetici rolü | admin role desc | +| Members can only view resources | Üyeler yalnızca kaynakları görüntüleyebilir | member role desc | From 8fb003d7cefd190c96d1e3a7ee5a051855640c9d Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:45:47 +0200 Subject: [PATCH 22/53] turkish translation of Invite User popup --- internationalization/tr.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 32fd2fd0..170e9e32 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -220,3 +220,23 @@ | Member | Üye | role | | Admin role with the most permissions | En fazla yetkiye sahip yönetici rolü | admin role desc | | Members can only view resources | Üyeler yalnızca kaynakları görüntüleyebilir | member role desc | + +##### Invite User popup + +| EN | TR | Notes | +| ----------------- | ----------------------------------------------------------------------- | ----------- | +| Invite User | Kullanıcı Davet Et | | +| Email | E-posta | | +| Enter an email | Bir e-posta adresi girin | placeholder | +| Role | Rol | | +| Select role | Rol seçin | placeholder | +| Gültig für | Geçerlilik Süresi | | +| 1 day | 1 gün | | +| 2 days | 2 gün | | +| 3 days | 3 gün | | +| 4 days | 4 gün | | +| 5 days | 5 gün | | +| 6 days | 6 gün | | +| 7 days | 7 gün | | +| Create Invitation | Davetiye Oluştur | | +| Close | Kapat | | From c286c28d46247c437696d0d949d96252c32c2edb Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:46:38 +0200 Subject: [PATCH 23/53] =?UTF-8?q?turkish=20translation=20of=20Main=20?= =?UTF-8?q?=E2=80=9CShareable=20Links=E2=80=9D=20->=20hero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internationalization/tr.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 170e9e32..452c89f8 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -240,3 +240,17 @@ | 7 days | 7 gün | | | Create Invitation | Davetiye Oluştur | | | Close | Kapat | | + +--- + +## Main “Shareable Links” + +##### “Hero” section + +| EN | TR | Notes | +| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----- | +| Shareable Links | Paylaşılabilir Bağlantılar | | +| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Kaynaklarınıza paylaşılabilir bağlantılar oluşturun. Bağlantılar, kaynağınıza geçici veya sınırsız erişim sağlar. Oluştururken bağlantının geçerlilik süresini ayarlayabilirsiniz. | | +| Easy to create and share | Oluşturması ve paylaşması kolay | | +| Configurable expiration duration | Yapılandırılabilir geçerlilik süresi | | +| Secure and revocable | Güvenli ve iptal edilebilir | | From f60f15345f31e87b3d1a1357bb48673b25afbb18 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:47:17 +0200 Subject: [PATCH 24/53] turkish translation of shareable links -> Content --- internationalization/tr.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 452c89f8..de922c81 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -254,3 +254,17 @@ | Easy to create and share | Oluşturması ve paylaşması kolay | | | Configurable expiration duration | Yapılandırılabilir geçerlilik süresi | | | Secure and revocable | Güvenli ve iptal edilebilir | | + +##### Content + +| EN | TR | Notes | +| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | -------------- | +| Manage Shareable Links | Paylaşılabilir Bağlantıları Yönet | | +| Create shareable links to grant temporary or permanent access to your resources | Kaynaklarınıza geçici veya kalıcı erişim sağlamak için paylaşılabilir bağlantılar oluşturun | | +| Search links | Bağlantıları ara | placeholder | +| Create Share Link | Bağlantı Oluştur | addbutton | +| Resource | Kaynak | table header | +| Title | Başlık | table header | +| Created | Oluşturulma Tarihi | table header | +| Expires | Son Kullanma Tarihi | table header | +| No links. Create one to get started. | Bağlantı yok. Başlamak için bir tane oluşturun. | table placeholder | From 65b29161a0b983e0755ba7ac3d48218b2a6fb8da Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:47:53 +0200 Subject: [PATCH 25/53] turkish translation of Create Shareable Link popup --- internationalization/tr.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index de922c81..7d44bfab 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -268,3 +268,25 @@ | Created | Oluşturulma Tarihi | table header | | Expires | Son Kullanma Tarihi | table header | | No links. Create one to get started. | Bağlantı yok. Başlamak için bir tane oluşturun. | table placeholder | + +##### Create Shareable Link popup + +| EN | TR | Notes | +| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| Create Shareable Link | Paylaşılabilir Bağlantı Oluştur | | +| Anyone with this link can access the resource | Bu bağlantıya sahip olan herkes kaynağa erişebilir | | +| Resource | Kaynak | | +| Select resource | Kaynak seçin | | +| Search resources… | Kaynak ara… | resource selector popup | +| Title (optional) | Başlık (isteğe bağlı) | | +| Enter title | Başlık girin | placeholder | +| Expire in | Sona Erme Süresi | | +| Minutes | Dakika | | +| Hours | Saat | | +| Days | Gün | | +| Months | Ay | | +| Years | Yıl | | +| Never expire | Asla sona erme | | +| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | Bağlantının geçerlilik süresi, bağlantının ne kadar süreyle kullanılabilir olacağını ve kaynağa erişim sağlayacağını belirler. Bu sürenin sonunda bağlantı çalışmaz hale gelir ve bağlantıyı kullananlar kaynağa erişimini kaybeder. | | +| Create Link | Bağlantı Oluştur | | +| Close | Kapat | | From 787ec50a9cc9d098595d803bfcd2ba79731c94d2 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 16 Apr 2025 21:48:50 +0200 Subject: [PATCH 26/53] =?UTF-8?q?turkish=20translation=20of=20Main=20?= =?UTF-8?q?=E2=80=9CGeneral=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internationalization/tr.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/internationalization/tr.md b/internationalization/tr.md index 7d44bfab..9e5bd274 100644 --- a/internationalization/tr.md +++ b/internationalization/tr.md @@ -290,3 +290,21 @@ | Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | Bağlantının geçerlilik süresi, bağlantının ne kadar süreyle kullanılabilir olacağını ve kaynağa erişim sağlayacağını belirler. Bu sürenin sonunda bağlantı çalışmaz hale gelir ve bağlantıyı kullananlar kaynağa erişimini kaybeder. | | | Create Link | Bağlantı Oluştur | | | Close | Kapat | | + +--- + +## Main “General” + +| EN | TR | Notes | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------ | +| General | Genel | | +| Configure your organization’s general settings | Organizasyonunuzun genel ayarlarını yapılandırın | | +| General | Genel | sidebar item | +| Organization Settings | Organizasyon Ayarları | | +| Manage your organization details and configuration | Organizasyonunuzun detaylarını ve yapılandırmasını yönetin | | +| Name | Ad | | +| This is the display name of the org | Bu, organizasyonunuzun görüntülenecek adıdır. | | +| Save Settings | Ayarları Kaydet | | +| Danger Zone | Tehlikeli Bölge | | +| Once you delete this org, there is no going back. Please be certain. | Bu organizasyonu sildikten sonra geri dönüş yoktur. Lütfen emin olun. | | +| Delete Organization Data | Organizasyon Verilerini Sil | | From bf8bb1a0df97b493bc1a9162bc422521b8093e5c Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 20 Apr 2025 20:50:50 -0400 Subject: [PATCH 27/53] adjustment to pr --- server/db/schemas/schema.ts | 4 +- server/routers/traefik/getTraefikConfig.ts | 2 +- .../resources/[resourceId]/general/page.tsx | 162 +++++++++--------- 3 files changed, 85 insertions(+), 83 deletions(-) diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index 5263161b..ecb5cd35 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -78,8 +78,8 @@ export const resources = sqliteTable("resources", { .notNull() .default(false), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), - tlsServerName: text("tlsServerName").notNull().default(""), - setHostHeader: text("setHostHeader").notNull().default("") + tlsServerName: text("tlsServerName"), + setHostHeader: text("setHostHeader") }); export const targets = sqliteTable("targets", { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 8a952546..d6053430 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -290,7 +290,7 @@ export async function traefikConfigProvider( } config_output.http.serversTransports![transportName] = { serverName: resource.tlsServerName, - //unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings + //unfortunately the following needs to be set. traefik doesn't merge the default serverTransport settings // if defined in the static config and here. if not set, self-signed certs won't work insecureSkipVerify: true }; diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index 05d263e6..cf75a426 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -122,8 +122,9 @@ const AdvancedFormSchema = z } return true; }, - { - message: "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", + { + message: + "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", path: ["tlsServerName"] } ) @@ -134,8 +135,9 @@ const AdvancedFormSchema = z } return true; }, - { - message: "Invalid custom Host Header value. Use domain name format, or save empty to unset the custom Host Header", + { + message: + "Invalid custom Host Header value. Use domain name format, or save empty to unset the custom Host Header", path: ["tlsServerName"] } ); @@ -187,8 +189,12 @@ export default function GeneralForm() { resolver: zodResolver(AdvancedFormSchema), defaultValues: { http: resource.http, - tlsServerName: resource.http ? resource.tlsServerName || "" : undefined, - setHostHeader: resource.http ? resource.setHostHeader || "" : undefined + tlsServerName: resource.http + ? resource.tlsServerName || "" + : undefined, + setHostHeader: resource.http + ? resource.setHostHeader || "" + : undefined }, mode: "onChange" }); @@ -683,81 +689,77 @@ export default function GeneralForm() {
- {resource.http && ( - <> - - - Advanced - - Adjust advanced settings for the resource, like customize the Host Header or set a TLS Server Name for SNI based routing. - - - - -
- - {/* New TLS Server Name Field */} -
- - TLS Server Name (optional) - - ( - - - - - - - )} - /> -
- {/* New Custom Host Header Field */} -
- - Custom Host Header (optional) - - ( - - - - - - - )} - /> -
-
- -
-
+ {resource.http && ( + <> + + + + Advanced + + + Adjust advanced settings for the resource, + like customize the Host Header or set a TLS + Server Name for SNI based routing. + + + + +
+ + + TLS Server Name (optional) + + ( + + + + + + + )} + /> - - - - - - )} + + Custom Host Header (optional) + + ( + + + + + + + )} + /> + + +
+
+ + + + +
+ + )} From c837899d82895bbd3007ad4d55c0ca54c54df2a5 Mon Sep 17 00:00:00 2001 From: Taylan Date: Mon, 21 Apr 2025 11:29:56 +0200 Subject: [PATCH 28/53] Relaxed health check for crowdsec and dependance to traefik --- install/config/crowdsec/docker-compose.yml | 3 +++ install/config/docker-compose.yml | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/install/config/crowdsec/docker-compose.yml b/install/config/crowdsec/docker-compose.yml index 20c69387..28470d14 100644 --- a/install/config/crowdsec/docker-compose.yml +++ b/install/config/crowdsec/docker-compose.yml @@ -9,6 +9,9 @@ services: PARSERS: crowdsecurity/whitelists ENROLL_TAGS: docker healthcheck: + interval: 10s + retries: 15 + timeout: 10s test: ["CMD", "cscli", "capi", "status"] labels: - "traefik.enable=false" # Disable traefik for crowdsec diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 496b0138..1cccffa3 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -48,6 +48,10 @@ services: depends_on: pangolin: condition: service_healthy +{{if .DoCrowdsecInstall}} + crowdsec: + condition: service_healthy +{{end}} command: - --configFile=/etc/traefik/traefik_config.yml volumes: From 181071e4f63b28d8e1ac3ecd198f87af4387a53f Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 23 Apr 2025 18:35:15 +0200 Subject: [PATCH 29/53] refactoring multiple used code parts, cleared some warnings and added more error checking --- install/main.go | 194 +++++++++++++++--------------------------------- 1 file changed, 58 insertions(+), 136 deletions(-) diff --git a/install/main.go b/install/main.go index 47c846cf..8decf147 100644 --- a/install/main.go +++ b/install/main.go @@ -87,7 +87,15 @@ func main() { if isDockerInstalled() { if readBool(reader, "Would you like to install and start the containers?", true) { - pullAndStartContainers() + if err := pullContainers(); err != nil { + fmt.Println("Error: ", err) + return + } + + if err := startContainers(); err != nil { + fmt.Println("Error: ", err) + return + } } } } else { @@ -427,24 +435,24 @@ func installDocker() error { apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin `, dockerArch)) case strings.Contains(osRelease, "ID=fedora"): - installCmd = exec.Command("bash", "-c", fmt.Sprintf(` + installCmd = exec.Command("bash", "-c", ` dnf -y install dnf-plugins-core && dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo && dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin - `)) + `) case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"): installCmd = exec.Command("bash", "-c", ` zypper install -y docker docker-compose && systemctl enable docker `) case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"): - installCmd = exec.Command("bash", "-c", fmt.Sprintf(` + installCmd = exec.Command("bash", "-c", ` dnf remove -y runc && dnf -y install yum-utils && dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo && dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin && systemctl enable docker - `)) + `) case strings.Contains(osRelease, "ID=amzn"): installCmd = exec.Command("bash", "-c", ` yum update -y && @@ -468,165 +476,79 @@ func isDockerInstalled() bool { return true } -func getCommandString(useNewStyle bool) string { - if useNewStyle { - return "'docker compose'" - } - return "'docker-compose'" -} - -func pullAndStartContainers() error { - fmt.Println("Starting containers...") - - // Check which docker compose command is available +// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied +func executeDockerComposeCommandWithArgs(args ...string) error { + var cmd *exec.Cmd var useNewStyle bool + + if !isDockerInstalled() { + return fmt.Errorf("docker is not installed") + } + checkCmd := exec.Command("docker", "compose", "version") if err := checkCmd.Run(); err == nil { useNewStyle = true } else { - // Check if docker-compose (old style) is available checkCmd = exec.Command("docker-compose", "version") - if err := checkCmd.Run(); err != nil { - return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err) - } - } - - // Helper function to execute docker compose commands - executeCommand := func(args ...string) error { - var cmd *exec.Cmd - if useNewStyle { - cmd = exec.Command("docker", append([]string{"compose"}, args...)...) + if err := checkCmd.Run(); err == nil { + useNewStyle = false } else { - cmd = exec.Command("docker-compose", args...) + return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available") } - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() + } + + if useNewStyle { + cmd = exec.Command("docker", append([]string{"compose"}, args...)...) + } else { + cmd = exec.Command("docker-compose", args...) } - // Pull containers - fmt.Printf("Using %s command to pull containers...\n", getCommandString(useNewStyle)) - if err := executeCommand("-f", "docker-compose.yml", "pull"); err != nil { - return fmt.Errorf("failed to pull containers: %v", err) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// pullContainers pulls the containers using the appropriate command. +func pullContainers() error { + fmt.Println("Pulling the container images...") + + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { + return fmt.Errorf("failed to pull the containers: %v", err) } - // Start containers - fmt.Printf("Using %s command to start containers...\n", getCommandString(useNewStyle)) - if err := executeCommand("-f", "docker-compose.yml", "up", "-d"); err != nil { + return nil +} + +// startContainers starts the containers using the appropriate command. +func startContainers() error { + fmt.Println("Starting containers...") + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil { return fmt.Errorf("failed to start containers: %v", err) } return nil } -// bring containers down +// stopContainers stops the containers using the appropriate command. func stopContainers() error { fmt.Println("Stopping containers...") - - // Check which docker compose command is available - var useNewStyle bool - checkCmd := exec.Command("docker", "compose", "version") - if err := checkCmd.Run(); err == nil { - useNewStyle = true - } else { - // Check if docker-compose (old style) is available - checkCmd = exec.Command("docker-compose", "version") - if err := checkCmd.Run(); err != nil { - return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err) - } - } - - // Helper function to execute docker compose commands - executeCommand := func(args ...string) error { - var cmd *exec.Cmd - if useNewStyle { - cmd = exec.Command("docker", append([]string{"compose"}, args...)...) - } else { - cmd = exec.Command("docker-compose", args...) - } - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - } - - if err := executeCommand("-f", "docker-compose.yml", "down"); err != nil { + + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil { return fmt.Errorf("failed to stop containers: %v", err) } return nil } -// just start containers -func startContainers() error { - fmt.Println("Starting containers...") - - // Check which docker compose command is available - var useNewStyle bool - checkCmd := exec.Command("docker", "compose", "version") - if err := checkCmd.Run(); err == nil { - useNewStyle = true - } else { - // Check if docker-compose (old style) is available - checkCmd = exec.Command("docker-compose", "version") - if err := checkCmd.Run(); err != nil { - return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err) - } - } - - // Helper function to execute docker compose commands - executeCommand := func(args ...string) error { - var cmd *exec.Cmd - if useNewStyle { - cmd = exec.Command("docker", append([]string{"compose"}, args...)...) - } else { - cmd = exec.Command("docker-compose", args...) - } - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - } - - if err := executeCommand("-f", "docker-compose.yml", "up", "-d"); err != nil { - return fmt.Errorf("failed to start containers: %v", err) - } - - return nil -} - +// restartContainer restarts a specific container using the appropriate command. func restartContainer(container string) error { - fmt.Printf("Restarting %s container...\n", container) + fmt.Println("Restarting containers...") + + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil { + return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) + } - // Check which docker compose command is available - var useNewStyle bool - checkCmd := exec.Command("docker", "compose", "version") - if err := checkCmd.Run(); err == nil { - useNewStyle = true - } else { - // Check if docker-compose (old style) is available - checkCmd = exec.Command("docker-compose", "version") - if err := checkCmd.Run(); err != nil { - return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available: %v", err) - } - } - - // Helper function to execute docker compose commands - executeCommand := func(args ...string) error { - var cmd *exec.Cmd - if useNewStyle { - cmd = exec.Command("docker", append([]string{"compose"}, args...)...) - } else { - cmd = exec.Command("docker-compose", args...) - } - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - } - - if err := executeCommand("-f", "docker-compose.yml", "restart", container); err != nil { - return fmt.Errorf("failed to restart %s container: %v", container, err) - } - - return nil + return nil } func copyFile(src, dst string) error { From 87915f29f6e7095baa3e1ceb0857b2cc17d17c8f Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 23 Apr 2025 18:37:36 +0200 Subject: [PATCH 30/53] correct formated the file --- install/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/install/main.go b/install/main.go index 8decf147..157a3abe 100644 --- a/install/main.go +++ b/install/main.go @@ -544,11 +544,11 @@ func stopContainers() error { func restartContainer(container string) error { fmt.Println("Restarting containers...") - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil { - return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) - } + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil { + return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) + } - return nil + return nil } func copyFile(src, dst string) error { From 6fd1dbc638a83e4a0a20459217f908c9f21e1bf3 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 23 Apr 2025 18:39:48 +0200 Subject: [PATCH 31/53] some more warning about indirect requirements --- install/go.mod | 7 ++++--- install/go.sum | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/install/go.mod b/install/go.mod index 536ac2dd..1d12aa12 100644 --- a/install/go.mod +++ b/install/go.mod @@ -3,7 +3,8 @@ module installer go 1.23.0 require ( - golang.org/x/sys v0.29.0 // indirect - golang.org/x/term v0.28.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + golang.org/x/term v0.28.0 + gopkg.in/yaml.v3 v3.0.1 ) + +require golang.org/x/sys v0.29.0 // indirect diff --git a/install/go.sum b/install/go.sum index 3316e039..169165e4 100644 --- a/install/go.sum +++ b/install/go.sum @@ -2,6 +2,7 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 6f59d0cd2ddcfb1bd51a7270a9ea99309f2693be Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 23 Apr 2025 13:26:38 -0400 Subject: [PATCH 32/53] add createOrgUser endpoint --- server/auth/actions.ts | 1 + server/routers/external.ts | 17 ++ server/routers/idp/validateOidcCallback.ts | 66 +++---- server/routers/user/createOrgUser.ts | 207 +++++++++++++++++++++ server/routers/user/index.ts | 1 + src/app/admin/idp/AdminIdpTable.tsx | 16 -- src/app/admin/idp/[idpId]/general/page.tsx | 33 ++-- src/app/admin/idp/create/page.tsx | 39 ++-- src/components/SwitchInput.tsx | 3 + 9 files changed, 302 insertions(+), 81 deletions(-) create mode 100644 server/routers/user/createOrgUser.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index b825b64c..251abb45 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -6,6 +6,7 @@ import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; export enum ActionsEnum { + createOrgUser = "createOrgUser", listOrgs = "listOrgs", listUserOrgs = "listUserOrgs", createOrg = "createOrg", diff --git a/server/routers/external.ts b/server/routers/external.ts index 7cc6b490..b543faac 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -26,7 +26,12 @@ import { verifyUserAccess, getUserOrgs, verifyUserIsServerAdmin, +<<<<<<< Updated upstream verifyIsLoggedInUser +======= + verifyIsLoggedInUser, + verifyClientAccess +>>>>>>> Stashed changes } from "@server/middlewares"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { ActionsEnum } from "@server/auth/actions"; @@ -46,6 +51,10 @@ unauthenticated.get("/", (_, res) => { export const authenticated = Router(); authenticated.use(verifySessionUserMiddleware); +<<<<<<< Updated upstream +======= +authenticated.get("/pick-org-defaults", org.pickOrgDefaults); +>>>>>>> Stashed changes authenticated.get("/org/checkId", org.checkId); authenticated.put("/org", getUserOrgs, org.createOrg); @@ -448,7 +457,15 @@ authenticated.delete( user.adminRemoveUser ); +authenticated.put( + "/org/:orgId/user", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.createOrgUser), + user.createOrgUser +); + authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser); + authenticated.get( "/org/:orgId/users", verifyOrgAccess, diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index 43c4c7f5..006c14a4 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -5,11 +5,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { - idp, - idpOidcConfig, - users -} from "@server/db/schemas"; +import { idp, idpOidcConfig, users } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import * as arctic from "arctic"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; @@ -17,6 +13,12 @@ import jmespath from "jmespath"; import jsonwebtoken from "jsonwebtoken"; import config from "@server/lib/config"; import { decrypt } from "@server/lib/crypto"; +import { + createSession, + generateSessionToken, + serializeSessionCookie +} from "@server/auth/sessions/app"; +import { response } from "@server/lib"; const paramsSchema = z .object({ @@ -213,31 +215,31 @@ export async function validateOidcCallback( return next( createHttpError( HttpCode.UNAUTHORIZED, - "User not found in the IdP" + "User not provisioned in the system" ) ); } - // - // const token = generateSessionToken(); - // const sess = await createSession(token, existingUser.userId); - // const isSecure = req.protocol === "https"; - // const cookie = serializeSessionCookie( - // token, - // isSecure, - // new Date(sess.expiresAt) - // ); - // - // res.appendHeader("Set-Cookie", cookie); - // - // return response(res, { - // data: { - // redirectUrl: postAuthRedirectUrl - // }, - // success: true, - // error: false, - // message: "OIDC callback validated successfully", - // status: HttpCode.CREATED - // }); + + const token = generateSessionToken(); + const sess = await createSession(token, existingUser.userId); + const isSecure = req.protocol === "https"; + const cookie = serializeSessionCookie( + token, + isSecure, + new Date(sess.expiresAt) + ); + + res.appendHeader("Set-Cookie", cookie); + + return response(res, { + data: { + redirectUrl: postAuthRedirectUrl + }, + success: true, + error: false, + message: "OIDC callback validated successfully", + status: HttpCode.CREATED + }); } } catch (error) { logger.error(error); @@ -246,13 +248,3 @@ export async function validateOidcCallback( ); } } - -function hydrateOrgMapping( - orgMapping: string | null, - orgId: string -): string | undefined { - if (!orgMapping) { - return undefined; - } - return orgMapping.split("{{orgId}}").join(orgId); -} diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts new file mode 100644 index 00000000..3ca2a5a3 --- /dev/null +++ b/server/routers/user/createOrgUser.ts @@ -0,0 +1,207 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import db from "@server/db"; +import { and, eq } from "drizzle-orm"; +import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db/schemas"; +import { generateId } from "@server/auth/sessions/app"; + +const paramsSchema = z + .object({ + orgId: z.string().nonempty() + }) + .strict(); + +const bodySchema = z + .object({ + email: z.string().email().optional(), + username: z.string().nonempty(), + name: z.string().optional(), + type: z.enum(["internal", "oidc"]).optional(), + idpId: z.number().optional(), + roleId: z.number() + }) + .strict(); + +export type CreateOrgUserResponse = {}; + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/user", + description: "Create an organization user.", + tags: [OpenAPITags.User, OpenAPITags.Org], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function createOrgUser( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + const { username, email, name, type, idpId, roleId } = parsedBody.data; + + const [role] = await db + .select() + .from(roles) + .where(eq(roles.roleId, roleId)); + + if (!role) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Role ID not found") + ); + } + + if (type === "internal") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Internal users are not supported yet" + ) + ); + } else if (type === "oidc") { + if (!idpId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "IDP ID is required for OIDC users" + ) + ); + } + + const [idpRes] = await db + .select() + .from(idp) + .innerJoin(idpOidcConfig, eq(idp.idpId, idpOidcConfig.idpId)) + .where(eq(idp.idpId, idpId)); + + if (!idpRes) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "IDP ID not found") + ); + } + + if (idpRes.idp.type !== "oidc") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "IDP ID is not of type OIDC" + ) + ); + } + + const [existingUser] = await db + .select() + .from(users) + .where(eq(users.username, username)); + + if (existingUser) { + const [existingOrgUser] = await db + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.orgId, orgId), + eq(userOrgs.userId, existingUser.userId) + ) + ); + + if (existingOrgUser) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "User already exists in this organization" + ) + ); + } + + await db + .insert(userOrgs) + .values({ + orgId, + userId: existingUser.userId, + roleId: role.roleId + }) + .returning(); + } else { + const userId = generateId(15); + + const [newUser] = await db + .insert(users) + .values({ + userId: userId, + email, + username, + name, + type: "oidc", + idpId, + dateCreated: new Date().toISOString(), + emailVerified: true + }) + .returning(); + + await db + .insert(userOrgs) + .values({ + orgId, + userId: newUser.userId, + roleId: role.roleId + }) + .returning(); + } + } else { + return next( + createHttpError(HttpCode.BAD_REQUEST, "User type is required") + ); + } + + return response(res, { + data: {}, + success: true, + error: false, + message: "Org user created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 8e8fd391..49278c14 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -9,3 +9,4 @@ export * from "./adminListUsers"; export * from "./adminRemoveUser"; export * from "./listInvitations"; export * from "./removeInvitation"; +export * from "./createOrgUser"; diff --git a/src/app/admin/idp/AdminIdpTable.tsx b/src/app/admin/idp/AdminIdpTable.tsx index 0048c23f..ee3104bd 100644 --- a/src/app/admin/idp/AdminIdpTable.tsx +++ b/src/app/admin/idp/AdminIdpTable.tsx @@ -153,22 +153,6 @@ export default function IdpTable({ idps }: Props) { ); } }, - { - accessorKey: "orgCount", - header: ({ column }) => { - return ( - - ); - } - }, { id: "actions", cell: ({ row }) => { diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 760f590b..4f4ad613 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -41,6 +41,7 @@ import { InfoSectionTitle } from "@app/components/InfoSection"; import CopyToClipboard from "@app/components/CopyToClipboard"; +import { Badge } from "@app/components/ui/badge"; const GeneralFormSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), @@ -218,20 +219,28 @@ export default function GeneralPage() { )} /> - { - form.setValue("autoProvision", checked); - }} - /> +
+ { + form.setValue( + "autoProvision", + checked + ); + }} + /> + Enterprise +
When enabled, users will be automatically - created in the system upon first login using - this identity provider. + created in the system upon first login with + the ability to map users to roles and + organizations. diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index 1d695f4d..45b30ac1 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -35,6 +35,7 @@ import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon, ExternalLink } from "lucide-react"; import { StrategySelect } from "@app/components/StrategySelect"; import { SwitchInput } from "@app/components/SwitchInput"; +import { Badge } from "@app/components/ui/badge"; const createIdpFormSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), @@ -87,7 +88,7 @@ export default function Page() { namePath: "name", emailPath: "email", scopes: "openid profile email", - autoProvision: true + autoProvision: false } }); @@ -182,24 +183,30 @@ export default function Page() { )} /> - { - form.setValue( - "autoProvision", - checked - ); - }} - /> +
+ { + form.setValue( + "autoProvision", + checked + ); + }} + /> + + Enterprise + +
When enabled, users will be automatically created in the system upon - first login using this identity - provider. + first login with the ability to map + users to roles and organizations. diff --git a/src/components/SwitchInput.tsx b/src/components/SwitchInput.tsx index e1bd725a..571a1ab4 100644 --- a/src/components/SwitchInput.tsx +++ b/src/components/SwitchInput.tsx @@ -7,6 +7,7 @@ interface SwitchComponentProps { label: string; description?: string; defaultChecked?: boolean; + disabled?: boolean; onCheckedChange: (checked: boolean) => void; } @@ -14,6 +15,7 @@ export function SwitchInput({ id, label, description, + disabled, defaultChecked = false, onCheckedChange }: SwitchComponentProps) { @@ -24,6 +26,7 @@ export function SwitchInput({ id={id} defaultChecked={defaultChecked} onCheckedChange={onCheckedChange} + disabled={disabled} /> From 960eb34c7d78cffcf55163fdf24d3251075b5a76 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 23 Apr 2025 13:46:06 -0400 Subject: [PATCH 33/53] refactor invite user to create wizard --- server/routers/external.ts | 12 +- .../settings/access/users/InviteUserForm.tsx | 369 --------------- .../settings/access/users/UsersDataTable.tsx | 2 +- .../settings/access/users/UsersTable.tsx | 13 +- .../settings/access/users/create/page.tsx | 443 ++++++++++++++++++ 5 files changed, 446 insertions(+), 393 deletions(-) delete mode 100644 src/app/[orgId]/settings/access/users/InviteUserForm.tsx create mode 100644 src/app/[orgId]/settings/access/users/create/page.tsx diff --git a/server/routers/external.ts b/server/routers/external.ts index b543faac..addd922b 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -26,12 +26,7 @@ import { verifyUserAccess, getUserOrgs, verifyUserIsServerAdmin, -<<<<<<< Updated upstream verifyIsLoggedInUser -======= - verifyIsLoggedInUser, - verifyClientAccess ->>>>>>> Stashed changes } from "@server/middlewares"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { ActionsEnum } from "@server/auth/actions"; @@ -51,10 +46,6 @@ unauthenticated.get("/", (_, res) => { export const authenticated = Router(); authenticated.use(verifySessionUserMiddleware); -<<<<<<< Updated upstream -======= -authenticated.get("/pick-org-defaults", org.pickOrgDefaults); ->>>>>>> Stashed changes authenticated.get("/org/checkId", org.checkId); authenticated.put("/org", getUserOrgs, org.createOrg); @@ -530,8 +521,7 @@ authenticated.post( authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp); -authenticated.get("/idp", verifyUserIsServerAdmin, idp.listIdps); - +authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); // Auth routes diff --git a/src/app/[orgId]/settings/access/users/InviteUserForm.tsx b/src/app/[orgId]/settings/access/users/InviteUserForm.tsx deleted file mode 100644 index 5ebc34cb..00000000 --- a/src/app/[orgId]/settings/access/users/InviteUserForm.tsx +++ /dev/null @@ -1,369 +0,0 @@ -"use client"; - -import { Button } from "@app/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { Input } from "@app/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import { toast } from "@app/hooks/useToast"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { InviteUserBody, InviteUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; -import CopyTextBox from "@app/components/CopyTextBox"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import { useOrgContext } from "@app/hooks/useOrgContext"; -import { ListRolesResponse } from "@server/routers/role"; -import { formatAxiosError } from "@app/lib/api"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { Checkbox } from "@app/components/ui/checkbox"; - -type InviteUserFormProps = { - open: boolean; - setOpen: (open: boolean) => void; -}; - -const formSchema = z.object({ - email: z.string().email({ message: "Invalid email address" }), - validForHours: z.string().min(1, { message: "Please select a duration" }), - roleId: z.string().min(1, { message: "Please select a role" }) -}); - -export default function InviteUserForm({ open, setOpen }: InviteUserFormProps) { - const { org } = useOrgContext(); - const { env } = useEnvContext(); - const api = createApiClient({ env }); - - const [inviteLink, setInviteLink] = useState(null); - const [loading, setLoading] = useState(false); - const [expiresInDays, setExpiresInDays] = useState(1); - const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); - const [sendEmail, setSendEmail] = useState(env.email.emailEnabled); - - const validFor = [ - { hours: 24, name: "1 day" }, - { hours: 48, name: "2 days" }, - { hours: 72, name: "3 days" }, - { hours: 96, name: "4 days" }, - { hours: 120, name: "5 days" }, - { hours: 144, name: "6 days" }, - { hours: 168, name: "7 days" } - ]; - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - email: "", - validForHours: "72", - roleId: "" - } - }); - - useEffect(() => { - if (open) { - setSendEmail(env.email.emailEnabled); - form.reset(); - setInviteLink(null); - setExpiresInDays(1); - } - }, [open, env.email.emailEnabled, form]); - - useEffect(() => { - if (!open) { - return; - } - - async function fetchRoles() { - const res = await api - .get< - AxiosResponse - >(`/org/${org?.org.orgId}/roles`) - .catch((e) => { - console.error(e); - toast({ - variant: "destructive", - title: "Failed to fetch roles", - description: formatAxiosError( - e, - "An error occurred while fetching the roles" - ) - }); - }); - - if (res?.status === 200) { - setRoles(res.data.data.roles); - } - } - - fetchRoles(); - }, [open]); - - async function onSubmit(values: z.infer) { - setLoading(true); - - const res = await api - .post>( - `/org/${org?.org.orgId}/create-invite`, - { - email: values.email, - roleId: parseInt(values.roleId), - validHours: parseInt(values.validForHours), - sendEmail: sendEmail - } as InviteUserBody - ) - .catch((e) => { - if (e.response?.status === 409) { - toast({ - variant: "destructive", - title: "User Already Exists", - description: - "This user is already a member of the organization." - }); - } else { - toast({ - variant: "destructive", - title: "Failed to invite user", - description: formatAxiosError( - e, - "An error occurred while inviting the user" - ) - }); - } - }); - - if (res && res.status === 200) { - setInviteLink(res.data.data.inviteLink); - toast({ - variant: "default", - title: "User invited", - description: "The user has been successfully invited." - }); - - setExpiresInDays(parseInt(values.validForHours) / 24); - } - - setLoading(false); - } - - return ( - <> - { - setOpen(val); - if (!val) { - setInviteLink(null); - setLoading(false); - setExpiresInDays(1); - form.reset(); - } - }} - > - - - Invite User - - Give new users access to your organization - - - -
- {!inviteLink && ( -
- - ( - - Email - - - - - - )} - /> - - {env.email.emailEnabled && ( -
- - setSendEmail( - e as boolean - ) - } - /> - -
- )} - - ( - - Role - - - - )} - /> - ( - - - Valid For - - - - - )} - /> - - - )} - - {inviteLink && ( -
- {sendEmail && ( -

- An email has been sent to the user - with the access link below. They - must access the link to accept the - invitation. -

- )} - {!sendEmail && ( -

- The user has been invited. They must - access the link below to accept the - invitation. -

- )} -

- The invite will expire in{" "} - - {expiresInDays}{" "} - {expiresInDays === 1 - ? "day" - : "days"} - - . -

- -
- )} -
-
- - - - - - -
-
- - ); -} diff --git a/src/app/[orgId]/settings/access/users/UsersDataTable.tsx b/src/app/[orgId]/settings/access/users/UsersDataTable.tsx index 1ce169e0..643d8641 100644 --- a/src/app/[orgId]/settings/access/users/UsersDataTable.tsx +++ b/src/app/[orgId]/settings/access/users/UsersDataTable.tsx @@ -24,7 +24,7 @@ export function UsersDataTable({ searchPlaceholder="Search users..." searchColumn="email" onAdd={inviteUser} - addButtonText="Invite User" + addButtonText="Create User" /> ); } diff --git a/src/app/[orgId]/settings/access/users/UsersTable.tsx b/src/app/[orgId]/settings/access/users/UsersTable.tsx index ea642800..8036cc84 100644 --- a/src/app/[orgId]/settings/access/users/UsersTable.tsx +++ b/src/app/[orgId]/settings/access/users/UsersTable.tsx @@ -11,7 +11,6 @@ import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, Crown, MoreHorizontal } from "lucide-react"; import { UsersDataTable } from "./UsersDataTable"; import { useState } from "react"; -import InviteUserForm from "./InviteUserForm"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; @@ -41,16 +40,11 @@ type UsersTableProps = { }; export default function UsersTable({ users: u }: UsersTableProps) { - const [isInviteModalOpen, setIsInviteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); - const [users, setUsers] = useState(u); - const router = useRouter(); - const api = createApiClient(useEnvContext()); - const { user, updateUser } = useUserContext(); const { org } = useOrgContext(); @@ -281,16 +275,11 @@ export default function UsersTable({ users: u }: UsersTableProps) { title="Remove User from Organization" /> - - { - setIsInviteModalOpen(true); + router.push(`/${org?.org.orgId}/settings/access/users/create`); }} /> diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx new file mode 100644 index 00000000..c2e9374d --- /dev/null +++ b/src/app/[orgId]/settings/access/users/create/page.tsx @@ -0,0 +1,443 @@ +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { StrategySelect } from "@app/components/StrategySelect"; +import HeaderTitle from "@app/components/SettingsSectionTitle"; +import { Button } from "@app/components/ui/button"; +import { useParams, useRouter } from "next/navigation"; +import { useState } from "react"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { toast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { InviteUserBody, InviteUserResponse } from "@server/routers/user"; +import { AxiosResponse } from "axios"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { ListRolesResponse } from "@server/routers/role"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { Checkbox } from "@app/components/ui/checkbox"; + +type UserType = "internal" | "external"; + +interface UserTypeOption { + id: UserType; + title: string; + description: string; +} + +const formSchema = z.object({ + email: z.string().email({ message: "Invalid email address" }), + validForHours: z.string().min(1, { message: "Please select a duration" }), + roleId: z.string().min(1, { message: "Please select a role" }) +}); + +export default function Page() { + const { orgId } = useParams(); + const router = useRouter(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); + + const [userType, setUserType] = useState("internal"); + const [inviteLink, setInviteLink] = useState(null); + const [loading, setLoading] = useState(false); + const [expiresInDays, setExpiresInDays] = useState(1); + const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); + const [sendEmail, setSendEmail] = useState(env.email.emailEnabled); + + const validFor = [ + { hours: 24, name: "1 day" }, + { hours: 48, name: "2 days" }, + { hours: 72, name: "3 days" }, + { hours: 96, name: "4 days" }, + { hours: 120, name: "5 days" }, + { hours: 144, name: "6 days" }, + { hours: 168, name: "7 days" } + ]; + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + validForHours: "72", + roleId: "" + } + }); + + useEffect(() => { + if (userType === "internal") { + setSendEmail(env.email.emailEnabled); + form.reset(); + setInviteLink(null); + setExpiresInDays(1); + } + }, [userType, env.email.emailEnabled, form]); + + useEffect(() => { + if (userType !== "internal") { + return; + } + + async function fetchRoles() { + const res = await api + .get>(`/org/${orgId}/roles`) + .catch((e) => { + console.error(e); + toast({ + variant: "destructive", + title: "Failed to fetch roles", + description: formatAxiosError( + e, + "An error occurred while fetching the roles" + ) + }); + }); + + if (res?.status === 200) { + setRoles(res.data.data.roles); + } + } + + fetchRoles(); + }, [userType]); + + async function onSubmit(values: z.infer) { + setLoading(true); + + const res = await api + .post>( + `/org/${orgId}/create-invite`, + { + email: values.email, + roleId: parseInt(values.roleId), + validHours: parseInt(values.validForHours), + sendEmail: sendEmail + } as InviteUserBody + ) + .catch((e) => { + if (e.response?.status === 409) { + toast({ + variant: "destructive", + title: "User Already Exists", + description: + "This user is already a member of the organization." + }); + } else { + toast({ + variant: "destructive", + title: "Failed to invite user", + description: formatAxiosError( + e, + "An error occurred while inviting the user" + ) + }); + } + }); + + if (res && res.status === 200) { + setInviteLink(res.data.data.inviteLink); + toast({ + variant: "default", + title: "User invited", + description: "The user has been successfully invited." + }); + + setExpiresInDays(parseInt(values.validForHours) / 24); + } + + setLoading(false); + } + + const userTypes: ReadonlyArray = [ + { + id: "internal", + title: "Internal User", + description: "Invite a user to join your organization directly." + }, + { + id: "external", + title: "External User", + description: + "Provision a user with an external identity provider (IdP)." + } + ]; + + return ( + <> +
+ + +
+ +
+ + + + + User Type + + + Determine how you want to create the user + + + + { + setUserType(value as UserType); + }} + cols={2} + /> + + + + {userType === "internal" && ( + + + + User Information + + + Enter the details for the new user + + + + +
+ + ( + + + Email + + + + + + + )} + /> + + {env.email.emailEnabled && ( +
+ + setSendEmail( + e as boolean + ) + } + /> + +
+ )} + + ( + + + Role + + + + + )} + /> + + ( + + + Valid For + + + + + )} + /> + + {inviteLink && ( +
+ {sendEmail && ( +

+ An email has been + sent to the user + with the access link + below. They must + access the link to + accept the + invitation. +

+ )} + {!sendEmail && ( +

+ The user has been + invited. They must + access the link + below to accept the + invitation. +

+ )} +

+ The invite will expire + in{" "} + + {expiresInDays}{" "} + {expiresInDays === 1 + ? "day" + : "days"} + + . +

+ +
+ )} + + +
+
+
+ )} +
+ +
+ + {userType === "internal" && ( + + )} +
+
+ + ); +} From 419bacf55ff6cd61ff29c4f8e61ac4c5549f8386 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 23 Apr 2025 20:36:16 +0200 Subject: [PATCH 34/53] check and add the service dependency of crowdsec to traefik --- install/config/docker-compose.yml | 4 -- install/crowdsec.go | 64 +++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 1cccffa3..496b0138 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -48,10 +48,6 @@ services: depends_on: pangolin: condition: service_healthy -{{if .DoCrowdsecInstall}} - crowdsec: - condition: service_healthy -{{end}} command: - --configFile=/etc/traefik/traefik_config.yml volumes: diff --git a/install/crowdsec.go b/install/crowdsec.go index 9fadadc6..c17bf540 100644 --- a/install/crowdsec.go +++ b/install/crowdsec.go @@ -3,9 +3,12 @@ package main import ( "bytes" "fmt" + "log" "os" "os/exec" "strings" + + "gopkg.in/yaml.v3" ) func installCrowdsec(config Config) error { @@ -63,6 +66,12 @@ func installCrowdsec(config Config) error { os.Exit(1) } + // check and add the service dependency of crowdsec to traefik + if err := CheckAndAddCrowdsecDependency("docker-compose.yml"); err != nil { + fmt.Printf("Error adding crowdsec dependency to traefik: %v\n", err) + os.Exit(1) + } + if err := startContainers(); err != nil { return fmt.Errorf("failed to start containers: %v", err) } @@ -135,3 +144,58 @@ func checkIfTextInFile(file, text string) bool { // Check for text return bytes.Contains(content, []byte(text)) } + +func CheckAndAddCrowdsecDependency(composePath string) error { + // Read the docker-compose.yml file + data, err := os.ReadFile(composePath) + if err != nil { + return fmt.Errorf("error reading compose file: %w", err) + } + + // Parse YAML into a generic map + var compose map[string]interface{} + if err := yaml.Unmarshal(data, &compose); err != nil { + return fmt.Errorf("error parsing compose file: %w", err) + } + + // Get services section + services, ok := compose["services"].(map[string]interface{}) + if !ok { + return fmt.Errorf("services section not found or invalid") + } + + // Get traefik service + traefik, ok := services["traefik"].(map[string]interface{}) + if !ok { + return fmt.Errorf("traefik service not found or invalid") + } + + // Get dependencies + dependsOn, ok := traefik["depends_on"].(map[string]interface{}) + if ok { + // Append the new block for crowdsec + dependsOn["crowdsec"] = map[string]interface{}{ + "condition": "service_healthy", + } + } else { + // No dependencies exist, create it + traefik["depends_on"] = map[string]interface{}{ + "crowdsec": map[string]interface{}{ + "condition": "service_healthy", + }, + } + } + + // Marshal the modified data back to YAML with indentation + modifiedData, err := MarshalYAMLWithIndent(compose, 2) // Set indentation to 2 spaces + if err != nil { + log.Fatalf("error marshaling YAML: %v", err) + } + + if err := os.WriteFile(composePath, modifiedData, 0644); err != nil { + return fmt.Errorf("error writing updated compose file: %w", err) + } + + fmt.Println("Added dependency of crowdsec to traefik") + return nil +} From 5d6e15b0d66b2fe70e82be1e1034591e5e516407 Mon Sep 17 00:00:00 2001 From: Taylan Date: Wed, 23 Apr 2025 20:54:03 +0200 Subject: [PATCH 35/53] indentation fix --- install/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/main.go b/install/main.go index 157a3abe..8411e304 100644 --- a/install/main.go +++ b/install/main.go @@ -545,7 +545,7 @@ func restartContainer(container string) error { fmt.Println("Restarting containers...") if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil { - return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) + return fmt.Errorf("failed to stop the container \"%s\": %v", container, err) } return nil From 97af632c61161007321224731aed1b3b7ccaa2b6 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 23 Apr 2025 15:44:27 -0400 Subject: [PATCH 36/53] add option to pre provision idp user --- .../settings/access/users/create/page.tsx | 726 +++++++++++++----- src/app/admin/idp/AdminIdpTable.tsx | 1 + src/app/admin/idp/[idpId]/general/page.tsx | 583 +++++++------- 3 files changed, 840 insertions(+), 470 deletions(-) diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx index c2e9374d..c270b350 100644 --- a/src/app/[orgId]/settings/access/users/create/page.tsx +++ b/src/app/[orgId]/settings/access/users/create/page.tsx @@ -38,14 +38,14 @@ import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import CopyTextBox from "@app/components/CopyTextBox"; -import { useOrgContext } from "@app/hooks/useOrgContext"; +import { useEnvContext } from "@app/hooks/useEnvContext"; import { ListRolesResponse } from "@server/routers/role"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; import { Checkbox } from "@app/components/ui/checkbox"; +import { ListIdpsResponse } from "@server/routers/idp"; -type UserType = "internal" | "external"; +type UserType = "internal" | "oidc"; interface UserTypeOption { id: UserType; @@ -53,12 +53,39 @@ interface UserTypeOption { description: string; } -const formSchema = z.object({ +interface IdpOption { + idpId: number; + name: string; + type: string; +} + +const internalFormSchema = z.object({ email: z.string().email({ message: "Invalid email address" }), validForHours: z.string().min(1, { message: "Please select a duration" }), roleId: z.string().min(1, { message: "Please select a role" }) }); +const externalFormSchema = z.object({ + username: z.string().min(1, { message: "Username is required" }), + email: z + .string() + .email({ message: "Invalid email address" }) + .optional() + .or(z.literal("")), + name: z.string().optional(), + roleId: z.string().min(1, { message: "Please select a role" }), + idpId: z.string().min(1, { message: "Please select an identity provider" }) +}); + +const formatIdpType = (type: string) => { + switch (type.toLowerCase()) { + case "oidc": + return "Generic OAuth2/OIDC provider."; + default: + return type; + } +}; + export default function Page() { const { orgId } = useParams(); const router = useRouter(); @@ -70,7 +97,10 @@ export default function Page() { const [loading, setLoading] = useState(false); const [expiresInDays, setExpiresInDays] = useState(1); const [roles, setRoles] = useState<{ roleId: number; name: string }[]>([]); + const [idps, setIdps] = useState([]); const [sendEmail, setSendEmail] = useState(env.email.emailEnabled); + const [selectedIdp, setSelectedIdp] = useState(null); + const [dataLoaded, setDataLoaded] = useState(false); const validFor = [ { hours: 24, name: "1 day" }, @@ -82,8 +112,8 @@ export default function Page() { { hours: 168, name: "7 days" } ]; - const form = useForm>({ - resolver: zodResolver(formSchema), + const internalForm = useForm>({ + resolver: zodResolver(internalFormSchema), defaultValues: { email: "", validForHours: "72", @@ -91,17 +121,30 @@ export default function Page() { } }); + const externalForm = useForm>({ + resolver: zodResolver(externalFormSchema), + defaultValues: { + username: "", + email: "", + name: "", + roleId: "", + idpId: "" + } + }); + useEffect(() => { if (userType === "internal") { setSendEmail(env.email.emailEnabled); - form.reset(); + internalForm.reset(); setInviteLink(null); setExpiresInDays(1); + } else if (userType === "oidc") { + externalForm.reset(); } - }, [userType, env.email.emailEnabled, form]); + }, [userType, env.email.emailEnabled, internalForm, externalForm]); useEffect(() => { - if (userType !== "internal") { + if (!userType) { return; } @@ -122,13 +165,43 @@ export default function Page() { if (res?.status === 200) { setRoles(res.data.data.roles); + if (userType === "internal") { + setDataLoaded(true); + } } } + async function fetchIdps() { + const res = await api + .get>("/idp") + .catch((e) => { + console.error(e); + toast({ + variant: "destructive", + title: "Failed to fetch identity providers", + description: formatAxiosError( + e, + "An error occurred while fetching identity providers" + ) + }); + }); + + if (res?.status === 200) { + setIdps(res.data.data.idps); + setDataLoaded(true); + } + } + + setDataLoaded(false); fetchRoles(); + if (userType !== "internal") { + fetchIdps(); + } }, [userType]); - async function onSubmit(values: z.infer) { + async function onSubmitInternal( + values: z.infer + ) { setLoading(true); const res = await api @@ -175,6 +248,43 @@ export default function Page() { setLoading(false); } + async function onSubmitExternal( + values: z.infer + ) { + setLoading(true); + + const res = await api + .put(`/org/${orgId}/user`, { + username: values.username, + email: values.email, + name: values.name, + type: "oidc", + idpId: parseInt(values.idpId), + roleId: parseInt(values.roleId) + }) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to create user", + description: formatAxiosError( + e, + "An error occurred while creating the user" + ) + }); + }); + + if (res && res.status === 201) { + toast({ + variant: "default", + title: "User created", + description: "The user has been successfully created." + }); + router.push(`/${orgId}/settings/access/users`); + } + + setLoading(false); + } + const userTypes: ReadonlyArray = [ { id: "internal", @@ -182,10 +292,9 @@ export default function Page() { description: "Invite a user to join your organization directly." }, { - id: "external", + id: "oidc", title: "External User", - description: - "Provision a user with an external identity provider (IdP)." + description: "Create a user with an external identity provider." } ]; @@ -223,196 +332,434 @@ export default function Page() { defaultValue={userType || undefined} onChange={(value) => { setUserType(value as UserType); + if (value === "internal") { + internalForm.reset(); + } else if (value === "oidc") { + externalForm.reset(); + setSelectedIdp(null); + } }} cols={2} />
- {userType === "internal" && ( - - - - User Information - - - Enter the details for the new user - - - - -
- + {userType === "internal" && dataLoaded && ( + <> + + + + User Information + + + Enter the details for the new user + + + + + + + ( + + + Email + + + + + + + )} + /> + + {env.email.emailEnabled && ( +
+ + setSendEmail( + e as boolean + ) + } + /> + +
+ )} + + ( + + + Valid For + + + + + )} + /> + + ( + + + Role + + + + + )} + /> + + {inviteLink && ( +
+ {sendEmail && ( +

+ An email has + been sent to the + user with the + access link + below. They must + access the link + to accept the + invitation. +

+ )} + {!sendEmail && ( +

+ The user has + been invited. + They must access + the link below + to accept the + invitation. +

+ )} +

+ The invite will + expire in{" "} + + {expiresInDays}{" "} + {expiresInDays === + 1 + ? "day" + : "days"} + + . +

+ +
+ )} + + +
+
+
+ + )} + + {userType !== "internal" && dataLoaded && ( + <> + + + + Identity Provider + + + Select the identity provider for the + external user + + + + {idps.length === 0 ? ( +

+ No identity providers are + configured. Please configure an + identity provider before creating + external users. +

+ ) : ( +
( - - Email - - - - + ({ + id: idp.idpId.toString(), + title: idp.name, + description: + formatIdpType( + idp.type + ) + }) + )} + defaultValue={ + field.value + } + onChange={( + value + ) => { + field.onChange( + value + ); + const idp = + idps.find( + (idp) => + idp.idpId.toString() === + value + ); + setSelectedIdp( + idp || null + ); + }} + cols={3} + /> )} /> + + )} +
+
- {env.email.emailEnabled && ( -
- - setSendEmail( - e as boolean - ) + {idps.length > 0 && ( + + + + User Information + + + Enter the details for the new user + + + + +
+ + ( + + + Username + + + + +

+ This must + match the + unique + username + that exists + in the + selected + identity + provider. +

+ +
+ )} /> - -
- )} - ( - - - Role - - - - - )} - /> - - ( - - - Valid For - - - - - )} - /> - - {inviteLink && ( -
- {sendEmail && ( -

- An email has been - sent to the user - with the access link - below. They must - access the link to - accept the - invitation. -

- )} - {!sendEmail && ( -

- The user has been - invited. They must - access the link - below to accept the - invitation. -

- )} -

- The invite will expire - in{" "} - - {expiresInDays}{" "} - {expiresInDays === 1 - ? "day" - : "days"} - - . -

- ( + + + Email + (Optional) + + + + + + + )} /> -
- )} - - -
-
-
+ + ( + + + Name + (Optional) + + + + + + + )} + /> + + ( + + + Role + + + + + )} + /> + + + + +
+ )} + )} @@ -426,12 +773,15 @@ export default function Page() { > Cancel - {userType === "internal" && ( + {userType && dataLoaded && ( diff --git a/src/app/admin/idp/AdminIdpTable.tsx b/src/app/admin/idp/AdminIdpTable.tsx index ee3104bd..b2415280 100644 --- a/src/app/admin/idp/AdminIdpTable.tsx +++ b/src/app/admin/idp/AdminIdpTable.tsx @@ -44,6 +44,7 @@ export default function IdpTable({ idps }: Props) { title: "Success", description: "Identity provider deleted successfully" }); + setIsDeleteModalOpen(false); router.refresh(); } catch (e) { toast({ diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 4f4ad613..7e23db1b 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -162,104 +162,42 @@ export default function GeneralPage() { } return ( - - - - - General Information - - - Configure the basic information for your identity - provider - - - - - - Redirect URL - - - - - - - - - - About Redirect URL - - - This is the URL to which users will be redirected - after authentication. You need to configure this URL - in your identity provider settings. - - - -
- - ( - - Name - - - - - A display name for this identity - provider - - - - )} - /> - -
- { - form.setValue( - "autoProvision", - checked - ); - }} - /> - Enterprise -
- - When enabled, users will be automatically - created in the system upon first login with - the ability to map users to roles and - organizations. - - - -
-
-
- - + <> + - OAuth2/OIDC Configuration + General Information - Configure the OAuth2/OIDC provider endpoints and - credentials + Configure the basic information for your identity + provider + + + + Redirect URL + + + + + + + + + + + About Redirect URL + + + This is the URL to which users will be + redirected after authentication. You need to + configure this URL in your identity provider + settings. + +
( - Client ID + Name - The OAuth2 client ID from - your identity provider + A display name for this + identity provider )} /> - ( - - - Client Secret - - - - - - The OAuth2 client secret - from your identity provider - - - - )} - /> - - ( - - - Authorization URL - - - - - - The OAuth2 authorization - endpoint URL - - - - )} - /> - - ( - - Token URL - - - - - The OAuth2 token endpoint - URL - - - - )} - /> +
+ { + form.setValue( + "autoProvision", + checked + ); + }} + /> + + Enterprise + +
+ + When enabled, users will be + automatically created in the system upon + first login with the ability to map + users to roles and organizations. +
- - - - Token Configuration - - - Configure how to extract user information from the - ID token - - - - -
- - - - - About JMESPath - - - The paths below use JMESPath syntax - to extract values from the ID token. - - Learn more about JMESPath{" "} - - - - + + + + + OAuth2/OIDC Configuration + + + Configure the OAuth2/OIDC provider endpoints and + credentials + + + + + + + ( + + + Client ID + + + + + + The OAuth2 client ID + from your identity + provider + + + + )} + /> - ( - - - Identifier Path - - - - - - The JMESPath to the user - identifier in the ID token - - - - )} - /> + ( + + + Client Secret + + + + + + The OAuth2 client secret + from your identity + provider + + + + )} + /> - ( - - - Email Path (Optional) - - - - - - The JMESPath to the user's - email in the ID token - - - - )} - /> + ( + + + Authorization URL + + + + + + The OAuth2 authorization + endpoint URL + + + + )} + /> - ( - - - Name Path (Optional) - - - - - - The JMESPath to the user's - name in the ID token - - - - )} - /> + ( + + + Token URL + + + + + + The OAuth2 token + endpoint URL + + + + )} + /> + + + + + - ( - - Scopes - - - - - Space-separated list of - OAuth2 scopes to request - - - - )} - /> - - -
-
+ + + + Token Configuration + + + Configure how to extract user information from + the ID token + + + + +
+ + + + + About JMESPath + + + The paths below use JMESPath + syntax to extract values from + the ID token. + + Learn more about JMESPath{" "} + + + + - - - - - - + ( + + + Identifier Path + + + + + + The JMESPath to the user + identifier in the ID + token + + + + )} + /> + + ( + + + Email Path (Optional) + + + + + + The JMESPath to the + user's email in the ID + token + + + + )} + /> + + ( + + + Name Path (Optional) + + + + + + The JMESPath to the + user's name in the ID + token + + + + )} + /> + + ( + + + Scopes + + + + + + Space-separated list of + OAuth2 scopes to request + + + + )} + /> + + +
+
+
+
+
+ +
+ +
+ ); } From 566e66daa49875169fdd9f142a33544978adf470 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 23 Apr 2025 15:52:02 -0400 Subject: [PATCH 37/53] fix org landing --- src/app/[orgId]/page.tsx | 22 +++++++++++++++++----- src/app/navigation.tsx | 8 ++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 438ebbe4..5f91fb62 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -8,7 +8,8 @@ import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/lib/api/cookies"; import { redirect } from "next/navigation"; import { Layout } from "@app/components/Layout"; -import { orgNavItems } from "../navigation"; +import { orgLangingNavItems, orgNavItems, rootNavItems } from "../navigation"; +import { ListUserOrgsResponse } from "@server/routers/org"; type OrgPageProps = { params: Promise<{ orgId: string }>; @@ -43,12 +44,23 @@ export default async function OrgPage(props: OrgPageProps) { redirect(`/${orgId}/settings`); } + let orgs: ListUserOrgsResponse["orgs"] = []; + try { + const getOrgs = cache(async () => + internal.get>( + `/user/${user.userId}/orgs`, + await authCookieHeader() + ) + ); + const res = await getOrgs(); + if (res && res.data.data.orgs) { + orgs = res.data.data.orgs; + } + } catch (e) {} + return ( - + {overview && (
+ } +]; + export const rootNavItems: SidebarNavItem[] = [ { title: "Home", From a6d6aaaadd1cc728d05c1546048d35e7d75b01a1 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 23 Apr 2025 16:18:51 -0400 Subject: [PATCH 38/53] add api keys to sidebar nav --- src/app/navigation.tsx | 15 ++++++++++++++- src/app/setup/page.tsx | 3 +++ src/components/SidebarNav.tsx | 31 +++++++++++++++++++++---------- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 8c4684a6..ea33b294 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -6,7 +6,8 @@ import { Link as LinkIcon, Waypoints, Combine, - Fingerprint + Fingerprint, + KeyRound } from "lucide-react"; export const orgLangingNavItems: SidebarNavItem[] = [ @@ -63,6 +64,12 @@ export const orgNavItems: SidebarNavItem[] = [ href: "/{orgId}/settings/share-links", icon: }, + { + title: "API Keys", + href: "/{orgId}/settings/api-keys", + icon: , + showEnterprise: true + }, { title: "Settings", href: "/{orgId}/settings/general", @@ -76,6 +83,12 @@ export const adminNavItems: SidebarNavItem[] = [ href: "/admin/users", icon: }, + { + title: "API Keys", + href: "/{orgId}/settings/api-keys", + icon: , + showEnterprise: true + }, { title: "Identity Providers", href: "/admin/idp", diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 1e416d26..5420748c 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -61,6 +61,9 @@ export default function StepperForm() { const router = useRouter(); const checkOrgIdAvailability = useCallback(async (value: string) => { + if (loading) { + return; + } try { const res = await api.get(`/org/checkId`, { params: { diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 6a7aec25..31dd2ce2 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -6,6 +6,7 @@ import { useParams, usePathname } from "next/navigation"; import { cn } from "@app/lib/cn"; import { ChevronDown, ChevronRight } from "lucide-react"; import { useUserContext } from "@app/hooks/useUserContext"; +import { Badge } from "@app/components/ui/badge"; export interface SidebarNavItem { href: string; @@ -13,6 +14,7 @@ export interface SidebarNavItem { icon?: React.ReactNode; children?: SidebarNavItem[]; autoExpand?: boolean; + showEnterprise?: boolean; } export interface SidebarNavProps extends React.HTMLAttributes { @@ -95,7 +97,9 @@ export function SidebarNav({ const isActive = pathname.startsWith(hydratedHref); const hasChildren = item.children && item.children.length > 0; const isExpanded = expandedItems.has(hydratedHref); - const indent = level * 16; // Base indent for each level + const indent = level * 28; // Base indent for each level + const isEnterprise = item.showEnterprise; + const isDisabled = disabled || isEnterprise; return (
@@ -110,34 +114,41 @@ export function SidebarNav({ )} > { - if (disabled) { + if (isDisabled) { e.preventDefault(); } else if (onItemClick) { onItemClick(); } }} - tabIndex={disabled ? -1 : undefined} - aria-disabled={disabled} + tabIndex={isDisabled ? -1 : undefined} + aria-disabled={isDisabled} > - {item.icon && ( - {item.icon} +
+ {item.icon && ( + {item.icon} + )} + {item.title} +
+ {isEnterprise && ( + + Enterprise + )} - {item.title} {hasChildren && ( - - - )} + {whitelistEnabled && env.email.emailEnabled && ( +
+ + ( + + + + + + {/* @ts-ignore */} + { + return z + .string() + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: + "Invalid email address. Wildcard (*) must be the entire local part." + } + ) + ) + .safeParse( + tag + ).success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder="Enter an email" + tags={ + whitelistForm.getValues() + .emails + } + setTags={( + newRoles + ) => { + whitelistForm.setValue( + "emails", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + allowDuplicates={ + false + } + sortTags={true} + /> + + + Press enter to add an + email after typing it in + the input field. + + + )} + /> + + + )} + + + + + ); From 4dba75f9139c903480f37849bf60e857aeef2567 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 23 Apr 2025 16:33:55 -0400 Subject: [PATCH 40/53] obscure pin code input closes #580 --- .../authentication/SetResourcePasswordForm.tsx | 6 ------ .../authentication/SetResourcePincodeForm.tsx | 12 ++++++------ .../resource/[resourceId]/ResourceAuthPortal.tsx | 6 ++++++ src/components/ui/input-otp.tsx | 10 +++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx index e4bdd1b4..3bf2966a 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx @@ -140,12 +140,6 @@ export default function SetResourcePasswordForm({ /> - - Users will be able to access - this resource by entering this - password. It must be at least 4 - characters long. - )} /> diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx index 58a997bf..31ccbea6 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx @@ -147,33 +147,33 @@ export default function SetResourcePincodeForm({
- - Users will be able to access - this resource by entering this - PIN code. It must be at least 6 - digits long. - )} /> diff --git a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx index d7efa59c..c7eca2c7 100644 --- a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx @@ -377,31 +377,37 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { index={ 0 } + obscured /> diff --git a/src/components/ui/input-otp.tsx b/src/components/ui/input-otp.tsx index f13ce8a6..57cfe388 100644 --- a/src/components/ui/input-otp.tsx +++ b/src/components/ui/input-otp.tsx @@ -8,8 +8,8 @@ import { cn } from "@app/lib/cn" const InputOTP = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, containerClassName, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { obscured?: boolean } +>(({ className, containerClassName, obscured = false, ...props }, ref) => ( , - React.ComponentPropsWithoutRef<"div"> & { index: number } ->(({ index, className, ...props }, ref) => { + React.ComponentPropsWithoutRef<"div"> & { index: number; obscured?: boolean } +>(({ index, className, obscured = false, ...props }, ref) => { const inputOTPContext = React.useContext(OTPInputContext) const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] @@ -47,7 +47,7 @@ const InputOTPSlot = React.forwardRef< )} {...props} > - {char} + {char && obscured ? "•" : char} {hasFakeCaret && (
From d6d6a59eee7c03a602cf02303201771f07aebf7b Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 23 Apr 2025 20:58:53 -0400 Subject: [PATCH 41/53] add new resource create wizard --- server/routers/resource/createResource.ts | 5 +- .../settings/resources/ResourcesTable.tsx | 13 +- .../page.tsx} | 1138 ++++++++--------- 3 files changed, 572 insertions(+), 584 deletions(-) rename src/app/[orgId]/settings/resources/{CreateResourceForm.tsx => create/page.tsx} (57%) diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index d43a4fdd..af6807b9 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -39,7 +39,6 @@ const createHttpResourceSchema = z isBaseDomain: z.boolean().optional(), siteId: z.number(), http: z.boolean(), - protocol: z.string(), domainId: z.string() }) .strict() @@ -203,7 +202,7 @@ async function createHttpResource( ); } - const { name, subdomain, isBaseDomain, http, protocol, domainId } = + const { name, subdomain, isBaseDomain, http, domainId } = parsedBody.data; const [orgDomain] = await db @@ -262,7 +261,7 @@ async function createHttpResource( name, subdomain, http, - protocol, + protocol: "tcp", ssl: true, isBaseDomain }) diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index fa83a761..2428472e 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -21,10 +21,8 @@ import { } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import CreateResourceForm from "./CreateResourceForm"; import { useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import { set } from "zod"; import { formatAxiosError } from "@app/lib/api"; import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; @@ -58,10 +56,8 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { const api = createApiClient(useEnvContext()); - const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selectedResource, setSelectedResource] = - useState(); + const [selectedResource, setSelectedResource] = useState(); const deleteResource = (resourceId: number) => { api.delete(`/resource/${resourceId}`) @@ -282,11 +278,6 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { return ( <> - - {selectedResource && ( { - setIsCreateModalOpen(true); + router.push(`/${orgId}/settings/resources/create`); }} /> diff --git a/src/app/[orgId]/settings/resources/CreateResourceForm.tsx b/src/app/[orgId]/settings/resources/create/page.tsx similarity index 57% rename from src/app/[orgId]/settings/resources/CreateResourceForm.tsx rename to src/app/[orgId]/settings/resources/create/page.tsx index 9df51e92..d500a809 100644 --- a/src/app/[orgId]/settings/resources/CreateResourceForm.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -1,6 +1,14 @@ "use client"; -import { Button, buttonVariants } from "@app/components/ui/button"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; import { Form, FormControl, @@ -10,48 +18,22 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import { Input } from "@app/components/ui/input"; -import { toast } from "@app/hooks/useToast"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; +import HeaderTitle from "@app/components/SettingsSectionTitle"; import { z } from "zod"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; +import { useEffect, useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Input } from "@app/components/ui/input"; +import { Button } from "@app/components/ui/button"; import { useParams, useRouter } from "next/navigation"; import { ListSitesResponse } from "@server/routers/site"; import { formatAxiosError } from "@app/lib/api"; -import { CheckIcon } from "lucide-react"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { CaretSortIcon } from "@radix-ui/react-icons"; -import CustomDomainInput from "./[resourceId]/CustomDomainInput"; -import { AxiosResponse } from "axios"; -import { Resource } from "@server/db/schemas"; -import { useOrgContext } from "@app/hooks/useOrgContext"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { cn } from "@app/lib/cn"; -import { Switch } from "@app/components/ui/switch"; +import { toast } from "@app/hooks/useToast"; +import { AxiosResponse } from "axios"; +import { Resource } from "@server/db/schemas"; +import { StrategySelect } from "@app/components/StrategySelect"; import { Select, SelectContent, @@ -60,222 +42,74 @@ import { SelectValue } from "@app/components/ui/select"; import { subdomainSchema } from "@server/lib/schemas"; -import Link from "next/link"; -import { SquareArrowOutUpRight } from "lucide-react"; -import CopyTextBox from "@app/components/CopyTextBox"; -import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; -import { Label } from "@app/components/ui/label"; import { ListDomainsResponse } from "@server/routers/domain"; import LoaderPlaceholder from "@app/components/PlaceHolderLoader"; -import { StrategySelect } from "@app/components/StrategySelect"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; +import { cn } from "@app/lib/cn"; -const createResourceFormSchema = z - .object({ - subdomain: z.string().optional(), - domainId: z.string().min(1).optional(), - name: z.string().min(1).max(255), - siteId: z.number(), - http: z.boolean(), - protocol: z.string(), - proxyPort: z.number().optional(), - isBaseDomain: z.boolean().optional() +const baseResourceFormSchema = z.object({ + name: z.string().min(1).max(255), + siteId: z.number(), + http: z.boolean() +}); + +const httpResourceFormSchema = z.discriminatedUnion("isBaseDomain", [ + z.object({ + isBaseDomain: z.literal(true), + domainId: z.string().min(1) + }), + z.object({ + isBaseDomain: z.literal(false), + domainId: z.string().min(1), + subdomain: z.string().pipe(subdomainSchema) }) - .refine( - (data) => { - if (!data.http) { - return z - .number() - .int() - .min(1) - .max(65535) - .safeParse(data.proxyPort).success; - } - return true; - }, - { - message: "Invalid port number", - path: ["proxyPort"] - } - ) - .refine( - (data) => { - if (data.http && !data.isBaseDomain) { - return subdomainSchema.safeParse(data.subdomain).success; - } - return true; - }, - { - message: "Invalid subdomain", - path: ["subdomain"] - } - ); +]); -type CreateResourceFormValues = z.infer; +const tcpUdpResourceFormSchema = z.object({ + protocol: z.string(), + proxyPort: z.number().int().min(1).max(65535) +}); -type CreateResourceFormProps = { - open: boolean; - setOpen: (open: boolean) => void; -}; +type BaseResourceFormValues = z.infer; +type HttpResourceFormValues = z.infer; +type TcpUdpResourceFormValues = z.infer; -export default function CreateResourceForm({ - open, - setOpen -}: CreateResourceFormProps) { - const [formKey, setFormKey] = useState(0); - const api = createApiClient(useEnvContext()); +type ResourceType = "http" | "raw"; - const [loading, setLoading] = useState(false); - const params = useParams(); +interface ResourceTypeOption { + id: ResourceType; + title: string; + description: string; + disabled?: boolean; +} - const orgId = params.orgId; +export default function Page() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { orgId } = useParams(); const router = useRouter(); - const { org } = useOrgContext(); - const { env } = useEnvContext(); - + const [loadingPage, setLoadingPage] = useState(true); const [sites, setSites] = useState([]); const [baseDomains, setBaseDomains] = useState< { domainId: string; baseDomain: string }[] >([]); - const [showSnippets, setShowSnippets] = useState(false); - const [resourceId, setResourceId] = useState(null); - const [domainType, setDomainType] = useState<"subdomain" | "basedomain">( - "subdomain" - ); - const [loadingPage, setLoadingPage] = useState(true); + const [createLoading, setCreateLoading] = useState(false); - const form = useForm({ - resolver: zodResolver(createResourceFormSchema), - defaultValues: { - subdomain: "", - domainId: "", - name: "", - http: true, - protocol: "tcp" - } - }); - - function reset() { - form.reset(); - setSites([]); - setShowSnippets(false); - setResourceId(null); - } - - useEffect(() => { - if (!open) { - return; - } - - reset(); - - const fetchSites = async () => { - const res = await api - .get>(`/org/${orgId}/sites/`) - .catch((e) => { - toast({ - variant: "destructive", - title: "Error fetching sites", - description: formatAxiosError( - e, - "An error occurred when fetching the sites" - ) - }); - }); - - if (res?.status === 200) { - setSites(res.data.data.sites); - - if (res.data.data.sites.length > 0) { - form.setValue("siteId", res.data.data.sites[0].siteId); - } - } - }; - - const fetchDomains = async () => { - const res = await api - .get< - AxiosResponse - >(`/org/${orgId}/domains/`) - .catch((e) => { - toast({ - variant: "destructive", - title: "Error fetching domains", - description: formatAxiosError( - e, - "An error occurred when fetching the domains" - ) - }); - }); - - if (res?.status === 200) { - const domains = res.data.data.domains; - setBaseDomains(domains); - if (domains.length) { - form.setValue("domainId", domains[0].domainId); - setFormKey((k) => k + 1); - } - } - }; - - const load = async () => { - setLoadingPage(true); - - await fetchSites(); - await fetchDomains(); - await new Promise((r) => setTimeout(r, 200)); - - setLoadingPage(false); - }; - - load(); - }, [open]); - - async function onSubmit(data: CreateResourceFormValues) { - const res = await api - .put>( - `/org/${orgId}/site/${data.siteId}/resource/`, - { - name: data.name, - subdomain: data.http ? data.subdomain : undefined, - domainId: data.http ? data.domainId : undefined, - http: data.http, - protocol: data.protocol, - proxyPort: data.http ? undefined : data.proxyPort, - siteId: data.siteId, - isBaseDomain: data.http ? data.isBaseDomain : undefined - } - ) - .catch((e) => { - toast({ - variant: "destructive", - title: "Error creating resource", - description: formatAxiosError( - e, - "An error occurred when creating the resource" - ) - }); - }); - - if (res && res.status === 201) { - const id = res.data.data.resourceId; - setResourceId(id); - - if (data.http) { - goToResource(id); - } else { - setShowSnippets(true); - router.refresh(); - } - } - } - - function goToResource(id?: number) { - // navigate to the resource page - router.push(`/${orgId}/settings/resources/${id || resourceId}`); - } - - const launchOptions = [ + const resourceTypes: ReadonlyArray = [ { id: "http", title: "HTTPS Resource", @@ -286,45 +120,203 @@ export default function CreateResourceForm({ id: "raw", title: "Raw TCP/UDP Resource", description: - "Proxy requests to your app over TCP/UDP using a port number." + "Proxy requests to your app over TCP/UDP using a port number.", + disabled: !env.flags.allowRawResources } ]; + const baseForm = useForm({ + resolver: zodResolver(baseResourceFormSchema), + defaultValues: { + name: "", + http: true + } + }); + + const httpForm = useForm({ + resolver: zodResolver(httpResourceFormSchema), + defaultValues: { + subdomain: "", + domainId: "", + isBaseDomain: false + } + }); + + const tcpUdpForm = useForm({ + resolver: zodResolver(tcpUdpResourceFormSchema), + defaultValues: { + protocol: "tcp", + proxyPort: undefined + } + }); + + async function onSubmit() { + setCreateLoading(true); + + const baseData = baseForm.getValues(); + const isHttp = baseData.http; + + try { + const payload = { + name: baseData.name, + siteId: baseData.siteId, + http: baseData.http + }; + + if (isHttp) { + const httpData = httpForm.getValues(); + if (httpData.isBaseDomain) { + Object.assign(payload, { + domainId: httpData.domainId, + isBaseDomain: true + }); + } else { + Object.assign(payload, { + subdomain: httpData.subdomain, + domainId: httpData.domainId, + isBaseDomain: false + }); + } + } else { + const tcpUdpData = tcpUdpForm.getValues(); + Object.assign(payload, { + protocol: tcpUdpData.protocol, + proxyPort: tcpUdpData.proxyPort + }); + } + + const res = await api + .put< + AxiosResponse + >(`/org/${orgId}/site/${baseData.siteId}/resource/`, payload) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error creating resource", + description: formatAxiosError( + e, + "An error occurred when creating the resource" + ) + }); + }); + + if (res && res.status === 201) { + const id = res.data.data.resourceId; + router.push(`/${orgId}/settings/resources/${id}`); + } + } catch (e) { + console.error("Error creating resource:", e); + toast({ + variant: "destructive", + title: "Error creating resource", + description: "An unexpected error occurred" + }); + } + + setCreateLoading(false); + } + + useEffect(() => { + const load = async () => { + setLoadingPage(true); + + const fetchSites = async () => { + const res = await api + .get< + AxiosResponse + >(`/org/${orgId}/sites/`) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error fetching sites", + description: formatAxiosError( + e, + "An error occurred when fetching the sites" + ) + }); + }); + + if (res?.status === 200) { + setSites(res.data.data.sites); + + if (res.data.data.sites.length > 0) { + baseForm.setValue( + "siteId", + res.data.data.sites[0].siteId + ); + } + } + }; + + const fetchDomains = async () => { + const res = await api + .get< + AxiosResponse + >(`/org/${orgId}/domains/`) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error fetching domains", + description: formatAxiosError( + e, + "An error occurred when fetching the domains" + ) + }); + }); + + if (res?.status === 200) { + const domains = res.data.data.domains; + setBaseDomains(domains); + if (domains.length) { + httpForm.setValue("domainId", domains[0].domainId); + } + } + }; + + await fetchSites(); + await fetchDomains(); + + setLoadingPage(false); + }; + + load(); + }, []); + return ( <> - { - setOpen(val); - setLoading(false); +
+ + +
- // reset all values - form.reset(); - }} - > - - - Create Resource - - Create a new resource to proxy requests to your app - - - - {loadingPage ? ( - - ) : ( -
- {!showSnippets && ( -
+ {!loadingPage && ( +
+ + + + + Resource Information + + + + + ( @@ -335,12 +327,17 @@ export default function CreateResourceForm({ + + This is the display + name for the + resource. + )} /> ( @@ -395,7 +392,7 @@ export default function CreateResourceForm({ site.siteId } onSelect={() => { - form.setValue( + baseForm.setValue( "siteId", site.siteId ); @@ -430,35 +427,61 @@ export default function CreateResourceForm({ )} /> + + + + + - {!env.flags.allowRawResources || ( -
- - Resource Type - - - form.setValue( - "http", - value === "http" - ) - } - /> - - You cannot change the - type of resource after - creation. - -
- )} + + + + Resource Type + + + Determine how you want to access your + resource + + + + { + baseForm.setValue( + "http", + value === "http" + ); + }} + cols={2} + /> + + - {form.watch("http") && - env.flags + {baseForm.watch("http") ? ( + + + + HTTPS Settings + + + Configure how your resource will be + accessed over HTTPS + + + + +
+ + {env.flags .allowBaseDomainResources && ( ( @@ -467,20 +490,15 @@ export default function CreateResourceForm({ + + + + )} + /> +
+
+ ( + + + + + - - - )} - /> -
-
- ( - - - - - )} - /> -
+ + {baseDomains.map( + ( + option + ) => ( + + . + { + option.baseDomain + } + + ) + )} + + + + + )} + />
- ) : ( - ( - - - Base - Domain - - - - - )} - /> - )} - - )} + + The subdomain where + your resource will + be accessible. + + + )} - {!form.watch("http") && ( - <> + {httpForm.watch( + "isBaseDomain" + ) && ( ( - Protocol + Base Domain )} /> - ( - - - Port Number - + )} + + + + + + ) : ( + + + + TCP/UDP Settings + + + Configure how your resource will be + accessed over TCP/UDP + + + + +
+ + ( + + + Protocol + + - field.onChange( - e - .target - .value - ? parseInt( - e - .target - .value - ) - : null - ) - } - /> + + + - - - The external - port number - to proxy - requests. - - - )} - /> - - )} - - - )} - - {showSnippets && ( -
-
-
-

- Traefik: Add Entrypoints -

- + + TCP + + + UDP + + + + + + )} /> -
-
-
-
-

- Gerbil: Expose Ports in - Docker Compose -

- ( + + + Port Number + + + + field.onChange( + e + .target + .value + ? parseInt( + e + .target + .value + ) + : undefined + ) + } + /> + + + + The external + port number to + proxy requests. + + + )} /> -
-
+ + + + + + )} + - - - Learn how to configure TCP/UDP - resources - - - -
- )} -
- )} - - - - - - {!showSnippets && ( - - )} +
+ + - )} - - - + if (baseValid && settingsValid) { + onSubmit(); + } + }} + loading={createLoading} + > + Create Resource + +
+
+ )} ); } From 91b4bb4683a6d49262c7086f49ebd95ea98967c6 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 23 Apr 2025 23:08:25 -0400 Subject: [PATCH 42/53] move proxy related settings to new proxy tab for resource --- server/routers/resource/updateResource.ts | 1 + server/routers/traefik/getTraefikConfig.ts | 4 +- src/app/[orgId]/settings/general/page.tsx | 2 +- .../settings/resources/ResourcesTable.tsx | 5 +- .../resources/[resourceId]/general/page.tsx | 161 +------ .../resources/[resourceId]/layout.tsx | 4 +- .../settings/resources/[resourceId]/page.tsx | 2 +- .../{connectivity => proxy}/page.tsx | 406 +++++++++++++----- .../[orgId]/settings/sites/CreateSiteForm.tsx | 4 +- src/app/[orgId]/settings/sites/SitesTable.tsx | 2 +- .../settings/sites/[niceId]/general/page.tsx | 2 +- src/app/[orgId]/settings/sites/page.tsx | 2 +- src/app/admin/idp/[idpId]/general/page.tsx | 2 +- src/components/Breadcrumbs.tsx | 4 +- 14 files changed, 324 insertions(+), 277 deletions(-) rename src/app/[orgId]/settings/resources/[resourceId]/{connectivity => proxy}/page.tsx (62%) diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index d17ad499..009bbaff 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -99,6 +99,7 @@ const updateRawResourceBodySchema = z .object({ name: z.string().min(1).max(255).optional(), proxyPort: z.number().int().min(1).max(65535).optional(), + stickySession: z.boolean().optional(), enabled: z.boolean().optional() }) .strict() diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index e6488b9f..2fd656ba 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -41,7 +41,7 @@ export async function traefikConfigProvider( orgId: orgs.orgId }, enabled: resources.enabled, - stickySession: resources.stickySessionk, + stickySession: resources.stickySession, tlsServerName: resources.tlsServerName, setHostHeader: resources.setHostHeader }) @@ -288,7 +288,7 @@ export async function traefikConfigProvider( ? { sticky: { cookie: { - name: "pangolin_sticky", + name: "p_sticky", // TODO: make this configurable via config.yml like other cookies secure: resource.ssl, httpOnly: true } diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 49f3d834..9819be59 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -234,7 +234,7 @@ export default function GeneralPage() { loading={loadingSave} disabled={loadingSave} > - Save Settings + Save General Settings diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index 2428472e..bfb4f08b 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -57,7 +57,8 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { const api = createApiClient(useEnvContext()); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selectedResource, setSelectedResource] = useState(); + const [selectedResource, setSelectedResource] = + useState(); const deleteResource = (resourceId: number) => { api.delete(`/resource/${resourceId}`) @@ -238,7 +239,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { Not Protected ) : ( - -- + - )} ); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index cf75a426..f1e152d5 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -109,42 +109,8 @@ const TransferFormSchema = z.object({ siteId: z.number() }); -const AdvancedFormSchema = z - .object({ - http: z.boolean(), - tlsServerName: z.string().optional(), - setHostHeader: z.string().optional() - }) - .refine( - (data) => { - if (data.tlsServerName) { - return tlsNameSchema.safeParse(data.tlsServerName).success; - } - return true; - }, - { - message: - "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.", - path: ["tlsServerName"] - } - ) - .refine( - (data) => { - if (data.setHostHeader) { - return tlsNameSchema.safeParse(data.setHostHeader).success; - } - return true; - }, - { - message: - "Invalid custom Host Header value. Use domain name format, or save empty to unset the custom Host Header", - path: ["tlsServerName"] - } - ); - type GeneralFormValues = z.infer; type TransferFormValues = z.infer; -type AdvancedFormValues = z.infer; export default function GeneralForm() { const [formKey, setFormKey] = useState(0); @@ -185,20 +151,6 @@ export default function GeneralForm() { mode: "onChange" }); - const advancedForm = useForm({ - resolver: zodResolver(AdvancedFormSchema), - defaultValues: { - http: resource.http, - tlsServerName: resource.http - ? resource.tlsServerName || "" - : undefined, - setHostHeader: resource.http - ? resource.setHostHeader || "" - : undefined - }, - mode: "onChange" - }); - const transferForm = useForm({ resolver: zodResolver(TransferFormSchema), defaultValues: { @@ -327,46 +279,6 @@ export default function GeneralForm() { setTransferLoading(false); } - async function onSubmitAdvanced(data: AdvancedFormValues) { - setSaveLoading(true); - - const res = await api - .post>( - `resource/${resource?.resourceId}`, - { - tlsServerName: data.http ? data.tlsServerName : undefined, - setHostHeader: data.http ? data.setHostHeader : undefined - } - ) - .catch((e) => { - toast({ - variant: "destructive", - title: "Failed to update resource", - description: formatAxiosError( - e, - "An error occurred while updating the resource" - ) - }); - }); - - if (res && res.status === 200) { - toast({ - title: "Resource updated", - description: "The resource has been updated successfully" - }); - - const resource = res.data.data; - - updateResource({ - tlsServerName: data.tlsServerName, - setHostHeader: data.setHostHeader - }); - - router.refresh(); - } - setSaveLoading(false); - } - async function toggleResourceEnabled(val: boolean) { const res = await api .post>( @@ -684,82 +596,11 @@ export default function GeneralForm() { disabled={saveLoading} form="general-settings-form" > - Save Settings + Save General Settings - {resource.http && ( - <> - - - - Advanced - - - Adjust advanced settings for the resource, - like customize the Host Header or set a TLS - Server Name for SNI based routing. - - - - -
- - - TLS Server Name (optional) - - ( - - - - - - - )} - /> - - - Custom Host Header (optional) - - ( - - - - - - - )} - /> - - -
-
- - - - -
- - )} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx index edaf7962..edb21303 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx @@ -86,8 +86,8 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { href: `/{orgId}/settings/resources/{resourceId}/general` }, { - title: "Connectivity", - href: `/{orgId}/settings/resources/{resourceId}/connectivity` + title: "Proxy", + href: `/{orgId}/settings/resources/{resourceId}/proxy` } ]; diff --git a/src/app/[orgId]/settings/resources/[resourceId]/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/page.tsx index 8eb27e4e..a0d45a94 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/page.tsx @@ -5,6 +5,6 @@ export default async function ResourcePage(props: { }) { const params = await props.params; redirect( - `/${params.orgId}/settings/resources/${params.resourceId}/connectivity` + `/${params.orgId}/settings/resources/${params.resourceId}/proxy` ); } diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx similarity index 62% rename from src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx rename to src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index 18bb8f34..8c027d30 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -60,17 +60,22 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionFooter + SettingsSectionFooter, + SettingsSectionForm } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; import { useRouter } from "next/navigation"; import { isTargetValid } from "@server/lib/validators"; +import { tlsNameSchema } from "@server/lib/schemas"; const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), port: z.coerce.number().int().positive() - // protocol: z.string(), +}); + +const targetsSettingsSchema = z.object({ + stickySession: z.boolean() }); type LocalTarget = Omit< @@ -81,6 +86,47 @@ type LocalTarget = Omit< "protocol" >; +const proxySettingsSchema = z.object({ + setHostHeader: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: + "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header." + } + ) +}); + +const tlsSettingsSchema = z.object({ + ssl: z.boolean(), + tlsServerName: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: + "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name." + } + ) +}); + +type ProxySettingsValues = z.infer; +type TlsSettingsValues = z.infer; +type TargetsSettingsValues = z.infer; + export default function ReverseProxyTargets(props: { params: Promise<{ resourceId: number }>; }) { @@ -93,10 +139,10 @@ export default function ReverseProxyTargets(props: { const [targets, setTargets] = useState([]); const [site, setSite] = useState(); const [targetsToRemove, setTargetsToRemove] = useState([]); - const [sslEnabled, setSslEnabled] = useState(resource.ssl); - const [stickySession, setStickySession] = useState(resource.stickySession); - const [loading, setLoading] = useState(false); + const [httpsTlsLoading, setHttpsTlsLoading] = useState(false); + const [targetsLoading, setTargetsLoading] = useState(false); + const [proxySettingsLoading, setProxySettingsLoading] = useState(false); const [pageLoading, setPageLoading] = useState(true); const router = useRouter(); @@ -110,6 +156,28 @@ export default function ReverseProxyTargets(props: { } as z.infer }); + const tlsSettingsForm = useForm({ + resolver: zodResolver(tlsSettingsSchema), + defaultValues: { + ssl: resource.ssl, + tlsServerName: resource.tlsServerName || "" + } + }); + + const proxySettingsForm = useForm({ + resolver: zodResolver(proxySettingsSchema), + defaultValues: { + setHostHeader: resource.setHostHeader || "" + } + }); + + const targetsSettingsForm = useForm({ + resolver: zodResolver(targetsSettingsSchema), + defaultValues: { + stickySession: resource.stickySession + } + }); + useEffect(() => { const fetchTargets = async () => { try { @@ -230,13 +298,12 @@ export default function ReverseProxyTargets(props: { async function saveTargets() { try { - setLoading(true); + setTargetsLoading(true); for (let target of targets) { const data = { ip: target.ip, port: target.port, - // protocol: target.protocol, method: target.method, enabled: target.enabled }; @@ -249,27 +316,22 @@ export default function ReverseProxyTargets(props: { } else if (target.updated) { await api.post(`/target/${target.targetId}`, data); } - - setTargets([ - ...targets.map((t) => { - let res = { - ...t, - new: false, - updated: false - }; - return res; - }) - ]); } for (const targetId of targetsToRemove) { await api.delete(`/target/${targetId}`); - setTargets(targets.filter((t) => t.targetId !== targetId)); } + // Save sticky session setting + const stickySessionData = targetsSettingsForm.getValues(); + await api.post(`/resource/${params.resourceId}`, { + stickySession: stickySessionData.stickySession + }); + updateResource({ stickySession: stickySessionData.stickySession }); + toast({ title: "Targets updated", - description: "Targets updated successfully" + description: "Targets and settings updated successfully" }); setTargetsToRemove([]); @@ -278,72 +340,75 @@ export default function ReverseProxyTargets(props: { console.error(err); toast({ variant: "destructive", - title: "Operation failed", + title: "Failed to update targets", description: formatAxiosError( err, - "An error occurred during the save operation" + "An error occurred while updating targets" ) }); - } - - setLoading(false); - } - - async function saveSsl(val: boolean) { - const res = await api - .post(`/resource/${params.resourceId}`, { - ssl: val - }) - .catch((err) => { - console.error(err); - toast({ - variant: "destructive", - title: "Failed to update SSL configuration", - description: formatAxiosError( - err, - "An error occurred while updating the SSL configuration" - ) - }); - }); - - if (res && res.status === 200) { - setSslEnabled(val); - updateResource({ ssl: val }); - - toast({ - title: "SSL Configuration", - description: "SSL configuration updated successfully" - }); - router.refresh(); + } finally { + setTargetsLoading(false); } } - async function saveStickySession(val: boolean) { - const res = await api - .post(`/resource/${params.resourceId}`, { - stickySession: val - }) - .catch((err) => { - console.error(err); - toast({ - variant: "destructive", - title: "Failed to update sticky session configuration", - description: formatAxiosError( - err, - "An error occurred while updating the sticky session configuration" - ) - }); + async function saveTlsSettings(data: TlsSettingsValues) { + try { + setHttpsTlsLoading(true); + await api.post(`/resource/${params.resourceId}`, { + ssl: data.ssl, + tlsServerName: data.tlsServerName || undefined + }); + updateResource({ + ...resource, + ssl: data.ssl, + tlsServerName: data.tlsServerName || undefined }); - - if (res && res.status === 200) { - setStickySession(val); - updateResource({ stickySession: val }); - toast({ - title: "Sticky Session Configuration", - description: "Sticky session configuration updated successfully" + title: "TLS settings updated", + description: "Your TLS settings have been updated successfully" }); - router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to update TLS settings", + description: formatAxiosError( + err, + "An error occurred while updating TLS settings" + ) + }); + } finally { + setHttpsTlsLoading(false); + } + } + + async function saveProxySettings(data: ProxySettingsValues) { + try { + setProxySettingsLoading(true); + await api.post(`/resource/${params.resourceId}`, { + setHostHeader: data.setHostHeader || undefined + }); + updateResource({ + ...resource, + setHostHeader: data.setHostHeader || undefined + }); + toast({ + title: "Proxy settings updated", + description: + "Your proxy settings have been updated successfully" + }); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: "Failed to update proxy settings", + description: formatAxiosError( + err, + "An error occurred while updating proxy settings" + ) + }); + } finally { + setProxySettingsLoading(false); } } @@ -486,46 +551,128 @@ export default function ReverseProxyTargets(props: { - Advanced Configuration + HTTPS & TLS Settings - Configure advanced settings for your resource + Configure TLS settings for your resource - {targets.length >= 2 && ( - { - await saveStickySession(val); - }} - /> - )} - { - await saveSsl(val); - }} - /> + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + ( + + + TLS Server Name (SNI) + + + + + + The TLS Server Name to use + for SNI. Leave empty to use + the default. + + + + )} + /> + + +
+ + +
)} - {/* Targets Section */} + - Target Configuration + Targets Configuration Set up targets to route traffic to your services + +
+ + {targets.length >= 2 && ( + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + )} + + +
+
+ + {resource.http && ( + + + + Additional Proxy Settings + + + Configure how your resource handles proxy settings + + + + + + + ( + + + Custom Host Header + + + + + + The Host header to set when + proxying requests. Leave + empty to use the default. + + + + )} + /> + + + + + + + +
+ )} ); } diff --git a/src/app/[orgId]/settings/sites/CreateSiteForm.tsx b/src/app/[orgId]/settings/sites/CreateSiteForm.tsx index fb90ba02..c4da2336 100644 --- a/src/app/[orgId]/settings/sites/CreateSiteForm.tsx +++ b/src/app/[orgId]/settings/sites/CreateSiteForm.tsx @@ -227,11 +227,11 @@ export default function CreateSiteForm({ mbIn: data.type == "wireguard" || data.type == "newt" ? "0 MB" - : "--", + : "-", mbOut: data.type == "wireguard" || data.type == "newt" ? "0 MB" - : "--", + : "-", orgId: orgId as string, type: data.type as any, online: false diff --git a/src/app/[orgId]/settings/sites/SitesTable.tsx b/src/app/[orgId]/settings/sites/SitesTable.tsx index c0cc5801..c032800f 100644 --- a/src/app/[orgId]/settings/sites/SitesTable.tsx +++ b/src/app/[orgId]/settings/sites/SitesTable.tsx @@ -163,7 +163,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { ); } } else { - return --; + return -; } } }, diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index 66a7ddd1..f107d960 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -134,7 +134,7 @@ export default function GeneralPage() { loading={loading} disabled={loading} > - Save Settings + Save General Settings
diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 917a74a3..442328b4 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -25,7 +25,7 @@ export default async function SitesPage(props: SitesPageProps) { function formatSize(mb: number, type: string): string { if (type === "local") { - return "--"; // because we are not able to track the data use in a local site right now + return "-"; // because we are not able to track the data use in a local site right now } if (mb >= 1024 * 1024) { return `${(mb / (1024 * 1024)).toFixed(2)} TB`; diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 7e23db1b..44af4434 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -499,7 +499,7 @@ export default function GeneralPage() { loading={loading} disabled={loading} > - Save Settings + Save General Settings diff --git a/src/components/Breadcrumbs.tsx b/src/components/Breadcrumbs.tsx index 3f170fbe..9740759a 100644 --- a/src/components/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs.tsx @@ -37,8 +37,8 @@ export function Breadcrumbs() { // label = "Roles"; // } else if (segment === "invitations") { // label = "Invitations"; - // } else if (segment === "connectivity") { - // label = "Connectivity"; + // } else if (segment === "proxy") { + // label = "proxy"; // } else if (segment === "authentication") { // label = "Authentication"; // } From c3dbc64a58d6e274fea7523f53683850eb729c71 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 24 Apr 2025 10:42:14 -0400 Subject: [PATCH 43/53] remove hover on badge --- src/components/ui/badge.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 0c44fda2..f6da3aa8 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -9,11 +9,11 @@ const badgeVariants = cva( variants: { variant: { default: - "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + "border-transparent bg-primary text-primary-foreground", secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + "border-transparent bg-secondary text-secondary-foreground", destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + "border-transparent bg-destructive text-destructive-foreground", outline: "text-foreground", green: "border-transparent bg-green-300", yellow: "border-transparent bg-yellow-300", From 80d76befc923af5593d257a2a8bc0397a3f177c3 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 25 Apr 2025 17:13:20 -0400 Subject: [PATCH 44/53] change terms --- src/app/admin/idp/[idpId]/general/page.tsx | 7 ++++-- src/app/admin/idp/create/page.tsx | 7 ++++-- src/app/navigation.tsx | 4 ++-- src/components/SidebarNav.tsx | 28 +++++++++++++++------- src/components/ui/badge.tsx | 1 + 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 44af4434..7bed1349 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -238,8 +238,11 @@ export default function GeneralPage() { ); }} /> - - Enterprise + + Professional diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index 45b30ac1..3842bf76 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -198,8 +198,11 @@ export default function Page() { ); }} /> - - Enterprise + + Professional diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index ea33b294..cc550d76 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -68,7 +68,7 @@ export const orgNavItems: SidebarNavItem[] = [ title: "API Keys", href: "/{orgId}/settings/api-keys", icon: , - showEnterprise: true + showProfessional: true }, { title: "Settings", @@ -87,7 +87,7 @@ export const adminNavItems: SidebarNavItem[] = [ title: "API Keys", href: "/{orgId}/settings/api-keys", icon: , - showEnterprise: true + showProfessional: true }, { title: "Identity Providers", diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 31dd2ce2..d8f64db0 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -14,7 +14,7 @@ export interface SidebarNavItem { icon?: React.ReactNode; children?: SidebarNavItem[]; autoExpand?: boolean; - showEnterprise?: boolean; + showProfessional?: boolean; } export interface SidebarNavProps extends React.HTMLAttributes { @@ -98,8 +98,8 @@ export function SidebarNav({ const hasChildren = item.children && item.children.length > 0; const isExpanded = expandedItems.has(hydratedHref); const indent = level * 28; // Base indent for each level - const isEnterprise = item.showEnterprise; - const isDisabled = disabled || isEnterprise; + const isProfessional = item.showProfessional; + const isDisabled = disabled || isProfessional; return (
@@ -114,7 +114,7 @@ export function SidebarNav({ )} > -
+
{item.icon && ( - {item.icon} + + {item.icon} + )} {item.title}
- {isEnterprise && ( - - Enterprise + {isProfessional && ( + + Professional )} diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index f6da3aa8..da278d1f 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -10,6 +10,7 @@ const badgeVariants = cva( variant: { default: "border-transparent bg-primary text-primary-foreground", + outlinePrimary: "border-transparent bg-transparent border-primary text-primary", secondary: "border-transparent bg-secondary text-secondary-foreground", destructive: From 4819f410e61569aa9a0cc2359ad6c5ffa7cdf223 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 27 Apr 2025 13:03:00 -0400 Subject: [PATCH 45/53] add license system and ui --- LICENSE | 32 ++ README.md | 2 +- esbuild.mjs | 1 + package-lock.json | 126 ++++- package.json | 2 + server/db/schemas/hostMeta.ts | 0 server/db/schemas/index.ts | 1 + server/db/schemas/proSchema.ts | 17 + server/lib/config.ts | 14 +- server/license/license.ts | 429 ++++++++++++++++ server/license/licenseJwt.ts | 114 +++++ server/routers/external.ts | 25 + server/routers/idp/createOidcIdp.ts | 7 +- server/routers/idp/updateOidcIdp.ts | 7 +- server/routers/internal.ts | 6 + server/routers/license/activateLicense.ts | 62 +++ server/routers/license/deleteLicenseKey.ts | 76 +++ server/routers/license/getLicenseStatus.ts | 36 ++ server/routers/license/index.ts | 10 + server/routers/license/listLicenseKeys.ts | 36 ++ server/routers/license/recheckStatus.ts | 42 ++ .../supporterKey/isSupporterKeyVisible.ts | 7 + server/setup/index.ts | 2 + server/setup/setHostMeta.ts | 17 + src/app/admin/idp/[idpId]/general/page.tsx | 18 +- src/app/admin/idp/[idpId]/layout.tsx | 3 +- src/app/admin/idp/create/page.tsx | 18 +- .../admin/license/LicenseKeysDataTable.tsx | 147 ++++++ .../components/SitePriceCalculator.tsx | 131 +++++ src/app/admin/license/page.tsx | 471 ++++++++++++++++++ .../oidc/callback/ValidateOidcToken.tsx | 7 + src/app/components/LicenseViolation.tsx | 45 ++ src/app/layout.tsx | 43 +- src/app/navigation.tsx | 8 +- src/components/CopyTextBox.tsx | 16 +- src/components/CopyToClipboard.tsx | 9 +- src/components/HorizontalTabs.tsx | 63 ++- src/components/Layout.tsx | 8 - src/components/ProfessionalContentOverlay.tsx | 42 ++ src/components/Settings.tsx | 6 +- src/components/SidebarNav.tsx | 4 +- src/components/ui/badge.tsx | 4 +- src/components/ui/progress.tsx | 30 ++ src/contexts/licenseStatusContext.ts | 20 + src/hooks/useLicenseStatusContext.ts | 17 + src/providers/LicenseStatusProvider.tsx | 72 +++ 46 files changed, 2159 insertions(+), 94 deletions(-) create mode 100644 server/db/schemas/hostMeta.ts create mode 100644 server/db/schemas/proSchema.ts create mode 100644 server/license/license.ts create mode 100644 server/license/licenseJwt.ts create mode 100644 server/routers/license/activateLicense.ts create mode 100644 server/routers/license/deleteLicenseKey.ts create mode 100644 server/routers/license/getLicenseStatus.ts create mode 100644 server/routers/license/index.ts create mode 100644 server/routers/license/listLicenseKeys.ts create mode 100644 server/routers/license/recheckStatus.ts create mode 100644 server/setup/setHostMeta.ts create mode 100644 src/app/admin/license/LicenseKeysDataTable.tsx create mode 100644 src/app/admin/license/components/SitePriceCalculator.tsx create mode 100644 src/app/admin/license/page.tsx create mode 100644 src/app/components/LicenseViolation.tsx create mode 100644 src/components/ProfessionalContentOverlay.tsx create mode 100644 src/components/ui/progress.tsx create mode 100644 src/contexts/licenseStatusContext.ts create mode 100644 src/hooks/useLicenseStatusContext.ts create mode 100644 src/providers/LicenseStatusProvider.tsx diff --git a/LICENSE b/LICENSE index 0ad25db4..8c5cfb89 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,35 @@ +Copyright (c) 2025 Fossorial, LLC. + +Portions of this software are licensed as follows: + +* All files that include a header specifying they are licensed under the + "Fossorial Commercial License" are governed by the Fossorial Commercial + License terms. The specific terms applicable to each customer depend on the + commercial license tier agreed upon in writing with Fossorial LLC. + Unauthorized use, copying, modification, or distribution is strictly + prohibited. + +* All files that include a header specifying they are licensed under the GNU + Affero General Public License, Version 3 ("AGPL-3"), are governed by the + AGPL-3 terms. A full copy of the AGPL-3 license is provided below. However, + these files are also available under the Fossorial Commercial License if a + separate commercial license agreement has been executed between the customer + and Fossorial LLC. + +* All files without a license header are, by default, licensed under the GNU + Affero General Public License, Version 3 (AGPL-3). These files may also be + made available under the Fossorial Commercial License upon agreement with + Fossorial LLC. + +* All third-party components included in this repository are licensed under + their respective original licenses, as provided by their authors. + +Please consult the header of each individual file to determine the applicable +license. For AGPL-3 licensed files, dual-licensing under the Fossorial +Commercial License is available subject to written agreement with Fossorial +LLC. + + GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/README.md b/README.md index 0b130dc6..2cee8bff 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ View the [project board](https://github.com/orgs/fosrl/projects/1) for more deta ## Licensing -Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. To see our commercial offerings, please see our [website](https://fossorial.io) for details. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io). +Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. Please see the [LICENSE](./LICENSE) file in the repository for details. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io). ## Contributions diff --git a/esbuild.mjs b/esbuild.mjs index 321c6288..48a2fb31 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -52,6 +52,7 @@ esbuild bundle: true, outfile: argv.out, format: "esm", + minify: true, banner: { js: banner, }, diff --git a/package-lock.json b/package-lock.json index 812635e4..5bb58341 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@radix-ui/react-icons": "1.3.2", "@radix-ui/react-label": "2.1.1", "@radix-ui/react-popover": "1.1.4", + "@radix-ui/react-progress": "^1.1.4", "@radix-ui/react-radio-group": "1.2.2", "@radix-ui/react-select": "2.1.4", "@radix-ui/react-separator": "1.1.1", @@ -78,6 +79,7 @@ "swagger-ui-express": "^5.0.1", "tailwind-merge": "2.6.0", "tw-animate-css": "^1.2.5", + "uuid": "^11.1.0", "vaul": "1.1.2", "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", @@ -3351,6 +3353,101 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.4.tgz", + "integrity": "sha512-8rl9w7lJdcVPor47Dhws9mUHRHLE+8JEgyJRdNWCpGPa6HIlr3eh+Yn9gyx1CnCLbw5naHsI2gaO9dBWO50vzw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-radio-group": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.2.tgz", @@ -4381,7 +4478,7 @@ "version": "7.6.12", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -4522,7 +4619,7 @@ "version": "22.14.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4556,7 +4653,7 @@ "version": "19.1.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz", "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -4566,7 +4663,7 @@ "version": "19.1.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -6178,7 +6275,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -9487,7 +9584,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -15865,6 +15962,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz", "integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==", + "dev": true, "license": "MIT" }, "node_modules/tapable": { @@ -16232,6 +16330,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -16263,7 +16362,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -16378,6 +16477,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 2cca81ec..cb18a7cc 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-icons": "1.3.2", "@radix-ui/react-label": "2.1.1", "@radix-ui/react-popover": "1.1.4", + "@radix-ui/react-progress": "^1.1.4", "@radix-ui/react-radio-group": "1.2.2", "@radix-ui/react-select": "2.1.4", "@radix-ui/react-separator": "1.1.1", @@ -89,6 +90,7 @@ "swagger-ui-express": "^5.0.1", "tailwind-merge": "2.6.0", "tw-animate-css": "^1.2.5", + "uuid": "^11.1.0", "vaul": "1.1.2", "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", diff --git a/server/db/schemas/hostMeta.ts b/server/db/schemas/hostMeta.ts new file mode 100644 index 00000000..e69de29b diff --git a/server/db/schemas/index.ts b/server/db/schemas/index.ts index 686fbd9e..adf6d066 100644 --- a/server/db/schemas/index.ts +++ b/server/db/schemas/index.ts @@ -1 +1,2 @@ export * from "./schema"; +export * from "./proSchema"; diff --git a/server/db/schemas/proSchema.ts b/server/db/schemas/proSchema.ts new file mode 100644 index 00000000..6b1d879c --- /dev/null +++ b/server/db/schemas/proSchema.ts @@ -0,0 +1,17 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +export const licenseKey = sqliteTable("licenseKey", { + licenseKeyId: text("licenseKeyId").primaryKey().notNull(), + instanceId: text("instanceId").notNull(), + token: text("token").notNull() +}); + +export const hostMeta = sqliteTable("hostMeta", { + hostMetaId: text("hostMetaId").primaryKey().notNull(), + createdAt: integer("createdAt").notNull() +}); diff --git a/server/lib/config.ts b/server/lib/config.ts index 8bac6809..e9675284 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -12,8 +12,8 @@ import { passwordSchema } from "@server/auth/passwordSchema"; import stoi from "./stoi"; import db from "@server/db"; import { SupporterKey, supporterKey } from "@server/db/schemas"; -import { suppressDeprecationWarnings } from "moment"; import { eq } from "drizzle-orm"; +import { license } from "@server/license/license"; const portSchema = z.number().positive().gt(0).lte(65535); @@ -267,13 +267,19 @@ export class Config { : "false"; process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; - if (!this.isDev) { - this.checkSupporterKey(); - } + this.checkKeyStatus(); this.rawConfig = parsedConfig.data; } + private async checkKeyStatus() { + const licenseStatus = await license.check(); + console.log("License status", licenseStatus); + if (!licenseStatus.isHostLicensed) { + this.checkSupporterKey(); + } + } + public getRawConfig() { return this.rawConfig; } diff --git a/server/license/license.ts b/server/license/license.ts new file mode 100644 index 00000000..68704e26 --- /dev/null +++ b/server/license/license.ts @@ -0,0 +1,429 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import db from "@server/db"; +import { hostMeta, licenseKey, sites } from "@server/db/schemas"; +import logger from "@server/logger"; +import NodeCache from "node-cache"; +import { validateJWT } from "./licenseJwt"; +import { count, eq } from "drizzle-orm"; +import moment from "moment"; + +export type LicenseStatus = { + isHostLicensed: boolean; // Are there any license keys? + isLicenseValid: boolean; // Is the license key valid? + hostId: string; // Host ID + maxSites?: number; + usedSites?: number; +}; + +export type LicenseKeyCache = { + licenseKey: string; + valid: boolean; + iat?: Date; + type?: "LICENSE" | "SITES"; + numSites?: number; +}; + +type ActivateLicenseKeyAPIResponse = { + data: { + instanceId: string; + }; + success: boolean; + error: string; + message: string; + status: number; +}; + +type ValidateLicenseAPIResponse = { + data: { + licenseKeys: { + [key: string]: string; + }; + }; + success: boolean; + error: string; + message: string; + status: number; +}; + +type TokenPayload = { + valid: boolean; + type: "LICENSE" | "SITES"; + quantity: number; + terminateAt: string; // ISO + iat: number; // Issued at +}; + +export class License { + private phoneHomeInterval = 6 * 60 * 60; // 6 hours = 6 * 60 * 60 = 21600 seconds + private validationServerUrl = + "https://api.dev.fossorial.io/api/v1/license/professional/validate"; + private activationServerUrl = + "https://api.dev.fossorial.io/api/v1/license/professional/activate"; + + private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval }); + private licenseKeyCache = new NodeCache(); + + private ephemeralKey!: string; + private statusKey = "status"; + + private publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF +FNkRDggQfYO6Ae+EWHGujZ9WYAZ10spLh9F/zoLhhr3XhsjpoRXwMfgNuO5HstWf +CYM20I0l7EUUMWEyWd4tZLd+5XQ4jY5xWOCWyFJAGQSp7flcRmxdfde+l+xg9eKl +apbY84aVp09/GqM96hCS+CsQZrhohu/aOqYVB/eAhF01qsbmiZ7Y3WtdhTldveYt +h4mZWGmjf8d/aEgePf/tk1gp0BUxf+Ae5yqoAqU+6aiFbjJ7q1kgxc18PWFGfE9y +zSk+OZk887N5ThQ52154+oOUCMMR2Y3t5OH1hVZod51vuY2u5LsQXsf+87PwB91y +LQIDAQAB +-----END PUBLIC KEY-----`; + + constructor(private hostId: string) { + this.ephemeralKey = Buffer.from( + JSON.stringify({ ts: new Date().toISOString() }) + ).toString("base64"); + + setInterval( + () => { + this.check(); + }, + 1000 * 60 * 60 + ); // 1 hour = 60 * 60 = 3600 seconds + } + + public listKeys(): LicenseKeyCache[] { + const keys = this.licenseKeyCache.keys(); + return keys.map((key) => { + return this.licenseKeyCache.get(key)!; + }); + } + + public async forceRecheck() { + this.statusCache.flushAll(); + this.licenseKeyCache.flushAll(); + + return await this.check(); + } + + public async isUnlocked(): Promise { + const status = await this.check(); + if (status.isHostLicensed) { + if (status.isLicenseValid) { + return true; + } + } + return false; + } + + public async check(): Promise { + // Set used sites + const [siteCount] = await db + .select({ + value: count() + }) + .from(sites); + + const status: LicenseStatus = { + hostId: this.hostId, + isHostLicensed: true, + isLicenseValid: false, + maxSites: undefined, + usedSites: 150 + }; + + try { + if (this.statusCache.has(this.statusKey)) { + const res = this.statusCache.get("status") as LicenseStatus; + res.usedSites = status.usedSites; + return res; + } + + // Invalidate all + this.licenseKeyCache.flushAll(); + + logger.debug("Checking license status..."); + + const allKeysRes = await db.select().from(licenseKey); + + if (allKeysRes.length === 0) { + status.isHostLicensed = false; + return status; + } + + // Validate stored license keys + for (const key of allKeysRes) { + try { + const payload = validateJWT( + key.token, + this.publicKey + ); + + this.licenseKeyCache.set( + key.licenseKeyId, + { + licenseKey: key.licenseKeyId, + valid: payload.valid, + type: payload.type, + numSites: payload.quantity, + iat: new Date(payload.iat * 1000) + } + ); + } catch (e) { + logger.error( + `Error validating license key: ${key.licenseKeyId}` + ); + logger.error(e); + + this.licenseKeyCache.set( + key.licenseKeyId, + { + licenseKey: key.licenseKeyId, + valid: false + } + ); + } + } + + const keys = allKeysRes.map((key) => ({ + licenseKey: key.licenseKeyId, + instanceId: key.instanceId + })); + + let apiResponse: ValidateLicenseAPIResponse | undefined; + try { + // Phone home to validate license keys + apiResponse = await this.phoneHome(keys); + + if (!apiResponse?.success) { + throw new Error(apiResponse?.error); + } + } catch (e) { + logger.error("Error communicating with license server:"); + logger.error(e); + } + + logger.debug("Validate response", apiResponse); + + // Check and update all license keys with server response + for (const key of keys) { + try { + const cached = this.licenseKeyCache.get( + key.licenseKey + )!; + const licenseKeyRes = + apiResponse?.data?.licenseKeys[key.licenseKey]; + + if (!apiResponse || !licenseKeyRes) { + logger.debug( + `No response from server for license key: ${key.licenseKey}` + ); + if (cached.iat) { + const exp = moment(cached.iat) + .add(7, "days") + .toDate(); + if (exp > new Date()) { + logger.debug( + `Using cached license key: ${key.licenseKey}, valid ${cached.valid}` + ); + continue; + } + } + + logger.debug( + `Can't trust license key: ${key.licenseKey}` + ); + cached.valid = false; + this.licenseKeyCache.set( + key.licenseKey, + cached + ); + continue; + } + + const payload = validateJWT( + licenseKeyRes, + this.publicKey + ); + cached.valid = payload.valid; + cached.type = payload.type; + cached.numSites = payload.quantity; + cached.iat = new Date(payload.iat * 1000); + + await db + .update(licenseKey) + .set({ + token: licenseKeyRes + }) + .where(eq(licenseKey.licenseKeyId, key.licenseKey)); + + this.licenseKeyCache.set( + key.licenseKey, + cached + ); + } catch (e) { + logger.error(`Error validating license key: ${key}`); + logger.error(e); + } + } + + // Compute host status + for (const key of keys) { + const cached = this.licenseKeyCache.get( + key.licenseKey + )!; + + logger.debug("Checking key", cached); + + if (cached.type === "LICENSE") { + status.isLicenseValid = cached.valid; + } + + if (!cached.valid) { + continue; + } + + if (!status.maxSites) { + status.maxSites = 0; + } + + status.maxSites += cached.numSites || 0; + } + } catch (error) { + logger.error("Error checking license status:"); + logger.error(error); + } + + this.statusCache.set(this.statusKey, status); + return status; + } + + public async activateLicenseKey(key: string) { + const [existingKey] = await db + .select() + .from(licenseKey) + .where(eq(licenseKey.licenseKeyId, key)) + .limit(1); + + if (existingKey) { + throw new Error("License key already exists"); + } + + let instanceId: string | undefined; + try { + // Call activate + const apiResponse = await fetch(this.activationServerUrl, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + licenseKey: key, + instanceName: this.hostId + }) + }); + + const data = await apiResponse.json(); + + if (!data.success) { + throw new Error(`${data.message || data.error}`); + } + + const response = data as ActivateLicenseKeyAPIResponse; + + if (!response.data) { + throw new Error("No response from server"); + } + + if (!response.data.instanceId) { + throw new Error("No instance ID in response"); + } + + instanceId = response.data.instanceId; + } catch (error) { + throw Error(`Error activating license key: ${error}`); + } + + // Phone home to validate license key + const keys = [ + { + licenseKey: key, + instanceId: instanceId! + } + ]; + + let validateResponse: ValidateLicenseAPIResponse; + try { + validateResponse = await this.phoneHome(keys); + + if (!validateResponse) { + throw new Error("No response from server"); + } + + if (!validateResponse.success) { + throw new Error(validateResponse.error); + } + + // Validate the license key + const licenseKeyRes = validateResponse.data.licenseKeys[key]; + if (!licenseKeyRes) { + throw new Error("Invalid license key"); + } + + const payload = validateJWT( + licenseKeyRes, + this.publicKey + ); + + if (!payload.valid) { + throw new Error("Invalid license key"); + } + + // Store the license key in the database + await db.insert(licenseKey).values({ + licenseKeyId: key, + token: licenseKeyRes, + instanceId: instanceId! + }); + } catch (error) { + throw Error(`Error validating license key: ${error}`); + } + + // Invalidate the cache and re-compute the status + return await this.forceRecheck(); + } + + private async phoneHome( + keys: { + licenseKey: string; + instanceId: string; + }[] + ): Promise { + const response = await fetch(this.validationServerUrl, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + licenseKeys: keys, + ephemeralKey: this.ephemeralKey, + instanceName: this.hostId + }) + }); + + const data = await response.json(); + + return data as ValidateLicenseAPIResponse; + } +} + +const [info] = await db.select().from(hostMeta).limit(1); + +if (!info) { + throw new Error("Host information not found"); +} + +export const license = new License(info.hostMetaId); + +export default license; diff --git a/server/license/licenseJwt.ts b/server/license/licenseJwt.ts new file mode 100644 index 00000000..ed7f4a0a --- /dev/null +++ b/server/license/licenseJwt.ts @@ -0,0 +1,114 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import * as crypto from "crypto"; + +/** + * Validates a JWT using a public key + * @param token - The JWT to validate + * @param publicKey - The public key used for verification (PEM format) + * @returns The decoded payload if validation succeeds, throws an error otherwise + */ +function validateJWT( + token: string, + publicKey: string +): Payload { + // Split the JWT into its three parts + const parts = token.split("."); + if (parts.length !== 3) { + throw new Error("Invalid JWT format"); + } + + const [encodedHeader, encodedPayload, signature] = parts; + + // Decode the header to get the algorithm + const header = JSON.parse(Buffer.from(encodedHeader, "base64").toString()); + const algorithm = header.alg; + + // Verify the signature + const signatureInput = `${encodedHeader}.${encodedPayload}`; + const isValid = verify(signatureInput, signature, publicKey, algorithm); + + if (!isValid) { + throw new Error("Invalid signature"); + } + + // Decode the payload + const payload = JSON.parse( + Buffer.from(encodedPayload, "base64").toString() + ); + + // Check if the token has expired + const now = Math.floor(Date.now() / 1000); + if (payload.exp && payload.exp < now) { + throw new Error("Token has expired"); + } + + return payload; +} + +/** + * Verifies the signature of a JWT + */ +function verify( + input: string, + signature: string, + publicKey: string, + algorithm: string +): boolean { + let verifyAlgorithm: string; + + // Map JWT algorithm name to Node.js crypto algorithm name + switch (algorithm) { + case "RS256": + verifyAlgorithm = "RSA-SHA256"; + break; + case "RS384": + verifyAlgorithm = "RSA-SHA384"; + break; + case "RS512": + verifyAlgorithm = "RSA-SHA512"; + break; + case "ES256": + verifyAlgorithm = "SHA256"; + break; + case "ES384": + verifyAlgorithm = "SHA384"; + break; + case "ES512": + verifyAlgorithm = "SHA512"; + break; + default: + throw new Error(`Unsupported algorithm: ${algorithm}`); + } + + // Convert base64url signature to standard base64 + const base64Signature = base64URLToBase64(signature); + + // Verify the signature + const verifier = crypto.createVerify(verifyAlgorithm); + verifier.update(input); + return verifier.verify(publicKey, base64Signature, "base64"); +} + +/** + * Converts base64url format to standard base64 + */ +function base64URLToBase64(base64url: string): string { + // Add padding if needed + let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/"); + + const pad = base64.length % 4; + if (pad) { + if (pad === 1) { + throw new Error("Invalid base64url string"); + } + base64 += "=".repeat(4 - pad); + } + + return base64; +} + +export { validateJWT }; diff --git a/server/routers/external.ts b/server/routers/external.ts index addd922b..a74e2759 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -11,6 +11,7 @@ import * as role from "./role"; import * as supporterKey from "./supporterKey"; import * as accessToken from "./accessToken"; import * as idp from "./idp"; +import * as license from "./license"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, @@ -524,6 +525,30 @@ authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp); authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); +authenticated.post( + "/license/activate", + verifyUserIsServerAdmin, + license.activateLicense +); + +authenticated.get( + "/license/keys", + verifyUserIsServerAdmin, + license.listLicenseKeys +); + +authenticated.delete( + "/license/:licenseKey", + verifyUserIsServerAdmin, + license.deleteLicenseKey +); + +authenticated.post( + "/license/recheck", + verifyUserIsServerAdmin, + license.recheckStatus +); + // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter); diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index 2591de10..910a0953 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -11,6 +11,7 @@ import { idp, idpOidcConfig } from "@server/db/schemas"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; +import license from "@server/license/license"; const paramsSchema = z.object({}).strict(); @@ -67,7 +68,7 @@ export async function createOidcIdp( ); } - const { + let { clientId, clientSecret, authUrl, @@ -80,6 +81,10 @@ export async function createOidcIdp( autoProvision } = parsedBody.data; + if (!(await license.isUnlocked())) { + autoProvision = false; + } + const key = config.getRawConfig().server.secret; const encryptedSecret = encrypt(clientSecret, key); diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts index 4eba73d2..a495adb5 100644 --- a/server/routers/idp/updateOidcIdp.ts +++ b/server/routers/idp/updateOidcIdp.ts @@ -11,6 +11,7 @@ import { idp, idpOidcConfig } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; +import license from "@server/license/license"; const paramsSchema = z .object({ @@ -84,7 +85,7 @@ export async function updateOidcIdp( } const { idpId } = parsedParams.data; - const { + let { clientId, clientSecret, authUrl, @@ -99,6 +100,10 @@ export async function updateOidcIdp( defaultOrgMapping } = parsedBody.data; + if (!(await license.isUnlocked())) { + autoProvision = false; + } + // Check if IDP exists and is of type OIDC const [existingIdp] = await db .select() diff --git a/server/routers/internal.ts b/server/routers/internal.ts index aaa955e6..eee72e9e 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -5,6 +5,7 @@ import * as resource from "./resource"; import * as badger from "./badger"; import * as auth from "@server/routers/auth"; import * as supporterKey from "@server/routers/supporterKey"; +import * as license from "@server/routers/license"; import HttpCode from "@server/types/HttpCode"; import { verifyResourceAccess, @@ -37,6 +38,11 @@ internalRouter.get( supporterKey.isSupporterKeyVisible ); +internalRouter.get( + `/license/status`, + license.getLicenseStatus +); + // Gerbil routes const gerbilRouter = Router(); internalRouter.use("/gerbil", gerbilRouter); diff --git a/server/routers/license/activateLicense.ts b/server/routers/license/activateLicense.ts new file mode 100644 index 00000000..da2b76c4 --- /dev/null +++ b/server/routers/license/activateLicense.ts @@ -0,0 +1,62 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { response as sendResponse } from "@server/lib"; +import license, { LicenseStatus } from "@server/license/license"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; + +const bodySchema = z + .object({ + licenseKey: z.string().min(1).max(255) + }) + .strict(); + +export type ActivateLicenseStatus = LicenseStatus; + +export async function activateLicense( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { licenseKey } = parsedBody.data; + + try { + const status = await license.activateLicenseKey(licenseKey); + return sendResponse(res, { + data: status, + success: true, + error: false, + message: "License key activated successfully", + status: HttpCode.OK + }); + } catch (e) { + logger.error(e); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `${e}`) + ); + } + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/license/deleteLicenseKey.ts b/server/routers/license/deleteLicenseKey.ts new file mode 100644 index 00000000..db98e78a --- /dev/null +++ b/server/routers/license/deleteLicenseKey.ts @@ -0,0 +1,76 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { response as sendResponse } from "@server/lib"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import db from "@server/db"; +import { eq } from "drizzle-orm"; +import { licenseKey } from "@server/db/schemas"; +import license, { LicenseStatus } from "@server/license/license"; + +const paramsSchema = z + .object({ + licenseKey: z.string().min(1).max(255) + }) + .strict(); + +export type DeleteLicenseKeyResponse = LicenseStatus; + +export async function deleteLicenseKey( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { licenseKey: key } = parsedParams.data; + + const [existing] = await db + .select() + .from(licenseKey) + .where(eq(licenseKey.licenseKeyId, key)) + .limit(1); + + if (!existing) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `License key ${key} not found` + ) + ); + } + + await db.delete(licenseKey).where(eq(licenseKey.licenseKeyId, key)); + + const status = await license.forceRecheck(); + + return sendResponse(res, { + data: status, + success: true, + error: false, + message: "License key deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/license/getLicenseStatus.ts b/server/routers/license/getLicenseStatus.ts new file mode 100644 index 00000000..a4e4151a --- /dev/null +++ b/server/routers/license/getLicenseStatus.ts @@ -0,0 +1,36 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { response as sendResponse } from "@server/lib"; +import license, { LicenseStatus } from "@server/license/license"; + +export type GetLicenseStatusResponse = LicenseStatus; + +export async function getLicenseStatus( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const status = await license.check(); + + return sendResponse(res, { + data: status, + success: true, + error: false, + message: "Got status", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/license/index.ts b/server/routers/license/index.ts new file mode 100644 index 00000000..6c848c2a --- /dev/null +++ b/server/routers/license/index.ts @@ -0,0 +1,10 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +export * from "./getLicenseStatus"; +export * from "./activateLicense"; +export * from "./listLicenseKeys"; +export * from "./deleteLicenseKey"; +export * from "./recheckStatus"; diff --git a/server/routers/license/listLicenseKeys.ts b/server/routers/license/listLicenseKeys.ts new file mode 100644 index 00000000..12a19564 --- /dev/null +++ b/server/routers/license/listLicenseKeys.ts @@ -0,0 +1,36 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { response as sendResponse } from "@server/lib"; +import license, { LicenseKeyCache } from "@server/license/license"; + +export type ListLicenseKeysResponse = LicenseKeyCache[]; + +export async function listLicenseKeys( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const keys = license.listKeys(); + + return sendResponse(res, { + data: keys, + success: true, + error: false, + message: "Successfully retrieved license keys", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/license/recheckStatus.ts b/server/routers/license/recheckStatus.ts new file mode 100644 index 00000000..5f0bd949 --- /dev/null +++ b/server/routers/license/recheckStatus.ts @@ -0,0 +1,42 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { response as sendResponse } from "@server/lib"; +import license, { LicenseStatus } from "@server/license/license"; + +export type RecheckStatusResponse = LicenseStatus; + +export async function recheckStatus( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + try { + const status = await license.forceRecheck(); + return sendResponse(res, { + data: status, + success: true, + error: false, + message: "License status rechecked successfully", + status: HttpCode.OK + }); + } catch (e) { + logger.error(e); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `${e}`) + ); + } + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/supporterKey/isSupporterKeyVisible.ts b/server/routers/supporterKey/isSupporterKeyVisible.ts index 3eab2ac8..15e313de 100644 --- a/server/routers/supporterKey/isSupporterKeyVisible.ts +++ b/server/routers/supporterKey/isSupporterKeyVisible.ts @@ -7,6 +7,7 @@ import config from "@server/lib/config"; import db from "@server/db"; import { count } from "drizzle-orm"; import { users } from "@server/db/schemas"; +import license from "@server/license/license"; export type IsSupporterKeyVisibleResponse = { visible: boolean; @@ -26,6 +27,12 @@ export async function isSupporterKeyVisible( let visible = !hidden && key?.valid !== true; + const licenseStatus = await license.check(); + + if (licenseStatus.isLicenseValid) { + visible = false; + } + if (key?.tier === "Limited Supporter") { const [numUsers] = await db.select({ count: count() }).from(users); diff --git a/server/setup/index.ts b/server/setup/index.ts index b93af2aa..51cb358d 100644 --- a/server/setup/index.ts +++ b/server/setup/index.ts @@ -3,9 +3,11 @@ import { copyInConfig } from "./copyInConfig"; import { setupServerAdmin } from "./setupServerAdmin"; import logger from "@server/logger"; import { clearStaleData } from "./clearStaleData"; +import { setHostMeta } from "./setHostMeta"; export async function runSetupFunctions() { try { + await setHostMeta(); await copyInConfig(); // copy in the config to the db as needed await setupServerAdmin(); await ensureActions(); // make sure all of the actions are in the db and the roles diff --git a/server/setup/setHostMeta.ts b/server/setup/setHostMeta.ts new file mode 100644 index 00000000..2a5b16a5 --- /dev/null +++ b/server/setup/setHostMeta.ts @@ -0,0 +1,17 @@ +import db from "@server/db"; +import { hostMeta } from "@server/db/schemas"; +import { v4 as uuidv4 } from "uuid"; + +export async function setHostMeta() { + const [existing] = await db.select().from(hostMeta).limit(1); + + if (existing && existing.hostMetaId) { + return; + } + + const id = uuidv4(); + + await db + .insert(hostMeta) + .values({ hostMetaId: id, createdAt: new Date().getTime() }); +} diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 7bed1349..f7844c7c 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -42,6 +42,7 @@ import { } from "@app/components/InfoSection"; import CopyToClipboard from "@app/components/CopyToClipboard"; import { Badge } from "@app/components/ui/badge"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; const GeneralFormSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), @@ -67,6 +68,7 @@ export default function GeneralPage() { const { idpId } = useParams(); const [loading, setLoading] = useState(false); const [initialLoading, setInitialLoading] = useState(true); + const { isUnlocked } = useLicenseStatusContext(); const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`; @@ -230,7 +232,7 @@ export default function GeneralPage() { defaultChecked={form.getValues( "autoProvision" )} - disabled={true} + disabled={!isUnlocked()} onCheckedChange={(checked) => { form.setValue( "autoProvision", @@ -238,12 +240,14 @@ export default function GeneralPage() { ); }} /> - - Professional - + {!isUnlocked() && ( + + Professional + + )}
When enabled, users will be diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index 193cbe4e..ebda31a1 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -4,6 +4,7 @@ import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { ProfessionalContentOverlay } from "@app/components/ProfessionalContentOverlay"; import Link from "next/link"; import { ArrowLeft } from "lucide-react"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; @@ -35,7 +36,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { redirect("/admin/idp"); } - const navItems = [ + const navItems: HorizontalTabs = [ { title: "General", href: `/admin/idp/${params.idpId}/general` diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index 3842bf76..034cc69a 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -36,6 +36,7 @@ import { InfoIcon, ExternalLink } from "lucide-react"; import { StrategySelect } from "@app/components/StrategySelect"; import { SwitchInput } from "@app/components/SwitchInput"; import { Badge } from "@app/components/ui/badge"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; const createIdpFormSchema = z.object({ name: z.string().min(2, { message: "Name must be at least 2 characters." }), @@ -74,6 +75,7 @@ export default function Page() { const api = createApiClient({ env }); const router = useRouter(); const [createLoading, setCreateLoading] = useState(false); + const { isUnlocked } = useLicenseStatusContext(); const form = useForm({ resolver: zodResolver(createIdpFormSchema), @@ -190,7 +192,7 @@ export default function Page() { defaultChecked={form.getValues( "autoProvision" )} - disabled={true} + disabled={!isUnlocked()} onCheckedChange={(checked) => { form.setValue( "autoProvision", @@ -198,12 +200,14 @@ export default function Page() { ); }} /> - - Professional - + {!isUnlocked() && ( + + Professional + + )}
When enabled, users will be diff --git a/src/app/admin/license/LicenseKeysDataTable.tsx b/src/app/admin/license/LicenseKeysDataTable.tsx new file mode 100644 index 00000000..dea3f38d --- /dev/null +++ b/src/app/admin/license/LicenseKeysDataTable.tsx @@ -0,0 +1,147 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { DataTable } from "@app/components/ui/data-table"; +import { Button } from "@app/components/ui/button"; +import { Badge } from "@app/components/ui/badge"; +import { LicenseKeyCache } from "@server/license/license"; +import { ArrowUpDown } from "lucide-react"; +import moment from "moment"; +import CopyToClipboard from "@app/components/CopyToClipboard"; + +type LicenseKeysDataTableProps = { + licenseKeys: LicenseKeyCache[]; + onDelete: (key: string) => void; + onCreate: () => void; +}; + +function obfuscateLicenseKey(key: string): string { + if (key.length <= 8) return key; + const firstPart = key.substring(0, 4); + const lastPart = key.substring(key.length - 4); + return `${firstPart}••••••••••••••••••••${lastPart}`; +} + +export function LicenseKeysDataTable({ + licenseKeys, + onDelete, + onCreate +}: LicenseKeysDataTableProps) { + const columns: ColumnDef[] = [ + { + accessorKey: "licenseKey", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const licenseKey = row.original.licenseKey; + return ( + + ); + } + }, + { + accessorKey: "valid", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return row.original.valid ? "Yes" : "No"; + } + }, + { + accessorKey: "type", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const type = row.original.type; + const label = + type === "SITES" ? "Additional Sites" : "Host License"; + const variant = type === "SITES" ? "secondary" : "default"; + return row.original.valid ? ( + {label} + ) : null; + } + }, + { + accessorKey: "numSites", + header: ({ column }) => { + return ( + + ); + } + }, + { + id: "delete", + cell: ({ row }) => ( +
+ +
+ ) + } + ]; + + return ( + + ); +} diff --git a/src/app/admin/license/components/SitePriceCalculator.tsx b/src/app/admin/license/components/SitePriceCalculator.tsx new file mode 100644 index 00000000..cee00a44 --- /dev/null +++ b/src/app/admin/license/components/SitePriceCalculator.tsx @@ -0,0 +1,131 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { useState } from "react"; +import { Button } from "@app/components/ui/button"; +import { MinusCircle, PlusCircle } from "lucide-react"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; + +type SitePriceCalculatorProps = { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + mode: "license" | "additional-sites"; +}; + +export function SitePriceCalculator({ + isOpen, + onOpenChange, + mode +}: SitePriceCalculatorProps) { + const [siteCount, setSiteCount] = useState(1); + const pricePerSite = 5; + const licenseFlatRate = 125; + + const incrementSites = () => { + setSiteCount((prev) => prev + 1); + }; + + const decrementSites = () => { + setSiteCount((prev) => (prev > 1 ? prev - 1 : 1)); + }; + + const totalCost = mode === "license" + ? licenseFlatRate + (siteCount * pricePerSite) + : siteCount * pricePerSite; + + return ( + + + + + {mode === "license" ? "Purchase License" : "Purchase Additional Sites"} + + + Choose how many sites you want to {mode === "license" ? "purchase a license for" : "add to your existing license"}. + + + +
+
+
+ Number of Sites +
+
+ + + {siteCount} + + +
+
+ +
+ {mode === "license" && ( +
+ + License fee: + + + ${licenseFlatRate.toFixed(2)} + +
+ )} +
+ + Price per site: + + + ${pricePerSite.toFixed(2)} + +
+
+ + Number of sites: + + + {siteCount} + +
+
+ Total: + ${totalCost.toFixed(2)} / mo +
+
+
+
+ + + + + + +
+
+ ); +} diff --git a/src/app/admin/license/page.tsx b/src/app/admin/license/page.tsx new file mode 100644 index 00000000..153fe7a6 --- /dev/null +++ b/src/app/admin/license/page.tsx @@ -0,0 +1,471 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { useState, useEffect } from "react"; +import { LicenseKeyCache } from "@server/license/license"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/api"; +import { LicenseKeysDataTable } from "./LicenseKeysDataTable"; +import { AxiosResponse } from "axios"; +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { useRouter } from "next/navigation"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { + SettingsContainer, + SettingsSectionTitle as SSTitle, + SettingsSection, + SettingsSectionDescription, + SettingsSectionGrid, + SettingsSectionHeader, + SettingsSectionFooter +} from "@app/components/Settings"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { Badge } from "@app/components/ui/badge"; +import { Check, ShieldCheck, ShieldOff } from "lucide-react"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { Progress } from "@app/components/ui/progress"; +import { MinusCircle, PlusCircle } from "lucide-react"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import { SitePriceCalculator } from "./components/SitePriceCalculator"; + +const formSchema = z.object({ + licenseKey: z + .string() + .nonempty({ message: "License key is required" }) + .max(255) +}); + +function obfuscateLicenseKey(key: string): string { + if (key.length <= 8) return key; + const firstPart = key.substring(0, 4); + const lastPart = key.substring(key.length - 4); + return `${firstPart}••••••••••••••••••••${lastPart}`; +} + +export default function LicensePage() { + const api = createApiClient(useEnvContext()); + const [rows, setRows] = useState([]); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selectedLicenseKey, setSelectedLicenseKey] = useState( + null + ); + const router = useRouter(); + const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext(); + const [hostLicense, setHostLicense] = useState(null); + const [isPurchaseModalOpen, setIsPurchaseModalOpen] = useState(false); + const [purchaseMode, setPurchaseMode] = useState< + "license" | "additional-sites" + >("license"); + + // Separate loading states for different actions + const [isInitialLoading, setIsInitialLoading] = useState(true); + const [isActivatingLicense, setIsActivatingLicense] = useState(false); + const [isDeletingLicense, setIsDeletingLicense] = useState(false); + const [isRecheckingLicense, setIsRecheckingLicense] = useState(false); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + licenseKey: "" + } + }); + + useEffect(() => { + async function load() { + setIsInitialLoading(true); + await loadLicenseKeys(); + setIsInitialLoading(false); + } + load(); + }, []); + + async function loadLicenseKeys() { + try { + const response = + await api.get>( + "/license/keys" + ); + const keys = response.data.data; + setRows(keys); + const hostKey = keys.find((key) => key.type === "LICENSE"); + if (hostKey) { + setHostLicense(hostKey.licenseKey); + } else { + setHostLicense(null); + } + } catch (e) { + toast({ + title: "Failed to load license keys", + description: formatAxiosError( + e, + "An error occurred loading license keys" + ) + }); + } + } + + async function deleteLicenseKey(key: string) { + try { + setIsDeletingLicense(true); + const res = await api.delete(`/license/${key}`); + if (res.data.data) { + updateLicenseStatus(res.data.data); + } + await loadLicenseKeys(); + toast({ + title: "License key deleted", + description: "The license key has been deleted" + }); + setIsDeleteModalOpen(false); + } catch (e) { + toast({ + title: "Failed to delete license key", + description: formatAxiosError( + e, + "An error occurred deleting license key" + ) + }); + } finally { + setIsDeletingLicense(false); + } + } + + async function recheck() { + try { + setIsRecheckingLicense(true); + const res = await api.post(`/license/recheck`); + if (res.data.data) { + updateLicenseStatus(res.data.data); + } + await loadLicenseKeys(); + toast({ + title: "License keys rechecked", + description: "All license keys have been rechecked" + }); + } catch (e) { + toast({ + title: "Failed to recheck license keys", + description: formatAxiosError( + e, + "An error occurred rechecking license keys" + ) + }); + } finally { + setIsRecheckingLicense(false); + } + } + + async function onSubmit(values: z.infer) { + try { + setIsActivatingLicense(true); + const res = await api.post("/license/activate", { + licenseKey: values.licenseKey + }); + if (res.data.data) { + updateLicenseStatus(res.data.data); + } + + toast({ + title: "License key activated", + description: "The license key has been successfully activated." + }); + + setIsCreateModalOpen(false); + form.reset(); + await loadLicenseKeys(); + } catch (e) { + toast({ + variant: "destructive", + title: "Failed to activate license key", + description: formatAxiosError( + e, + "An error occurred while activating the license key." + ) + }); + } finally { + setIsActivatingLicense(false); + } + } + + if (isInitialLoading) { + return null; + } + + return ( + <> + { + setIsPurchaseModalOpen(val); + }} + mode={purchaseMode} + /> + + { + setIsCreateModalOpen(val); + form.reset(); + }} + > + + + Activate License Key + + Enter a license key to activate it. + + + +
+ + ( + + License Key + + + + + + )} + /> + + +
+ + + + + + +
+
+ + {selectedLicenseKey && ( + { + setIsDeleteModalOpen(val); + setSelectedLicenseKey(null); + }} + dialog={ +
+

+ Are you sure you want to delete the license key{" "} + {obfuscateLicenseKey(selectedLicenseKey)} + ? +

+

+ + This will remove the license key and all + associated permissions. Any sites using this + license key will no longer be accessible. + +

+

+ To confirm, please type the license key below. +

+
+ } + buttonText="Confirm Delete License Key" + onConfirm={async () => deleteLicenseKey(selectedLicenseKey)} + string={selectedLicenseKey} + title="Delete License Key" + /> + )} + + + + + + + + Host License + + Manage the main license key for the host. + + +
+
+ {licenseStatus?.isLicenseValid ? ( +
+
+ + Licensed +
+
+ ) : ( +
+
+ Not Licensed +
+
+ )} +
+ {licenseStatus?.hostId && ( +
+
+ Host ID +
+ +
+ )} + {hostLicense && ( +
+
+ License Key +
+ +
+ )} +
+ + + +
+ + + Sites Usage + + View the number of sites using this license. + + +
+
+
+ {licenseStatus?.usedSites || 0}{" "} + {licenseStatus?.usedSites === 1 + ? "site" + : "sites"}{" "} + in system +
+
+ {licenseStatus?.maxSites && ( +
+
+ + {licenseStatus.usedSites || 0} of{" "} + {licenseStatus.maxSites} sites used + + + {Math.round( + ((licenseStatus.usedSites || + 0) / + licenseStatus.maxSites) * + 100 + )} + % + +
+ +
+ )} +
+ + {!licenseStatus?.isHostLicensed ? ( + <> + + + + ) : ( + + )} + +
+
+ { + setSelectedLicenseKey(key); + setIsDeleteModalOpen(true); + }} + onCreate={() => setIsCreateModalOpen(true)} + /> +
+ + ); +} diff --git a/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx b/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx index c946869b..87a7683f 100644 --- a/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx @@ -15,6 +15,7 @@ import { } from "@/components/ui/card"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2, CheckCircle2, AlertCircle } from "lucide-react"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; type ValidateOidcTokenParams = { orgId: string; @@ -33,6 +34,8 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const { licenseStatus, isLicenseViolation } = useLicenseStatusContext(); + useEffect(() => { async function validate() { setLoading(true); @@ -43,6 +46,10 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { stateCookie: props.stateCookie }); + if (isLicenseViolation()) { + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + try { const res = await api.post< AxiosResponse diff --git a/src/app/components/LicenseViolation.tsx b/src/app/components/LicenseViolation.tsx new file mode 100644 index 00000000..6e1a58be --- /dev/null +++ b/src/app/components/LicenseViolation.tsx @@ -0,0 +1,45 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; + +export default function LicenseViolation() { + const { licenseStatus } = useLicenseStatusContext(); + + if (!licenseStatus) return null; + + // Show invalid license banner + if (licenseStatus.isHostLicensed && !licenseStatus.isLicenseValid) { + return ( +
+

+ Invalid or expired license keys detected. Follow license + terms to continue using all features. +

+
+ ); + } + + // Show usage violation banner + if ( + licenseStatus.maxSites && + licenseStatus.usedSites && + licenseStatus.usedSites > licenseStatus.maxSites + ) { + return ( +
+

+ License Violation: Using {licenseStatus.usedSites} sites + exceeds your licensed limit of {licenseStatus.maxSites}{" "} + sites. Follow license terms to continue using all features. +

+
+ ); + } + + return null; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1d8deaed..e0089bc5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,25 +1,17 @@ import type { Metadata } from "next"; import "./globals.css"; -import { - Figtree, - Inter, - Red_Hat_Display, - Red_Hat_Mono, - Red_Hat_Text, - Space_Grotesk -} from "next/font/google"; +import { Inter } from "next/font/google"; import { Toaster } from "@/components/ui/toaster"; import { ThemeProvider } from "@app/providers/ThemeProvider"; import EnvProvider from "@app/providers/EnvProvider"; -import { Separator } from "@app/components/ui/separator"; import { pullEnv } from "@app/lib/pullEnv"; -import { BookOpenText, ExternalLink } from "lucide-react"; -import Image from "next/image"; import SupportStatusProvider from "@app/providers/SupporterStatusProvider"; -import { createApiClient, internal, priv } from "@app/lib/api"; +import { priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { IsSupporterKeyVisibleResponse } from "@server/routers/supporterKey"; -import SupporterMessage from "./components/SupporterMessage"; +import LicenseStatusProvider from "@app/providers/LicenseStatusProvider"; +import { GetLicenseStatusResponse } from "@server/routers/license"; +import LicenseViolation from "./components/LicenseViolation"; export const metadata: Metadata = { title: `Dashboard - Pangolin`, @@ -48,6 +40,12 @@ export default async function RootLayout({ supporterData.visible = res.data.data.visible; supporterData.tier = res.data.data.tier; + const licenseStatusRes = + await priv.get>( + "/license/status" + ); + const licenseStatus = licenseStatusRes.data.data; + return ( @@ -58,14 +56,19 @@ export default async function RootLayout({ disableTransitionOnChange > - - {/* Main content */} -
-
- {children} + + + {/* Main content */} +
+
+ + {children} +
-
- + + diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index cc550d76..c8b98453 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -7,7 +7,8 @@ import { Waypoints, Combine, Fingerprint, - KeyRound + KeyRound, + TicketCheck } from "lucide-react"; export const orgLangingNavItems: SidebarNavItem[] = [ @@ -93,5 +94,10 @@ export const adminNavItems: SidebarNavItem[] = [ title: "Identity Providers", href: "/admin/idp", icon: + }, + { + title: "License", + href: "/admin/license", + icon: } ]; diff --git a/src/components/CopyTextBox.tsx b/src/components/CopyTextBox.tsx index c8ba2049..e6009019 100644 --- a/src/components/CopyTextBox.tsx +++ b/src/components/CopyTextBox.tsx @@ -4,20 +4,26 @@ import { useState, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Copy, Check } from "lucide-react"; +type CopyTextBoxProps = { + text?: string; + displayText?: string; + wrapText?: boolean; + outline?: boolean; +}; + export default function CopyTextBox({ text = "", + displayText, wrapText = false, outline = true -}) { +}: CopyTextBoxProps) { const [isCopied, setIsCopied] = useState(false); const textRef = useRef(null); const copyToClipboard = async () => { if (textRef.current) { try { - await navigator.clipboard.writeText( - textRef.current.textContent || "" - ); + await navigator.clipboard.writeText(text); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); } catch (err) { @@ -38,7 +44,7 @@ export default function CopyTextBox({ : "overflow-x-auto" }`} > - {text} + {displayText || text}
diff --git a/src/components/ProfessionalContentOverlay.tsx b/src/components/ProfessionalContentOverlay.tsx new file mode 100644 index 00000000..cd484a2b --- /dev/null +++ b/src/components/ProfessionalContentOverlay.tsx @@ -0,0 +1,42 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { cn } from "@app/lib/cn"; + +type ProfessionalContentOverlayProps = { + children: React.ReactNode; + isProfessional?: boolean; +}; + +export function ProfessionalContentOverlay({ + children, + isProfessional = false +}: ProfessionalContentOverlayProps) { + return ( +
+ {isProfessional && ( +
+
+

+ Professional Edition Required +

+

+ This feature is only available in the Professional + Edition. +

+
+
+ )} + {children} +
+ ); +} diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index f2d828e5..7fa689f8 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -3,7 +3,7 @@ export function SettingsContainer({ children }: { children: React.ReactNode }) { } export function SettingsSection({ children }: { children: React.ReactNode }) { - return
{children}
; + return
{children}
; } export function SettingsSectionHeader({ @@ -47,7 +47,7 @@ export function SettingsSectionBody({ }: { children: React.ReactNode; }) { - return
{children}
; + return
{children}
; } export function SettingsSectionFooter({ @@ -55,7 +55,7 @@ export function SettingsSectionFooter({ }: { children: React.ReactNode; }) { - return
{children}
; + return
{children}
; } export function SettingsSectionGrid({ diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index d8f64db0..21016998 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -7,6 +7,7 @@ import { cn } from "@app/lib/cn"; import { ChevronDown, ChevronRight } from "lucide-react"; import { useUserContext } from "@app/hooks/useUserContext"; import { Badge } from "@app/components/ui/badge"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; export interface SidebarNavItem { href: string; @@ -37,6 +38,7 @@ export function SidebarNav({ const resourceId = params.resourceId as string; const userId = params.userId as string; const [expandedItems, setExpandedItems] = useState>(new Set()); + const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const { user } = useUserContext(); @@ -98,7 +100,7 @@ export function SidebarNav({ const hasChildren = item.children && item.children.length > 0; const isExpanded = expandedItems.has(hydratedHref); const indent = level * 28; // Base indent for each level - const isProfessional = item.showProfessional; + const isProfessional = item.showProfessional && !isUnlocked(); const isDisabled = disabled || isProfessional; return ( diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index da278d1f..222a234f 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -16,8 +16,8 @@ const badgeVariants = cva( destructive: "border-transparent bg-destructive text-destructive-foreground", outline: "text-foreground", - green: "border-transparent bg-green-300", - yellow: "border-transparent bg-yellow-300", + green: "border-transparent bg-green-500", + yellow: "border-transparent bg-yellow-500", red: "border-transparent bg-red-300", }, }, diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 00000000..e101715d --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,30 @@ +"use client"; + +import * as React from "react"; +import * as ProgressPrimitive from "@radix-ui/react-progress"; +import { cn } from "@app/lib/cn"; + +function Progress({ + className, + value, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { Progress }; diff --git a/src/contexts/licenseStatusContext.ts b/src/contexts/licenseStatusContext.ts new file mode 100644 index 00000000..eca63573 --- /dev/null +++ b/src/contexts/licenseStatusContext.ts @@ -0,0 +1,20 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { LicenseStatus } from "@server/license/license"; +import { createContext } from "react"; + +type LicenseStatusContextType = { + licenseStatus: LicenseStatus | null; + updateLicenseStatus: (updatedSite: LicenseStatus) => void; + isLicenseViolation: () => boolean; + isUnlocked: () => boolean; +}; + +const LicenseStatusContext = createContext< + LicenseStatusContextType | undefined +>(undefined); + +export default LicenseStatusContext; diff --git a/src/hooks/useLicenseStatusContext.ts b/src/hooks/useLicenseStatusContext.ts new file mode 100644 index 00000000..b1da3434 --- /dev/null +++ b/src/hooks/useLicenseStatusContext.ts @@ -0,0 +1,17 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import LicenseStatusContext from "@app/contexts/licenseStatusContext"; +import { useContext } from "react"; + +export function useLicenseStatusContext() { + const context = useContext(LicenseStatusContext); + if (context === undefined) { + throw new Error( + "useLicenseStatusContext must be used within an LicenseStatusProvider" + ); + } + return context; +} diff --git a/src/providers/LicenseStatusProvider.tsx b/src/providers/LicenseStatusProvider.tsx new file mode 100644 index 00000000..c3fe9684 --- /dev/null +++ b/src/providers/LicenseStatusProvider.tsx @@ -0,0 +1,72 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import LicenseStatusContext from "@app/contexts/licenseStatusContext"; +import { LicenseStatus } from "@server/license/license"; +import { useState } from "react"; + +interface ProviderProps { + children: React.ReactNode; + licenseStatus: LicenseStatus | null; +} + +export function LicenseStatusProvider({ + children, + licenseStatus +}: ProviderProps) { + const [licenseStatusState, setLicenseStatusState] = + useState(licenseStatus); + + const updateLicenseStatus = (updatedLicenseStatus: LicenseStatus) => { + setLicenseStatusState((prev) => { + return { + ...updatedLicenseStatus + }; + }); + }; + + const isUnlocked = () => { + if (licenseStatusState?.isHostLicensed) { + if (licenseStatusState?.isLicenseValid) { + return true; + } + } + return false; + }; + + const isLicenseViolation = () => { + if ( + licenseStatusState?.isHostLicensed && + !licenseStatusState?.isLicenseValid + ) { + return true; + } + if ( + licenseStatusState?.maxSites && + licenseStatusState?.usedSites && + licenseStatusState.usedSites > licenseStatusState.maxSites + ) { + return true; + } + return false; + }; + + return ( + + {children} + + ); +} + +export default LicenseStatusProvider; From 599d0a52bfd775f0c37feaf44e318c88f812b80c Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 28 Apr 2025 21:14:09 -0400 Subject: [PATCH 46/53] add api key code and oidc auto provision code --- package-lock.json | 31 +- package.json | 2 + server/auth/actions.ts | 10 +- server/db/schemas/index.ts | 1 - server/db/schemas/proSchema.ts | 17 - server/db/schemas/schema.ts | 54 ++ server/index.ts | 12 +- server/integrationApiServer.ts | 112 +++ server/lib/config.ts | 16 +- server/lib/crypto.ts | 40 +- server/license/license.ts | 99 ++- server/middlewares/index.ts | 4 + server/middlewares/integration/index.ts | 17 + .../integration/verifyAccessTokenAccess.ts | 115 ++++ .../middlewares/integration/verifyApiKey.ts | 65 ++ .../integration/verifyApiKeyApiKeyAccess.ts | 86 +++ .../integration/verifyApiKeyHasAction.ts | 61 ++ .../integration/verifyApiKeyIsRoot.ts | 44 ++ .../integration/verifyApiKeyOrgAccess.ts | 66 ++ .../integration/verifyApiKeyResourceAccess.ts | 90 +++ .../integration/verifyApiKeyRoleAccess.ts | 132 ++++ .../verifyApiKeySetResourceUsers.ts | 74 ++ .../integration/verifyApiKeySiteAccess.ts | 94 +++ .../integration/verifyApiKeyTargetAccess.ts | 117 ++++ .../integration/verifyApiKeyUserAccess.ts | 72 ++ server/middlewares/verifyApiKeyAccess.ts | 104 +++ server/middlewares/verifyValidLicense.ts | 33 + server/openApi.ts | 4 +- server/routers/apiKeys/createOrgApiKey.ts | 133 ++++ server/routers/apiKeys/createRootApiKey.ts | 105 +++ server/routers/apiKeys/deleteApiKey.ts | 81 +++ server/routers/apiKeys/deleteOrgApiKey.ts | 104 +++ server/routers/apiKeys/getApiKey.ts | 81 +++ server/routers/apiKeys/index.ts | 16 + server/routers/apiKeys/listApiKeyActions.ts | 118 ++++ server/routers/apiKeys/listOrgApiKeys.ts | 121 ++++ server/routers/apiKeys/listRootApiKeys.ts | 90 +++ server/routers/apiKeys/setApiKeyActions.ts | 141 ++++ server/routers/apiKeys/setApiKeyOrgs.ts | 122 ++++ server/routers/external.ts | 131 +++- server/routers/idp/createIdpOrgPolicy.ts | 129 ++++ server/routers/idp/createOidcIdp.ts | 2 +- server/routers/idp/deleteIdp.ts | 7 +- server/routers/idp/deleteIdpOrgPolicy.ts | 95 +++ server/routers/idp/generateOidcUrl.ts | 9 +- server/routers/idp/index.ts | 4 + server/routers/idp/listIdpOrgPolicies.ts | 121 ++++ server/routers/idp/listIdps.ts | 5 +- server/routers/idp/oidcAutoProvision.ts | 233 +++++++ server/routers/idp/updateIdpOrgPolicy.ts | 131 ++++ server/routers/idp/updateOidcIdp.ts | 2 +- server/routers/idp/validateOidcCallback.ts | 38 +- server/routers/integration.ts | 499 ++++++++++++++ server/routers/license/deleteLicenseKey.ts | 2 + server/routers/org/listUserOrgs.ts | 2 +- server/setup/index.ts | 2 - server/setup/migrations.ts | 1 + .../settings/api-keys/OrgApiKeysDataTable.tsx | 33 + .../settings/api-keys/OrgApiKeysTable.tsx | 204 ++++++ .../settings/api-keys/[apiKeyId]/layout.tsx | 62 ++ .../settings/api-keys/[apiKeyId]/page.tsx | 13 + .../api-keys/[apiKeyId]/permissions/page.tsx | 138 ++++ .../[orgId]/settings/api-keys/create/page.tsx | 412 +++++++++++ src/app/[orgId]/settings/api-keys/page.tsx | 49 ++ src/app/admin/api-keys/ApiKeysDataTable.tsx | 58 ++ src/app/admin/api-keys/ApiKeysTable.tsx | 199 ++++++ src/app/admin/api-keys/[apiKeyId]/layout.tsx | 62 ++ src/app/admin/api-keys/[apiKeyId]/page.tsx | 13 + .../api-keys/[apiKeyId]/permissions/page.tsx | 139 ++++ src/app/admin/api-keys/create/page.tsx | 402 +++++++++++ src/app/admin/api-keys/page.tsx | 46 ++ src/app/admin/idp/[idpId]/layout.tsx | 5 + .../idp/[idpId]/policies/PolicyDataTable.tsx | 33 + .../idp/[idpId]/policies/PolicyTable.tsx | 159 +++++ src/app/admin/idp/[idpId]/policies/page.tsx | 645 ++++++++++++++++++ .../admin/license/LicenseKeysDataTable.tsx | 4 +- .../components/SitePriceCalculator.tsx | 34 +- src/app/admin/license/page.tsx | 47 +- src/app/navigation.tsx | 2 +- src/components/HorizontalTabs.tsx | 3 +- src/components/PermissionsSelectBox.tsx | 238 +++++++ src/contexts/apiKeyContext.ts | 16 + src/hooks/useApikeyContext.ts | 17 + src/providers/ApiKeyProvider.tsx | 42 ++ 84 files changed, 7021 insertions(+), 151 deletions(-) delete mode 100644 server/db/schemas/proSchema.ts create mode 100644 server/integrationApiServer.ts create mode 100644 server/middlewares/integration/index.ts create mode 100644 server/middlewares/integration/verifyAccessTokenAccess.ts create mode 100644 server/middlewares/integration/verifyApiKey.ts create mode 100644 server/middlewares/integration/verifyApiKeyApiKeyAccess.ts create mode 100644 server/middlewares/integration/verifyApiKeyHasAction.ts create mode 100644 server/middlewares/integration/verifyApiKeyIsRoot.ts create mode 100644 server/middlewares/integration/verifyApiKeyOrgAccess.ts create mode 100644 server/middlewares/integration/verifyApiKeyResourceAccess.ts create mode 100644 server/middlewares/integration/verifyApiKeyRoleAccess.ts create mode 100644 server/middlewares/integration/verifyApiKeySetResourceUsers.ts create mode 100644 server/middlewares/integration/verifyApiKeySiteAccess.ts create mode 100644 server/middlewares/integration/verifyApiKeyTargetAccess.ts create mode 100644 server/middlewares/integration/verifyApiKeyUserAccess.ts create mode 100644 server/middlewares/verifyApiKeyAccess.ts create mode 100644 server/middlewares/verifyValidLicense.ts create mode 100644 server/routers/apiKeys/createOrgApiKey.ts create mode 100644 server/routers/apiKeys/createRootApiKey.ts create mode 100644 server/routers/apiKeys/deleteApiKey.ts create mode 100644 server/routers/apiKeys/deleteOrgApiKey.ts create mode 100644 server/routers/apiKeys/getApiKey.ts create mode 100644 server/routers/apiKeys/index.ts create mode 100644 server/routers/apiKeys/listApiKeyActions.ts create mode 100644 server/routers/apiKeys/listOrgApiKeys.ts create mode 100644 server/routers/apiKeys/listRootApiKeys.ts create mode 100644 server/routers/apiKeys/setApiKeyActions.ts create mode 100644 server/routers/apiKeys/setApiKeyOrgs.ts create mode 100644 server/routers/idp/createIdpOrgPolicy.ts create mode 100644 server/routers/idp/deleteIdpOrgPolicy.ts create mode 100644 server/routers/idp/listIdpOrgPolicies.ts create mode 100644 server/routers/idp/oidcAutoProvision.ts create mode 100644 server/routers/idp/updateIdpOrgPolicy.ts create mode 100644 server/routers/integration.ts create mode 100644 src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx create mode 100644 src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx create mode 100644 src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx create mode 100644 src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx create mode 100644 src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx create mode 100644 src/app/[orgId]/settings/api-keys/create/page.tsx create mode 100644 src/app/[orgId]/settings/api-keys/page.tsx create mode 100644 src/app/admin/api-keys/ApiKeysDataTable.tsx create mode 100644 src/app/admin/api-keys/ApiKeysTable.tsx create mode 100644 src/app/admin/api-keys/[apiKeyId]/layout.tsx create mode 100644 src/app/admin/api-keys/[apiKeyId]/page.tsx create mode 100644 src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx create mode 100644 src/app/admin/api-keys/create/page.tsx create mode 100644 src/app/admin/api-keys/page.tsx create mode 100644 src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx create mode 100644 src/app/admin/idp/[idpId]/policies/PolicyTable.tsx create mode 100644 src/app/admin/idp/[idpId]/policies/page.tsx create mode 100644 src/components/PermissionsSelectBox.tsx create mode 100644 src/contexts/apiKeyContext.ts create mode 100644 src/hooks/useApikeyContext.ts create mode 100644 src/providers/ApiKeyProvider.tsx diff --git a/package-lock.json b/package-lock.json index 5bb58341..4801202c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "cookie-parser": "1.4.7", "cookies": "^0.9.1", "cors": "2.8.5", + "crypto-js": "^4.2.0", "drizzle-orm": "0.38.3", "eslint": "9.17.0", "eslint-config-next": "15.1.3", @@ -94,6 +95,7 @@ "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.8", "@types/cors": "2.8.17", + "@types/crypto-js": "^4.2.2", "@types/express": "5.0.0", "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", @@ -4478,7 +4480,7 @@ "version": "7.6.12", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -4525,6 +4527,13 @@ "@types/node": "*" } }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -4619,7 +4628,7 @@ "version": "22.14.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4653,7 +4662,7 @@ "version": "19.1.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz", "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -4663,7 +4672,7 @@ "version": "19.1.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -6271,11 +6280,17 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -9584,7 +9599,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -15962,7 +15977,6 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz", "integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==", - "dev": true, "license": "MIT" }, "node_modules/tapable": { @@ -16330,7 +16344,6 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -16362,7 +16375,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unpipe": { diff --git a/package.json b/package.json index cb18a7cc..f2ce2cd4 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "cookie-parser": "1.4.7", "cookies": "^0.9.1", "cors": "2.8.5", + "crypto-js": "^4.2.0", "drizzle-orm": "0.38.3", "eslint": "9.17.0", "eslint-config-next": "15.1.3", @@ -105,6 +106,7 @@ "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.8", "@types/cors": "2.8.17", + "@types/crypto-js": "^4.2.2", "@types/express": "5.0.0", "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 251abb45..e83031a1 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -77,7 +77,15 @@ export enum ActionsEnum { createIdpOrg = "createIdpOrg", deleteIdpOrg = "deleteIdpOrg", listIdpOrgs = "listIdpOrgs", - updateIdpOrg = "updateIdpOrg" + updateIdpOrg = "updateIdpOrg", + checkOrgId = "checkOrgId", + createApiKey = "createApiKey", + deleteApiKey = "deleteApiKey", + setApiKeyActions = "setApiKeyActions", + setApiKeyOrgs = "setApiKeyOrgs", + listApiKeyActions = "listApiKeyActions", + listApiKeys = "listApiKeys", + getApiKey = "getApiKey" } export async function checkUserActionPermission( diff --git a/server/db/schemas/index.ts b/server/db/schemas/index.ts index adf6d066..686fbd9e 100644 --- a/server/db/schemas/index.ts +++ b/server/db/schemas/index.ts @@ -1,2 +1 @@ export * from "./schema"; -export * from "./proSchema"; diff --git a/server/db/schemas/proSchema.ts b/server/db/schemas/proSchema.ts deleted file mode 100644 index 6b1d879c..00000000 --- a/server/db/schemas/proSchema.ts +++ /dev/null @@ -1,17 +0,0 @@ -// This file is licensed under the Fossorial Commercial License. -// Unauthorized use, copying, modification, or distribution is strictly prohibited. -// -// Copyright (c) 2025 Fossorial LLC. All rights reserved. - -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; - -export const licenseKey = sqliteTable("licenseKey", { - licenseKeyId: text("licenseKeyId").primaryKey().notNull(), - instanceId: text("instanceId").notNull(), - token: text("token").notNull() -}); - -export const hostMeta = sqliteTable("hostMeta", { - hostMetaId: text("hostMetaId").primaryKey().notNull(), - createdAt: integer("createdAt").notNull() -}); diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index b92759b6..4fb2416b 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -458,6 +458,57 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", { scopes: text("scopes").notNull() }); +export const licenseKey = sqliteTable("licenseKey", { + licenseKeyId: text("licenseKeyId").primaryKey().notNull(), + instanceId: text("instanceId").notNull(), + token: text("token").notNull() +}); + +export const hostMeta = sqliteTable("hostMeta", { + hostMetaId: text("hostMetaId").primaryKey().notNull(), + createdAt: integer("createdAt").notNull() +}); + +export const apiKeys = sqliteTable("apiKeys", { + apiKeyId: text("apiKeyId").primaryKey(), + name: text("name").notNull(), + apiKeyHash: text("apiKeyHash").notNull(), + lastChars: text("lastChars").notNull(), + createdAt: text("dateCreated").notNull(), + isRoot: integer("isRoot", { mode: "boolean" }).notNull().default(false) +}); + +export const apiKeyActions = sqliteTable("apiKeyActions", { + apiKeyId: text("apiKeyId") + .notNull() + .references(() => apiKeys.apiKeyId, { onDelete: "cascade" }), + actionId: text("actionId") + .notNull() + .references(() => actions.actionId, { onDelete: "cascade" }) +}); + +export const apiKeyOrg = sqliteTable("apiKeyOrg", { + apiKeyId: text("apiKeyId") + .notNull() + .references(() => apiKeys.apiKeyId, { onDelete: "cascade" }), + orgId: text("orgId") + .references(() => orgs.orgId, { + onDelete: "cascade" + }) + .notNull() +}); + +export const idpOrg = sqliteTable("idpOrg", { + idpId: integer("idpId") + .notNull() + .references(() => idp.idpId, { onDelete: "cascade" }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + roleMapping: text("roleMapping"), + orgMapping: text("orgMapping") +}); + export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; @@ -494,3 +545,6 @@ export type ResourceRule = InferSelectModel; export type Domain = InferSelectModel; export type SupporterKey = InferSelectModel; export type Idp = InferSelectModel; +export type ApiKey = InferSelectModel; +export type ApiKeyAction = InferSelectModel; +export type ApiKeyOrg = InferSelectModel; diff --git a/server/index.ts b/server/index.ts index 0168535c..4c16caaa 100644 --- a/server/index.ts +++ b/server/index.ts @@ -4,7 +4,9 @@ import { runSetupFunctions } from "./setup"; import { createApiServer } from "./apiServer"; import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; -import { Session, User, UserOrg } from "./db/schemas/schema"; +import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas"; +import { createIntegrationApiServer } from "./integrationApiServer"; +import license from "./license/license.js"; async function startServers() { await runSetupFunctions(); @@ -14,10 +16,16 @@ async function startServers() { const internalServer = createInternalServer(); const nextServer = await createNextServer(); + let integrationServer; + if (await license.isUnlocked()) { + integrationServer = createIntegrationApiServer(); + } + return { apiServer, nextServer, internalServer, + integrationServer }; } @@ -25,9 +33,11 @@ async function startServers() { declare global { namespace Express { interface Request { + apiKey?: ApiKey; user?: User; session?: Session; userOrg?: UserOrg; + apiKeyOrg?: ApiKeyOrg; userOrgRoleId?: number; userOrgId?: string; userOrgIds?: string[]; diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts new file mode 100644 index 00000000..20925326 --- /dev/null +++ b/server/integrationApiServer.ts @@ -0,0 +1,112 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import express from "express"; +import cors from "cors"; +import cookieParser from "cookie-parser"; +import config from "@server/lib/config"; +import logger from "@server/logger"; +import { + errorHandlerMiddleware, + notFoundMiddleware, + verifyValidLicense +} from "@server/middlewares"; +import { authenticated, unauthenticated } from "@server/routers/integration"; +import { logIncomingMiddleware } from "./middlewares/logIncoming"; +import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; +import helmet from "helmet"; +import swaggerUi from "swagger-ui-express"; +import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi"; +import { registry } from "./openApi"; + +const dev = process.env.ENVIRONMENT !== "prod"; +const externalPort = config.getRawConfig().server.integration_port; + +export function createIntegrationApiServer() { + const apiServer = express(); + + apiServer.use(verifyValidLicense); + + if (config.getRawConfig().server.trust_proxy) { + apiServer.set("trust proxy", 1); + } + + apiServer.use(cors()); + + if (!dev) { + apiServer.use(helmet()); + apiServer.use(csrfProtectionMiddleware); + } + + apiServer.use(cookieParser()); + apiServer.use(express.json()); + + apiServer.use( + "/v1/docs", + swaggerUi.serve, + swaggerUi.setup(getOpenApiDocumentation()) + ); + + // API routes + const prefix = `/v1`; + apiServer.use(logIncomingMiddleware); + apiServer.use(prefix, unauthenticated); + apiServer.use(prefix, authenticated); + + // Error handling + apiServer.use(notFoundMiddleware); + apiServer.use(errorHandlerMiddleware); + + // Create HTTP server + const httpServer = apiServer.listen(externalPort, (err?: any) => { + if (err) throw err; + logger.info( + `Integration API server is running on http://localhost:${externalPort}` + ); + }); + + return httpServer; +} + +function getOpenApiDocumentation() { + const bearerAuth = registry.registerComponent( + "securitySchemes", + "Bearer Auth", + { + type: "http", + scheme: "bearer" + } + ); + + for (const def of registry.definitions) { + if (def.type === "route") { + def.route.security = [ + { + [bearerAuth.name]: [] + } + ]; + } + } + + registry.registerPath({ + method: "get", + path: "/", + description: "Health check", + tags: [], + request: {}, + responses: {} + }); + + const generator = new OpenApiGeneratorV3(registry.definitions); + + return generator.generateDocument({ + openapi: "3.0.0", + info: { + version: "v1", + title: "Pangolin Integration API" + }, + servers: [{ url: "/v1" }] + }); +} diff --git a/server/lib/config.ts b/server/lib/config.ts index e9675284..f2ae7afc 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -60,6 +60,10 @@ const configSchema = z.object({ } ), server: z.object({ + integration_port: portSchema + .optional() + .transform(stoi) + .pipe(portSchema.optional()), external_port: portSchema.optional().transform(stoi).pipe(portSchema), internal_port: portSchema.optional().transform(stoi).pipe(portSchema), next_port: portSchema.optional().transform(stoi).pipe(portSchema), @@ -96,14 +100,7 @@ const configSchema = z.object({ .string() .optional() .transform(getEnvOrYaml("SERVER_SECRET")) - .pipe( - z - .string() - .min( - 32, - "SERVER_SECRET must be at least 32 characters long" - ) - ) + .pipe(z.string().min(8)) }), traefik: z.object({ http_entrypoint: z.string(), @@ -267,6 +264,8 @@ export class Config { : "false"; process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url; + license.setServerSecret(parsedConfig.data.server.secret); + this.checkKeyStatus(); this.rawConfig = parsedConfig.data; @@ -274,7 +273,6 @@ export class Config { private async checkKeyStatus() { const licenseStatus = await license.check(); - console.log("License status", licenseStatus); if (!licenseStatus.isHostLicensed) { this.checkSupporterKey(); } diff --git a/server/lib/crypto.ts b/server/lib/crypto.ts index e1e9c2b1..bd7df85a 100644 --- a/server/lib/crypto.ts +++ b/server/lib/crypto.ts @@ -1,40 +1,12 @@ -import * as crypto from "crypto"; - -const ALGORITHM = "aes-256-gcm"; +import CryptoJS from "crypto-js"; export function encrypt(value: string, key: string): string { - const iv = crypto.randomBytes(12); - const keyBuffer = Buffer.from(key, "base64"); // assuming base64 input - - const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv); - - const encrypted = Buffer.concat([ - cipher.update(value, "utf8"), - cipher.final() - ]); - const authTag = cipher.getAuthTag(); - - return [ - iv.toString("base64"), - encrypted.toString("base64"), - authTag.toString("base64") - ].join(":"); + const ciphertext = CryptoJS.AES.encrypt(value, key).toString(); + return ciphertext; } export function decrypt(encryptedValue: string, key: string): string { - const [ivB64, encryptedB64, authTagB64] = encryptedValue.split(":"); - - const iv = Buffer.from(ivB64, "base64"); - const encrypted = Buffer.from(encryptedB64, "base64"); - const authTag = Buffer.from(authTagB64, "base64"); - const keyBuffer = Buffer.from(key, "base64"); - - const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv); - decipher.setAuthTag(authTag); - - const decrypted = Buffer.concat([ - decipher.update(encrypted), - decipher.final() - ]); - return decrypted.toString("utf8"); + const bytes = CryptoJS.AES.decrypt(encryptedValue, key); + const originalText = bytes.toString(CryptoJS.enc.Utf8); + return originalText; } diff --git a/server/license/license.ts b/server/license/license.ts index 68704e26..b1398d13 100644 --- a/server/license/license.ts +++ b/server/license/license.ts @@ -10,6 +10,8 @@ import NodeCache from "node-cache"; import { validateJWT } from "./licenseJwt"; import { count, eq } from "drizzle-orm"; import moment from "moment"; +import { setHostMeta } from "@server/setup/setHostMeta"; +import { encrypt, decrypt } from "@server/lib/crypto"; export type LicenseStatus = { isHostLicensed: boolean; // Are there any license keys? @@ -21,6 +23,7 @@ export type LicenseStatus = { export type LicenseKeyCache = { licenseKey: string; + licenseKeyEncrypted: string; valid: boolean; iat?: Date; type?: "LICENSE" | "SITES"; @@ -69,6 +72,7 @@ export class License { private ephemeralKey!: string; private statusKey = "status"; + private serverSecret!: string; private publicKey = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF @@ -100,6 +104,10 @@ LQIDAQAB }); } + public setServerSecret(secret: string) { + this.serverSecret = secret; + } + public async forceRecheck() { this.statusCache.flushAll(); this.licenseKeyCache.flushAll(); @@ -129,8 +137,7 @@ LQIDAQAB hostId: this.hostId, isHostLicensed: true, isLicenseValid: false, - maxSites: undefined, - usedSites: 150 + maxSites: undefined }; try { @@ -143,8 +150,6 @@ LQIDAQAB // Invalidate all this.licenseKeyCache.flushAll(); - logger.debug("Checking license status..."); - const allKeysRes = await db.select().from(licenseKey); if (allKeysRes.length === 0) { @@ -152,24 +157,37 @@ LQIDAQAB return status; } + let foundHostKey = false; // Validate stored license keys for (const key of allKeysRes) { try { - const payload = validateJWT( + // Decrypt the license key and token + const decryptedKey = decrypt( + key.licenseKeyId, + this.serverSecret + ); + const decryptedToken = decrypt( key.token, + this.serverSecret + ); + + const payload = validateJWT( + decryptedToken, this.publicKey ); - this.licenseKeyCache.set( - key.licenseKeyId, - { - licenseKey: key.licenseKeyId, - valid: payload.valid, - type: payload.type, - numSites: payload.quantity, - iat: new Date(payload.iat * 1000) - } - ); + this.licenseKeyCache.set(decryptedKey, { + licenseKey: decryptedKey, + licenseKeyEncrypted: key.licenseKeyId, + valid: payload.valid, + type: payload.type, + numSites: payload.quantity, + iat: new Date(payload.iat * 1000) + }); + + if (payload.type === "LICENSE") { + foundHostKey = true; + } } catch (e) { logger.error( `Error validating license key: ${key.licenseKeyId}` @@ -180,15 +198,21 @@ LQIDAQAB key.licenseKeyId, { licenseKey: key.licenseKeyId, + licenseKeyEncrypted: key.licenseKeyId, valid: false } ); } } + if (!foundHostKey && allKeysRes.length) { + logger.debug("No host license key found"); + status.isHostLicensed = false; + } + const keys = allKeysRes.map((key) => ({ - licenseKey: key.licenseKeyId, - instanceId: key.instanceId + licenseKey: decrypt(key.licenseKeyId, this.serverSecret), + instanceId: decrypt(key.instanceId, this.serverSecret) })); let apiResponse: ValidateLicenseAPIResponse | undefined; @@ -251,12 +275,22 @@ LQIDAQAB cached.numSites = payload.quantity; cached.iat = new Date(payload.iat * 1000); + // Encrypt the updated token before storing + const encryptedKey = encrypt( + key.licenseKey, + this.serverSecret + ); + const encryptedToken = encrypt( + licenseKeyRes, + this.serverSecret + ); + await db .update(licenseKey) .set({ - token: licenseKeyRes + token: encryptedToken }) - .where(eq(licenseKey.licenseKeyId, key.licenseKey)); + .where(eq(licenseKey.licenseKeyId, encryptedKey)); this.licenseKeyCache.set( key.licenseKey, @@ -300,10 +334,13 @@ LQIDAQAB } public async activateLicenseKey(key: string) { + // Encrypt the license key before storing + const encryptedKey = encrypt(key, this.serverSecret); + const [existingKey] = await db .select() .from(licenseKey) - .where(eq(licenseKey.licenseKeyId, key)) + .where(eq(licenseKey.licenseKeyId, encryptedKey)) .limit(1); if (existingKey) { @@ -380,11 +417,15 @@ LQIDAQAB throw new Error("Invalid license key"); } + const encryptedToken = encrypt(licenseKeyRes, this.serverSecret); + // Encrypt the instanceId before storing + const encryptedInstanceId = encrypt(instanceId!, this.serverSecret); + // Store the license key in the database await db.insert(licenseKey).values({ - licenseKeyId: key, - token: licenseKeyRes, - instanceId: instanceId! + licenseKeyId: encryptedKey, + token: encryptedToken, + instanceId: encryptedInstanceId }); } catch (error) { throw Error(`Error validating license key: ${error}`); @@ -400,13 +441,21 @@ LQIDAQAB instanceId: string; }[] ): Promise { + // Decrypt the instanceIds before sending to the server + const decryptedKeys = keys.map((key) => ({ + licenseKey: key.licenseKey, + instanceId: key.instanceId + ? decrypt(key.instanceId, this.serverSecret) + : key.instanceId + })); + const response = await fetch(this.validationServerUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - licenseKeys: keys, + licenseKeys: decryptedKeys, ephemeralKey: this.ephemeralKey, instanceName: this.hostId }) @@ -418,6 +467,8 @@ LQIDAQAB } } +await setHostMeta(); + const [info] = await db.select().from(hostMeta).limit(1); if (!info) { diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index f5a9cdc1..03d6f3bb 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -16,3 +16,7 @@ export * from "./verifyUserInRole"; export * from "./verifyAccessTokenAccess"; export * from "./verifyUserIsServerAdmin"; export * from "./verifyIsLoggedInUser"; +export * from "./integration"; +export * from "./verifyValidLicense"; +export * from "./verifyUserHasAction"; +export * from "./verifyApiKeyAccess"; diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts new file mode 100644 index 00000000..c16e1294 --- /dev/null +++ b/server/middlewares/integration/index.ts @@ -0,0 +1,17 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +export * from "./verifyApiKey"; +export * from "./verifyApiKeyOrgAccess"; +export * from "./verifyApiKeyHasAction"; +export * from "./verifyApiKeySiteAccess"; +export * from "./verifyApiKeyResourceAccess"; +export * from "./verifyApiKeyTargetAccess"; +export * from "./verifyApiKeyRoleAccess"; +export * from "./verifyApiKeyUserAccess"; +export * from "./verifyApiKeySetResourceUsers"; +export * from "./verifyAccessTokenAccess"; +export * from "./verifyApiKeyIsRoot"; +export * from "./verifyApiKeyApiKeyAccess"; diff --git a/server/middlewares/integration/verifyAccessTokenAccess.ts b/server/middlewares/integration/verifyAccessTokenAccess.ts new file mode 100644 index 00000000..82badcd4 --- /dev/null +++ b/server/middlewares/integration/verifyAccessTokenAccess.ts @@ -0,0 +1,115 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { resourceAccessToken, resources, apiKeyOrg } from "@server/db/schemas"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyAccessTokenAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const accessTokenId = req.params.accessTokenId; + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + const [accessToken] = await db + .select() + .from(resourceAccessToken) + .where(eq(resourceAccessToken.accessTokenId, accessTokenId)) + .limit(1); + + if (!accessToken) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Access token with ID ${accessTokenId} not found` + ) + ); + } + + const resourceId = accessToken.resourceId; + + if (!resourceId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Access token with ID ${accessTokenId} does not have a resource ID` + ) + ); + } + + const [resource] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + + if (!resource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found` + ) + ); + } + + if (!resource.orgId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Resource with ID ${resourceId} does not have an organization ID` + ) + ); + } + + // Verify that the API key is linked to the resource's organization + if (!req.apiKeyOrg) { + const apiKeyOrgResult = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, resource.orgId) + ) + ) + .limit(1); + + if (apiKeyOrgResult.length > 0) { + req.apiKeyOrg = apiKeyOrgResult[0]; + } + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + + return next(); + } catch (e) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying access token access" + ) + ); + } +} diff --git a/server/middlewares/integration/verifyApiKey.ts b/server/middlewares/integration/verifyApiKey.ts new file mode 100644 index 00000000..39fc3de6 --- /dev/null +++ b/server/middlewares/integration/verifyApiKey.ts @@ -0,0 +1,65 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { verifyPassword } from "@server/auth/password"; +import db from "@server/db"; +import { apiKeys } from "@server/db/schemas"; +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import { eq } from "drizzle-orm"; +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; + +export async function verifyApiKey( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const authHeader = req.headers["authorization"]; + + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "API key required") + ); + } + + const key = authHeader.split(" ")[1]; // Get the token part after "Bearer" + const [apiKeyId, apiKeySecret] = key.split("."); + + const [apiKey] = await db + .select() + .from(apiKeys) + .where(eq(apiKeys.apiKeyId, apiKeyId)) + .limit(1); + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Invalid API key") + ); + } + + const secretHash = apiKey.apiKeyHash; + const valid = await verifyPassword(apiKeySecret, secretHash); + + if (!valid) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Invalid API key") + ); + } + + req.apiKey = apiKey; + + return next(); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred checking API key" + ) + ); + } +} diff --git a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts new file mode 100644 index 00000000..aedc60c1 --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts @@ -0,0 +1,86 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { apiKeys, apiKeyOrg } from "@server/db/schemas"; +import { and, eq, or } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyApiKeyAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const {apiKey: callerApiKey } = req; + + const apiKeyId = + req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId; + const orgId = req.params.orgId; + + if (!callerApiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!orgId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") + ); + } + + if (!apiKeyId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID") + ); + } + + const [callerApiKeyOrg] = await db + .select() + .from(apiKeyOrg) + .where( + and(eq(apiKeys.apiKeyId, callerApiKey.apiKeyId), eq(apiKeyOrg.orgId, orgId)) + ) + .limit(1); + + if (!callerApiKeyOrg) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `API key with ID ${apiKeyId} does not have an organization ID` + ) + ); + } + + const [otherApiKeyOrg] = await db + .select() + .from(apiKeyOrg) + .where( + and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId)) + ) + .limit(1); + + if (!otherApiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + `API key with ID ${apiKeyId} does not have access to organization with ID ${orgId}` + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying key access" + ) + ); + } +} diff --git a/server/middlewares/integration/verifyApiKeyHasAction.ts b/server/middlewares/integration/verifyApiKeyHasAction.ts new file mode 100644 index 00000000..0326c465 --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyHasAction.ts @@ -0,0 +1,61 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import logger from "@server/logger"; +import { ActionsEnum } from "@server/auth/actions"; +import db from "@server/db"; +import { apiKeyActions } from "@server/db/schemas"; +import { and, eq } from "drizzle-orm"; + +export function verifyApiKeyHasAction(action: ActionsEnum) { + return async function ( + req: Request, + res: Response, + next: NextFunction + ): Promise { + try { + if (!req.apiKey) { + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + "API Key not authenticated" + ) + ); + } + + const [actionRes] = await db + .select() + .from(apiKeyActions) + .where( + and( + eq(apiKeyActions.apiKeyId, req.apiKey.apiKeyId), + eq(apiKeyActions.actionId, action) + ) + ); + + if (!actionRes) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have permission perform this action" + ) + ); + } + + return next(); + } catch (error) { + logger.error("Error verifying key action access:", error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying key action access" + ) + ); + } + }; +} diff --git a/server/middlewares/integration/verifyApiKeyIsRoot.ts b/server/middlewares/integration/verifyApiKeyIsRoot.ts new file mode 100644 index 00000000..35cd0faf --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyIsRoot.ts @@ -0,0 +1,44 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; + +export async function verifyApiKeyIsRoot( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const { apiKey } = req; + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!apiKey.isRoot) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have root access" + ) + ); + } + + return next(); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred checking API key" + ) + ); + } +} diff --git a/server/middlewares/integration/verifyApiKeyOrgAccess.ts b/server/middlewares/integration/verifyApiKeyOrgAccess.ts new file mode 100644 index 00000000..e1e1e0d4 --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyOrgAccess.ts @@ -0,0 +1,66 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { apiKeyOrg } from "@server/db/schemas"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import logger from "@server/logger"; + +export async function verifyApiKeyOrgAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKeyId = req.apiKey?.apiKeyId; + const orgId = req.params.orgId; + + if (!apiKeyId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!orgId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") + ); + } + + if (!req.apiKeyOrg) { + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKeyId), + eq(apiKeyOrg.orgId, orgId) + ) + ); + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + if (!req.apiKeyOrg) { + next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (e) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying organization access" + ) + ); + } +} diff --git a/server/middlewares/integration/verifyApiKeyResourceAccess.ts b/server/middlewares/integration/verifyApiKeyResourceAccess.ts new file mode 100644 index 00000000..49180b59 --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyResourceAccess.ts @@ -0,0 +1,90 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { resources, apiKeyOrg } from "@server/db/schemas"; +import { eq, and } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyResourceAccess( + req: Request, + res: Response, + next: NextFunction +) { + const apiKey = req.apiKey; + const resourceId = + req.params.resourceId || req.body.resourceId || req.query.resourceId; + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + try { + // Retrieve the resource + const [resource] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + + if (!resource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found` + ) + ); + } + + if (!resource.orgId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Resource with ID ${resourceId} does not have an organization ID` + ) + ); + } + + // Verify that the API key is linked to the resource's organization + if (!req.apiKeyOrg) { + const apiKeyOrgResult = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, resource.orgId) + ) + ) + .limit(1); + + if (apiKeyOrgResult.length > 0) { + req.apiKeyOrg = apiKeyOrgResult[0]; + } + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying resource access" + ) + ); + } +} diff --git a/server/middlewares/integration/verifyApiKeyRoleAccess.ts b/server/middlewares/integration/verifyApiKeyRoleAccess.ts new file mode 100644 index 00000000..a7abf9a6 --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyRoleAccess.ts @@ -0,0 +1,132 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { roles, apiKeyOrg } from "@server/db/schemas"; +import { and, eq, inArray } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import logger from "@server/logger"; + +export async function verifyApiKeyRoleAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const singleRoleId = parseInt( + req.params.roleId || req.body.roleId || req.query.roleId + ); + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + const { roleIds } = req.body; + const allRoleIds = + roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]); + + if (allRoleIds.length === 0) { + return next(); + } + + const rolesData = await db + .select() + .from(roles) + .where(inArray(roles.roleId, allRoleIds)); + + if (rolesData.length !== allRoleIds.length) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "One or more roles not found" + ) + ); + } + + const orgIds = new Set(rolesData.map((role) => role.orgId)); + + for (const role of rolesData) { + const apiKeyOrgAccess = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, role.orgId!) + ) + ) + .limit(1); + + if (apiKeyOrgAccess.length === 0) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + `Key does not have access to organization for role ID ${role.roleId}` + ) + ); + } + } + + if (orgIds.size > 1) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Roles must belong to the same organization" + ) + ); + } + + const orgId = orgIds.values().next().value; + + if (!orgId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Roles do not have an organization ID" + ) + ); + } + + if (!req.apiKeyOrg) { + // Retrieve the API key's organization link if not already set + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, orgId) + ) + ) + .limit(1); + + if (apiKeyOrgRes.length === 0) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + return next(); + } catch (error) { + logger.error("Error verifying role access:", error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying role access" + ) + ); + } +} diff --git a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts new file mode 100644 index 00000000..d43021ba --- /dev/null +++ b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts @@ -0,0 +1,74 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { userOrgs } from "@server/db/schemas"; +import { and, eq, inArray } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeySetResourceUsers( + req: Request, + res: Response, + next: NextFunction +) { + const apiKey = req.apiKey; + const userIds = req.body.userIds; + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + if (!userIds) { + return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs")); + } + + if (userIds.length === 0) { + return next(); + } + + try { + const orgId = req.apiKeyOrg.orgId; + const userOrgsData = await db + .select() + .from(userOrgs) + .where( + and( + inArray(userOrgs.userId, userIds), + eq(userOrgs.orgId, orgId) + ) + ); + + if (userOrgsData.length !== userIds.length) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to one or more specified users" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error checking if key has access to the specified users" + ) + ); + } +} diff --git a/server/middlewares/integration/verifyApiKeySiteAccess.ts b/server/middlewares/integration/verifyApiKeySiteAccess.ts new file mode 100644 index 00000000..7d10ddee --- /dev/null +++ b/server/middlewares/integration/verifyApiKeySiteAccess.ts @@ -0,0 +1,94 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { + sites, + apiKeyOrg +} from "@server/db/schemas"; +import { and, eq, or } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeySiteAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const siteId = parseInt( + req.params.siteId || req.body.siteId || req.query.siteId + ); + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (isNaN(siteId)) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid site ID") + ); + } + + const site = await db + .select() + .from(sites) + .where(eq(sites.siteId, siteId)) + .limit(1); + + if (site.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with ID ${siteId} not found` + ) + ); + } + + if (!site[0].orgId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Site with ID ${siteId} does not have an organization ID` + ) + ); + } + + if (!req.apiKeyOrg) { + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, site[0].orgId) + ) + ); + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying site access" + ) + ); + } +} diff --git a/server/middlewares/integration/verifyApiKeyTargetAccess.ts b/server/middlewares/integration/verifyApiKeyTargetAccess.ts new file mode 100644 index 00000000..bd6e5bc0 --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyTargetAccess.ts @@ -0,0 +1,117 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { resources, targets, apiKeyOrg } from "@server/db/schemas"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyTargetAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const targetId = parseInt(req.params.targetId); + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (isNaN(targetId)) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid target ID") + ); + } + + const [target] = await db + .select() + .from(targets) + .where(eq(targets.targetId, targetId)) + .limit(1); + + if (!target) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Target with ID ${targetId} not found` + ) + ); + } + + const resourceId = target.resourceId; + if (!resourceId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Target with ID ${targetId} does not have a resource ID` + ) + ); + } + + const [resource] = await db + .select() + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + + if (!resource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource with ID ${resourceId} not found` + ) + ); + } + + if (!resource.orgId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Resource with ID ${resourceId} does not have an organization ID` + ) + ); + } + + if (!req.apiKeyOrg) { + const apiKeyOrgResult = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, resource.orgId) + ) + ) + .limit(1); + if (apiKeyOrgResult.length > 0) { + req.apiKeyOrg = apiKeyOrgResult[0]; + } + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying target access" + ) + ); + } +} diff --git a/server/middlewares/integration/verifyApiKeyUserAccess.ts b/server/middlewares/integration/verifyApiKeyUserAccess.ts new file mode 100644 index 00000000..e1b5d3d3 --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyUserAccess.ts @@ -0,0 +1,72 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { userOrgs } from "@server/db/schemas"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyUserAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const reqUserId = + req.params.userId || req.body.userId || req.query.userId; + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!reqUserId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid user ID") + ); + } + + if (!req.apiKeyOrg || !req.apiKeyOrg.orgId) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have organization access" + ) + ); + } + + const orgId = req.apiKeyOrg.orgId; + + const [userOrgRecord] = await db + .select() + .from(userOrgs) + .where( + and(eq(userOrgs.userId, reqUserId), eq(userOrgs.orgId, orgId)) + ) + .limit(1); + + if (!userOrgRecord) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this user" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error checking if key has access to this user" + ) + ); + } +} diff --git a/server/middlewares/verifyApiKeyAccess.ts b/server/middlewares/verifyApiKeyAccess.ts new file mode 100644 index 00000000..0bba8f4b --- /dev/null +++ b/server/middlewares/verifyApiKeyAccess.ts @@ -0,0 +1,104 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { userOrgs, apiKeys, apiKeyOrg } from "@server/db/schemas"; +import { and, eq, or } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const userId = req.user!.userId; + const apiKeyId = + req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId; + const orgId = req.params.orgId; + + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } + + if (!orgId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") + ); + } + + if (!apiKeyId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID") + ); + } + + const [apiKey] = await db + .select() + .from(apiKeys) + .innerJoin(apiKeyOrg, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId)) + .where( + and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId)) + ) + .limit(1); + + if (!apiKey.apiKeys) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `API key with ID ${apiKeyId} not found` + ) + ); + } + + if (!apiKeyOrg.orgId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `API key with ID ${apiKeyId} does not have an organization ID` + ) + ); + } + + if (!req.userOrg) { + const userOrgRole = await db + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.userId, userId), + eq(userOrgs.orgId, apiKeyOrg.orgId) + ) + ) + .limit(1); + req.userOrg = userOrgRole[0]; + } + + if (!req.userOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have access to this organization" + ) + ); + } + + const userOrgRoleId = req.userOrg.roleId; + req.userOrgRoleId = userOrgRoleId; + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying key access" + ) + ); + } +} diff --git a/server/middlewares/verifyValidLicense.ts b/server/middlewares/verifyValidLicense.ts new file mode 100644 index 00000000..7f4de34a --- /dev/null +++ b/server/middlewares/verifyValidLicense.ts @@ -0,0 +1,33 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import license from "@server/license/license"; + +export async function verifyValidLicense( + req: Request, + res: Response, + next: NextFunction +) { + try { + const unlocked = await license.isUnlocked(); + if (!unlocked) { + return next( + createHttpError(HttpCode.FORBIDDEN, "License is not valid") + ); + } + + return next(); + } catch (e) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying license" + ) + ); + } +} diff --git a/server/openApi.ts b/server/openApi.ts index 43e84e56..4df6cbdd 100644 --- a/server/openApi.ts +++ b/server/openApi.ts @@ -12,5 +12,7 @@ export enum OpenAPITags { Target = "Target", Rule = "Rule", AccessToken = "Access Token", - Idp = "Identity Provider" + Idp = "Identity Provider", + Client = "Client", + ApiKey = "API Key" } diff --git a/server/routers/apiKeys/createOrgApiKey.ts b/server/routers/apiKeys/createOrgApiKey.ts new file mode 100644 index 00000000..2fb9fd20 --- /dev/null +++ b/server/routers/apiKeys/createOrgApiKey.ts @@ -0,0 +1,133 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { NextFunction, Request, Response } from "express"; +import db from "@server/db"; +import HttpCode from "@server/types/HttpCode"; +import { z } from "zod"; +import { apiKeyOrg, apiKeys } from "@server/db/schemas"; +import { fromError } from "zod-validation-error"; +import createHttpError from "http-errors"; +import response from "@server/lib/response"; +import moment from "moment"; +import { + generateId, + generateIdFromEntropySize +} from "@server/auth/sessions/app"; +import logger from "@server/logger"; +import { hashPassword } from "@server/auth/password"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z.object({ + orgId: z.string().nonempty() +}); + +const bodySchema = z.object({ + name: z.string().min(1).max(255) +}); + +export type CreateOrgApiKeyBody = z.infer; + +export type CreateOrgApiKeyResponse = { + apiKeyId: string; + name: string; + apiKey: string; + lastChars: string; + createdAt: string; +}; + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/api-key", + description: "Create a new API key scoped to the organization.", + tags: [OpenAPITags.Org, OpenAPITags.ApiKey], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function createOrgApiKey( + req: Request, + res: Response, + next: NextFunction +): Promise { + const parsedParams = paramsSchema.safeParse(req.params); + + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = bodySchema.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + const { name } = parsedBody.data; + + const apiKeyId = generateId(15); + const apiKey = generateIdFromEntropySize(25); + const apiKeyHash = await hashPassword(apiKey); + const lastChars = apiKey.slice(-4); + const createdAt = moment().toISOString(); + + await db.transaction(async (trx) => { + await trx.insert(apiKeys).values({ + name, + apiKeyId, + apiKeyHash, + createdAt, + lastChars + }); + + await trx.insert(apiKeyOrg).values({ + apiKeyId, + orgId + }); + }); + + try { + return response(res, { + data: { + apiKeyId, + apiKey, + name, + lastChars, + createdAt + }, + success: true, + error: false, + message: "API key created", + status: HttpCode.CREATED + }); + } catch (e) { + logger.error(e); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to create API key" + ) + ); + } +} diff --git a/server/routers/apiKeys/createRootApiKey.ts b/server/routers/apiKeys/createRootApiKey.ts new file mode 100644 index 00000000..775ae576 --- /dev/null +++ b/server/routers/apiKeys/createRootApiKey.ts @@ -0,0 +1,105 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { NextFunction, Request, Response } from "express"; +import db from "@server/db"; +import HttpCode from "@server/types/HttpCode"; +import { z } from "zod"; +import { apiKeyOrg, apiKeys, orgs } from "@server/db/schemas"; +import { fromError } from "zod-validation-error"; +import createHttpError from "http-errors"; +import response from "@server/lib/response"; +import moment from "moment"; +import { + generateId, + generateIdFromEntropySize +} from "@server/auth/sessions/app"; +import logger from "@server/logger"; +import { hashPassword } from "@server/auth/password"; + +const bodySchema = z + .object({ + name: z.string().min(1).max(255) + }) + .strict(); + +export type CreateRootApiKeyBody = z.infer; + +export type CreateRootApiKeyResponse = { + apiKeyId: string; + name: string; + apiKey: string; + lastChars: string; + createdAt: string; +}; + +export async function createRootApiKey( + req: Request, + res: Response, + next: NextFunction +): Promise { + const parsedBody = bodySchema.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { name } = parsedBody.data; + + const apiKeyId = generateId(15); + const apiKey = generateIdFromEntropySize(25); + const apiKeyHash = await hashPassword(apiKey); + const lastChars = apiKey.slice(-4); + const createdAt = moment().toISOString(); + + await db.transaction(async (trx) => { + await trx.insert(apiKeys).values({ + apiKeyId, + name, + apiKeyHash, + createdAt, + lastChars, + isRoot: true + }); + + const allOrgs = await trx.select().from(orgs); + + for (const org of allOrgs) { + await trx.insert(apiKeyOrg).values({ + apiKeyId, + orgId: org.orgId + }); + } + }); + + try { + return response(res, { + data: { + apiKeyId, + name, + apiKey, + lastChars, + createdAt + }, + success: true, + error: false, + message: "API key created", + status: HttpCode.CREATED + }); + } catch (e) { + logger.error(e); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to create API key" + ) + ); + } +} diff --git a/server/routers/apiKeys/deleteApiKey.ts b/server/routers/apiKeys/deleteApiKey.ts new file mode 100644 index 00000000..2af4ae23 --- /dev/null +++ b/server/routers/apiKeys/deleteApiKey.ts @@ -0,0 +1,81 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { apiKeys } from "@server/db/schemas"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z.object({ + apiKeyId: z.string().nonempty() +}); + +registry.registerPath({ + method: "delete", + path: "/org/{orgId}/api-key/{apiKeyId}", + description: "Delete an API key.", + tags: [OpenAPITags.Org, OpenAPITags.ApiKey], + request: { + params: paramsSchema + }, + responses: {} +}); + +export async function deleteApiKey( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { apiKeyId } = parsedParams.data; + + const [apiKey] = await db + .select() + .from(apiKeys) + .where(eq(apiKeys.apiKeyId, apiKeyId)) + .limit(1); + + if (!apiKey) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `API Key with ID ${apiKeyId} not found` + ) + ); + } + + await db.delete(apiKeys).where(eq(apiKeys.apiKeyId, apiKeyId)); + + return response(res, { + data: null, + success: true, + error: false, + message: "API key deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/apiKeys/deleteOrgApiKey.ts b/server/routers/apiKeys/deleteOrgApiKey.ts new file mode 100644 index 00000000..1834c82c --- /dev/null +++ b/server/routers/apiKeys/deleteOrgApiKey.ts @@ -0,0 +1,104 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { apiKeyOrg, apiKeys } from "@server/db/schemas"; +import { and, eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; + +const paramsSchema = z.object({ + apiKeyId: z.string().nonempty(), + orgId: z.string().nonempty() +}); + +export async function deleteOrgApiKey( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { apiKeyId, orgId } = parsedParams.data; + + const [apiKey] = await db + .select() + .from(apiKeys) + .where(eq(apiKeys.apiKeyId, apiKeyId)) + .innerJoin( + apiKeyOrg, + and( + eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId), + eq(apiKeyOrg.orgId, orgId) + ) + ) + .limit(1); + + if (!apiKey) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `API Key with ID ${apiKeyId} not found` + ) + ); + } + + if (apiKey.apiKeys.isRoot) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Cannot delete root API key" + ) + ); + } + + await db.transaction(async (trx) => { + await trx + .delete(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKeyId), + eq(apiKeyOrg.orgId, orgId) + ) + ); + + const apiKeyOrgs = await db + .select() + .from(apiKeyOrg) + .where(eq(apiKeyOrg.apiKeyId, apiKeyId)); + + if (apiKeyOrgs.length === 0) { + await trx.delete(apiKeys).where(eq(apiKeys.apiKeyId, apiKeyId)); + } + }); + + return response(res, { + data: null, + success: true, + error: false, + message: "API removed from organization", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/apiKeys/getApiKey.ts b/server/routers/apiKeys/getApiKey.ts new file mode 100644 index 00000000..bd495bdd --- /dev/null +++ b/server/routers/apiKeys/getApiKey.ts @@ -0,0 +1,81 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { apiKeys } from "@server/db/schemas"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; + +const paramsSchema = z.object({ + apiKeyId: z.string().nonempty() +}); + +async function query(apiKeyId: string) { + return await db + .select({ + apiKeyId: apiKeys.apiKeyId, + lastChars: apiKeys.lastChars, + createdAt: apiKeys.createdAt, + isRoot: apiKeys.isRoot, + name: apiKeys.name + }) + .from(apiKeys) + .where(eq(apiKeys.apiKeyId, apiKeyId)) + .limit(1); +} + +export type GetApiKeyResponse = NonNullable< + Awaited>[0] +>; + +export async function getApiKey( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { apiKeyId } = parsedParams.data; + + const [apiKey] = await query(apiKeyId); + + if (!apiKey) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `API Key with ID ${apiKeyId} not found` + ) + ); + } + + return response(res, { + data: apiKey, + success: true, + error: false, + message: "API key deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/apiKeys/index.ts b/server/routers/apiKeys/index.ts new file mode 100644 index 00000000..84d4ee68 --- /dev/null +++ b/server/routers/apiKeys/index.ts @@ -0,0 +1,16 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +export * from "./createRootApiKey"; +export * from "./deleteApiKey"; +export * from "./getApiKey"; +export * from "./listApiKeyActions"; +export * from "./listOrgApiKeys"; +export * from "./listApiKeyActions"; +export * from "./listRootApiKeys"; +export * from "./setApiKeyActions"; +export * from "./setApiKeyOrgs"; +export * from "./createOrgApiKey"; +export * from "./deleteOrgApiKey"; diff --git a/server/routers/apiKeys/listApiKeyActions.ts b/server/routers/apiKeys/listApiKeyActions.ts new file mode 100644 index 00000000..0cf694a0 --- /dev/null +++ b/server/routers/apiKeys/listApiKeyActions.ts @@ -0,0 +1,118 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { db } from "@server/db"; +import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db/schemas"; +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import response from "@server/lib/response"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import { eq } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z.object({ + apiKeyId: z.string().nonempty() +}); + +const querySchema = z.object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().positive()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) +}); + +function queryActions(apiKeyId: string) { + return db + .select({ + actionId: actions.actionId + }) + .from(apiKeyActions) + .where(eq(apiKeyActions.apiKeyId, apiKeyId)) + .innerJoin(actions, eq(actions.actionId, apiKeyActions.actionId)); +} + +export type ListApiKeyActionsResponse = { + actions: Awaited>; + pagination: { total: number; limit: number; offset: number }; +}; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/api-key/{apiKeyId}/actions", + description: + "List all actions set for an API key.", + tags: [OpenAPITags.Org, OpenAPITags.ApiKey], + request: { + params: paramsSchema, + query: querySchema + }, + responses: {} +}); + +export async function listApiKeyActions( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error) + ) + ); + } + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error) + ) + ); + } + + const { limit, offset } = parsedQuery.data; + const { apiKeyId } = parsedParams.data; + + const baseQuery = queryActions(apiKeyId); + + const actionsList = await baseQuery.limit(limit).offset(offset); + + return response(res, { + data: { + actions: actionsList, + pagination: { + total: actionsList.length, + limit, + offset + } + }, + success: true, + error: false, + message: "API keys retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/apiKeys/listOrgApiKeys.ts b/server/routers/apiKeys/listOrgApiKeys.ts new file mode 100644 index 00000000..a0169074 --- /dev/null +++ b/server/routers/apiKeys/listOrgApiKeys.ts @@ -0,0 +1,121 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { db } from "@server/db"; +import { apiKeyOrg, apiKeys } from "@server/db/schemas"; +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import response from "@server/lib/response"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import { eq, and } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const querySchema = z.object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().positive()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) +}); + +const paramsSchema = z.object({ + orgId: z.string() +}); + +function queryApiKeys(orgId: string) { + return db + .select({ + apiKeyId: apiKeys.apiKeyId, + orgId: apiKeyOrg.orgId, + lastChars: apiKeys.lastChars, + createdAt: apiKeys.createdAt, + name: apiKeys.name + }) + .from(apiKeyOrg) + .where(and(eq(apiKeyOrg.orgId, orgId), eq(apiKeys.isRoot, false))) + .innerJoin(apiKeys, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId)); +} + +export type ListOrgApiKeysResponse = { + apiKeys: Awaited>; + pagination: { total: number; limit: number; offset: number }; +}; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/api-keys", + description: "List all API keys for an organization", + tags: [OpenAPITags.Org, OpenAPITags.ApiKey], + request: { + params: paramsSchema, + query: querySchema + }, + responses: {} +}); + +export async function listOrgApiKeys( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error) + ) + ); + } + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error) + ) + ); + } + + const { limit, offset } = parsedQuery.data; + const { orgId } = parsedParams.data; + + const baseQuery = queryApiKeys(orgId); + + const apiKeysList = await baseQuery.limit(limit).offset(offset); + + return response(res, { + data: { + apiKeys: apiKeysList, + pagination: { + total: apiKeysList.length, + limit, + offset + } + }, + success: true, + error: false, + message: "API keys retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/apiKeys/listRootApiKeys.ts b/server/routers/apiKeys/listRootApiKeys.ts new file mode 100644 index 00000000..7feca733 --- /dev/null +++ b/server/routers/apiKeys/listRootApiKeys.ts @@ -0,0 +1,90 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { db } from "@server/db"; +import { apiKeys } from "@server/db/schemas"; +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import response from "@server/lib/response"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import { eq } from "drizzle-orm"; + +const querySchema = z.object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().positive()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) +}); + +function queryApiKeys() { + return db + .select({ + apiKeyId: apiKeys.apiKeyId, + lastChars: apiKeys.lastChars, + createdAt: apiKeys.createdAt, + name: apiKeys.name + }) + .from(apiKeys) + .where(eq(apiKeys.isRoot, true)); +} + +export type ListRootApiKeysResponse = { + apiKeys: Awaited>; + pagination: { total: number; limit: number; offset: number }; +}; + +export async function listRootApiKeys( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error) + ) + ); + } + const { limit, offset } = parsedQuery.data; + + const baseQuery = queryApiKeys(); + + const apiKeysList = await baseQuery.limit(limit).offset(offset); + + return response(res, { + data: { + apiKeys: apiKeysList, + pagination: { + total: apiKeysList.length, + limit, + offset + } + }, + success: true, + error: false, + message: "API keys retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/apiKeys/setApiKeyActions.ts b/server/routers/apiKeys/setApiKeyActions.ts new file mode 100644 index 00000000..187dd114 --- /dev/null +++ b/server/routers/apiKeys/setApiKeyActions.ts @@ -0,0 +1,141 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { actions, apiKeyActions } from "@server/db/schemas"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { eq, and, inArray } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const bodySchema = z + .object({ + actionIds: z + .array(z.string().nonempty()) + .transform((v) => Array.from(new Set(v))) + }) + .strict(); + +const paramsSchema = z.object({ + apiKeyId: z.string().nonempty() +}); + +registry.registerPath({ + method: "post", + path: "/org/{orgId}/api-key/{apiKeyId}/actions", + description: + "Set actions for an API key. This will replace any existing actions.", + tags: [OpenAPITags.Org, OpenAPITags.ApiKey], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function setApiKeyActions( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { actionIds: newActionIds } = parsedBody.data; + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { apiKeyId } = parsedParams.data; + + const actionsExist = await db + .select() + .from(actions) + .where(inArray(actions.actionId, newActionIds)); + + if (actionsExist.length !== newActionIds.length) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "One or more actions do not exist" + ) + ); + } + + await db.transaction(async (trx) => { + const existingActions = await trx + .select() + .from(apiKeyActions) + .where(eq(apiKeyActions.apiKeyId, apiKeyId)); + + const existingActionIds = existingActions.map((a) => a.actionId); + + const actionIdsToAdd = newActionIds.filter( + (id) => !existingActionIds.includes(id) + ); + const actionIdsToRemove = existingActionIds.filter( + (id) => !newActionIds.includes(id) + ); + + if (actionIdsToRemove.length > 0) { + await trx + .delete(apiKeyActions) + .where( + and( + eq(apiKeyActions.apiKeyId, apiKeyId), + inArray(apiKeyActions.actionId, actionIdsToRemove) + ) + ); + } + + if (actionIdsToAdd.length > 0) { + const insertValues = actionIdsToAdd.map((actionId) => ({ + apiKeyId, + actionId + })); + await trx.insert(apiKeyActions).values(insertValues); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "API key actions updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/apiKeys/setApiKeyOrgs.ts b/server/routers/apiKeys/setApiKeyOrgs.ts new file mode 100644 index 00000000..ee0611d3 --- /dev/null +++ b/server/routers/apiKeys/setApiKeyOrgs.ts @@ -0,0 +1,122 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { apiKeyOrg, orgs } from "@server/db/schemas"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { eq, and, inArray } from "drizzle-orm"; + +const bodySchema = z + .object({ + orgIds: z + .array(z.string().nonempty()) + .transform((v) => Array.from(new Set(v))) + }) + .strict(); + +const paramsSchema = z.object({ + apiKeyId: z.string().nonempty() +}); + +export async function setApiKeyOrgs( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { orgIds: newOrgIds } = parsedBody.data; + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { apiKeyId } = parsedParams.data; + + // make sure all orgs exist + const allOrgs = await db + .select() + .from(orgs) + .where(inArray(orgs.orgId, newOrgIds)); + + if (allOrgs.length !== newOrgIds.length) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "One or more orgs do not exist" + ) + ); + } + + await db.transaction(async (trx) => { + const existingOrgs = await trx + .select({ orgId: apiKeyOrg.orgId }) + .from(apiKeyOrg) + .where(eq(apiKeyOrg.apiKeyId, apiKeyId)); + + const existingOrgIds = existingOrgs.map((a) => a.orgId); + + const orgIdsToAdd = newOrgIds.filter( + (id) => !existingOrgIds.includes(id) + ); + const orgIdsToRemove = existingOrgIds.filter( + (id) => !newOrgIds.includes(id) + ); + + if (orgIdsToRemove.length > 0) { + await trx + .delete(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKeyId), + inArray(apiKeyOrg.orgId, orgIdsToRemove) + ) + ); + } + + if (orgIdsToAdd.length > 0) { + const insertValues = orgIdsToAdd.map((orgId) => ({ + apiKeyId, + orgId + })); + await trx.insert(apiKeyOrg).values(insertValues); + } + + return response(res, { + data: {}, + success: true, + error: false, + message: "API key orgs updated successfully", + status: HttpCode.OK + }); + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/external.ts b/server/routers/external.ts index a74e2759..d631c377 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -12,6 +12,7 @@ import * as supporterKey from "./supporterKey"; import * as accessToken from "./accessToken"; import * as idp from "./idp"; import * as license from "./license"; +import * as apiKeys from "./apiKeys"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, @@ -27,7 +28,9 @@ import { verifyUserAccess, getUserOrgs, verifyUserIsServerAdmin, - verifyIsLoggedInUser + verifyIsLoggedInUser, + verifyApiKeyAccess, + verifyValidLicense } from "@server/middlewares"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; import { ActionsEnum } from "@server/auth/actions"; @@ -522,6 +525,38 @@ authenticated.post( authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp); +authenticated.get("/idp", verifyUserIsServerAdmin, idp.listIdps); + +authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); + +authenticated.put( + "/idp/:idpId/org/:orgId", + verifyValidLicense, + verifyUserIsServerAdmin, + idp.createIdpOrgPolicy +); + +authenticated.post( + "/idp/:idpId/org/:orgId", + verifyValidLicense, + verifyUserIsServerAdmin, + idp.updateIdpOrgPolicy +); + +authenticated.delete( + "/idp/:idpId/org/:orgId", + verifyValidLicense, + verifyUserIsServerAdmin, + idp.deleteIdpOrgPolicy +); + +authenticated.get( + "/idp/:idpId/org", + verifyValidLicense, + verifyUserIsServerAdmin, + idp.listIdpOrgPolicies +); + authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp); @@ -549,6 +584,100 @@ authenticated.post( license.recheckStatus ); +authenticated.get( + `/api-key/:apiKeyId`, + verifyValidLicense, + verifyUserIsServerAdmin, + apiKeys.getApiKey +); + +authenticated.put( + `/api-key`, + verifyValidLicense, + verifyUserIsServerAdmin, + apiKeys.createRootApiKey +); + +authenticated.delete( + `/api-key/:apiKeyId`, + verifyValidLicense, + verifyUserIsServerAdmin, + apiKeys.deleteApiKey +); + +authenticated.get( + `/api-keys`, + verifyValidLicense, + verifyUserIsServerAdmin, + apiKeys.listRootApiKeys +); + +authenticated.get( + `/api-key/:apiKeyId/actions`, + verifyValidLicense, + verifyUserIsServerAdmin, + apiKeys.listApiKeyActions +); + +authenticated.post( + `/api-key/:apiKeyId/actions`, + verifyValidLicense, + verifyUserIsServerAdmin, + apiKeys.setApiKeyActions +); + +authenticated.get( + `/org/:orgId/api-keys`, + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listApiKeys), + apiKeys.listOrgApiKeys +); + +authenticated.post( + `/org/:orgId/api-key/:apiKeyId/actions`, + verifyValidLicense, + verifyOrgAccess, + verifyApiKeyAccess, + verifyUserHasAction(ActionsEnum.setApiKeyActions), + apiKeys.setApiKeyActions +); + +authenticated.get( + `/org/:orgId/api-key/:apiKeyId/actions`, + verifyValidLicense, + verifyOrgAccess, + verifyApiKeyAccess, + verifyUserHasAction(ActionsEnum.listApiKeyActions), + apiKeys.listApiKeyActions +); + +authenticated.put( + `/org/:orgId/api-key`, + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.createApiKey), + apiKeys.createOrgApiKey +); + +authenticated.delete( + `/org/:orgId/api-key/:apiKeyId`, + verifyValidLicense, + verifyOrgAccess, + verifyApiKeyAccess, + verifyUserHasAction(ActionsEnum.deleteApiKey), + apiKeys.deleteOrgApiKey +); + +authenticated.get( + `/org/:orgId/api-key/:apiKeyId`, + verifyValidLicense, + verifyOrgAccess, + verifyApiKeyAccess, + verifyUserHasAction(ActionsEnum.getApiKey), + apiKeys.getApiKey +); + // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter); diff --git a/server/routers/idp/createIdpOrgPolicy.ts b/server/routers/idp/createIdpOrgPolicy.ts new file mode 100644 index 00000000..ae5acce4 --- /dev/null +++ b/server/routers/idp/createIdpOrgPolicy.ts @@ -0,0 +1,129 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import config from "@server/lib/config"; +import { eq, and } from "drizzle-orm"; +import { idp, idpOrg } from "@server/db/schemas"; + +const paramsSchema = z + .object({ + idpId: z.coerce.number(), + orgId: z.string() + }) + .strict(); + +const bodySchema = z + .object({ + roleMapping: z.string().optional(), + orgMapping: z.string().optional() + }) + .strict(); + +export type CreateIdpOrgPolicyResponse = {}; + +registry.registerPath({ + method: "put", + path: "/idp/{idpId}/org/{orgId}", + description: "Create an IDP policy for an existing IDP on an organization.", + tags: [OpenAPITags.Idp], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function createIdpOrgPolicy( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { idpId, orgId } = parsedParams.data; + const { roleMapping, orgMapping } = parsedBody.data; + + const [existing] = await db + .select() + .from(idp) + .leftJoin( + idpOrg, + and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId)) + ) + .where(eq(idp.idpId, idpId)); + + if (!existing?.idp) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "An IDP with this ID does not exist." + ) + ); + } + + if (existing.idpOrg) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "An IDP org policy already exists." + ) + ); + } + + await db.insert(idpOrg).values({ + idpId, + orgId, + roleMapping, + orgMapping + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Idp created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index 910a0953..d663afef 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -7,7 +7,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { idp, idpOidcConfig } from "@server/db/schemas"; +import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db/schemas"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import { encrypt } from "@server/lib/crypto"; import config from "@server/lib/config"; diff --git a/server/routers/idp/deleteIdp.ts b/server/routers/idp/deleteIdp.ts index 79edd547..ac84c4f7 100644 --- a/server/routers/idp/deleteIdp.ts +++ b/server/routers/idp/deleteIdp.ts @@ -6,7 +6,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { idp, idpOidcConfig } from "@server/db/schemas"; +import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas"; import { eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; @@ -67,6 +67,11 @@ export async function deleteIdp( .delete(idpOidcConfig) .where(eq(idpOidcConfig.idpId, idpId)); + // Delete IDP-org mappings + await trx + .delete(idpOrg) + .where(eq(idpOrg.idpId, idpId)); + // Delete the IDP itself await trx .delete(idp) diff --git a/server/routers/idp/deleteIdpOrgPolicy.ts b/server/routers/idp/deleteIdpOrgPolicy.ts new file mode 100644 index 00000000..5c41c958 --- /dev/null +++ b/server/routers/idp/deleteIdpOrgPolicy.ts @@ -0,0 +1,95 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { idp, idpOrg } from "@server/db/schemas"; +import { eq, and } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z + .object({ + idpId: z.coerce.number(), + orgId: z.string() + }) + .strict(); + +registry.registerPath({ + method: "delete", + path: "/idp/{idpId}/org/{orgId}", + description: "Create an OIDC IdP for an organization.", + tags: [OpenAPITags.Idp], + request: { + params: paramsSchema + }, + responses: {} +}); + +export async function deleteIdpOrgPolicy( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { idpId, orgId } = parsedParams.data; + + const [existing] = await db + .select() + .from(idp) + .leftJoin(idpOrg, eq(idpOrg.orgId, orgId)) + .where(and(eq(idp.idpId, idpId), eq(idpOrg.orgId, orgId))); + + if (!existing.idp) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "An IDP with this ID does not exist." + ) + ); + } + + if (!existing.idpOrg) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "A policy for this IDP and org does not exist." + ) + ); + } + + await db + .delete(idpOrg) + .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId))); + + return response(res, { + data: null, + success: true, + error: false, + message: "Policy deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/idp/generateOidcUrl.ts b/server/routers/idp/generateOidcUrl.ts index 6d111451..4a62cac2 100644 --- a/server/routers/idp/generateOidcUrl.ts +++ b/server/routers/idp/generateOidcUrl.ts @@ -6,7 +6,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { idp, idpOidcConfig } from "@server/db/schemas"; +import { idp, idpOidcConfig, idpOrg } from "@server/db/schemas"; import { and, eq } from "drizzle-orm"; import * as arctic from "arctic"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; @@ -27,6 +27,10 @@ const bodySchema = z }) .strict(); +const ensureTrailingSlash = (url: string): string => { + return url.endsWith('/') ? url : `${url}/`; +}; + export type GenerateOidcUrlResponse = { redirectUrl: string; }; @@ -106,12 +110,13 @@ export async function generateOidcUrl( const codeVerifier = arctic.generateCodeVerifier(); const state = arctic.generateState(); const url = client.createAuthorizationURLWithPKCE( - existingIdp.idpOidcConfig.authUrl, + ensureTrailingSlash(existingIdp.idpOidcConfig.authUrl), state, arctic.CodeChallengeMethod.S256, codeVerifier, parsedScopes ); + logger.debug("Generated OIDC URL", { url }); const stateJwt = jsonwebtoken.sign( { diff --git a/server/routers/idp/index.ts b/server/routers/idp/index.ts index 185effde..f0dcf02e 100644 --- a/server/routers/idp/index.ts +++ b/server/routers/idp/index.ts @@ -5,3 +5,7 @@ export * from "./listIdps"; export * from "./generateOidcUrl"; export * from "./validateOidcCallback"; export * from "./getIdp"; +export * from "./createIdpOrgPolicy"; +export * from "./deleteIdpOrgPolicy"; +export * from "./listIdpOrgPolicies"; +export * from "./updateIdpOrgPolicy"; diff --git a/server/routers/idp/listIdpOrgPolicies.ts b/server/routers/idp/listIdpOrgPolicies.ts new file mode 100644 index 00000000..9ff9c97a --- /dev/null +++ b/server/routers/idp/listIdpOrgPolicies.ts @@ -0,0 +1,121 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { idpOrg } from "@server/db/schemas"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, sql } from "drizzle-orm"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z.object({ + idpId: z.coerce.number() +}); + +const querySchema = z + .object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) + }) + .strict(); + +async function query(idpId: number, limit: number, offset: number) { + const res = await db + .select() + .from(idpOrg) + .where(eq(idpOrg.idpId, idpId)) + .limit(limit) + .offset(offset); + return res; +} + +export type ListIdpOrgPoliciesResponse = { + policies: NonNullable>>; + pagination: { total: number; limit: number; offset: number }; +}; + +registry.registerPath({ + method: "get", + path: "/idp/{idpId}/org", + description: "List all org policies on an IDP.", + tags: [OpenAPITags.Idp], + request: { + params: paramsSchema, + query: querySchema + }, + responses: {} +}); + +export async function listIdpOrgPolicies( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + const { idpId } = parsedParams.data; + + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + const { limit, offset } = parsedQuery.data; + + const list = await query(idpId, limit, offset); + + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(idpOrg) + .where(eq(idpOrg.idpId, idpId)); + + return response(res, { + data: { + policies: list, + pagination: { + total: count, + limit, + offset + } + }, + success: true, + error: false, + message: "Policies retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/idp/listIdps.ts b/server/routers/idp/listIdps.ts index 76d0be87..a723ee05 100644 --- a/server/routers/idp/listIdps.ts +++ b/server/routers/idp/listIdps.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { idp } from "@server/db/schemas"; +import { domains, idp, orgDomains, users, idpOrg } from "@server/db/schemas"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -33,8 +33,10 @@ async function query(limit: number, offset: number) { idpId: idp.idpId, name: idp.name, type: idp.type, + orgCount: sql`count(${idpOrg.orgId})` }) .from(idp) + .leftJoin(idpOrg, sql`${idp.idpId} = ${idpOrg.idpId}`) .groupBy(idp.idpId) .limit(limit) .offset(offset); @@ -46,6 +48,7 @@ export type ListIdpsResponse = { idpId: number; name: string; type: string; + orgCount: number; }>; pagination: { total: number; diff --git a/server/routers/idp/oidcAutoProvision.ts b/server/routers/idp/oidcAutoProvision.ts new file mode 100644 index 00000000..7861fc41 --- /dev/null +++ b/server/routers/idp/oidcAutoProvision.ts @@ -0,0 +1,233 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { + createSession, + generateId, + generateSessionToken, + serializeSessionCookie +} from "@server/auth/sessions/app"; +import db from "@server/db"; +import { Idp, idpOrg, orgs, roles, User, userOrgs, users } from "@server/db/schemas"; +import logger from "@server/logger"; +import { UserType } from "@server/types/UserTypes"; +import { eq, and, inArray } from "drizzle-orm"; +import jmespath from "jmespath"; +import { Request, Response } from "express"; + +export async function oidcAutoProvision({ + idp, + claims, + existingUser, + userIdentifier, + email, + name, + req, + res +}: { + idp: Idp; + claims: any; + existingUser?: User; + userIdentifier: string; + email?: string; + name?: string; + req: Request; + res: Response; +}) { + const allOrgs = await db.select().from(orgs); + + const defaultRoleMapping = idp.defaultRoleMapping; + const defaultOrgMapping = idp.defaultOrgMapping; + + let userOrgInfo: { orgId: string; roleId: number }[] = []; + for (const org of allOrgs) { + const [idpOrgRes] = await db + .select() + .from(idpOrg) + .where( + and(eq(idpOrg.idpId, idp.idpId), eq(idpOrg.orgId, org.orgId)) + ); + + let roleId: number | undefined = undefined; + + const orgMapping = idpOrgRes?.orgMapping || defaultOrgMapping; + const hydratedOrgMapping = hydrateOrgMapping(orgMapping, org.orgId); + + if (hydratedOrgMapping) { + logger.debug("Hydrated Org Mapping", { + hydratedOrgMapping + }); + const orgId = jmespath.search(claims, hydratedOrgMapping); + logger.debug("Extraced Org ID", { orgId }); + if (orgId !== true && orgId !== org.orgId) { + // user not allowed to access this org + continue; + } + } + + const roleMapping = idpOrgRes?.roleMapping || defaultRoleMapping; + if (roleMapping) { + logger.debug("Role Mapping", { roleMapping }); + const roleName = jmespath.search(claims, roleMapping); + + if (!roleName) { + logger.error("Role name not found in the ID token", { + roleName + }); + continue; + } + + const [roleRes] = await db + .select() + .from(roles) + .where( + and(eq(roles.orgId, org.orgId), eq(roles.name, roleName)) + ); + + if (!roleRes) { + logger.error("Role not found", { + orgId: org.orgId, + roleName + }); + continue; + } + + roleId = roleRes.roleId; + + userOrgInfo.push({ + orgId: org.orgId, + roleId + }); + } + } + + logger.debug("User org info", { userOrgInfo }); + + let existingUserId = existingUser?.userId; + + // sync the user with the orgs and roles + await db.transaction(async (trx) => { + let userId = existingUser?.userId; + + // create user if not exists + if (!existingUser) { + userId = generateId(15); + + await trx.insert(users).values({ + userId, + username: userIdentifier, + email: email || null, + name: name || null, + type: UserType.OIDC, + idpId: idp.idpId, + emailVerified: true, // OIDC users are always verified + dateCreated: new Date().toISOString() + }); + } else { + // set the name and email + await trx + .update(users) + .set({ + username: userIdentifier, + email: email || null, + name: name || null + }) + .where(eq(users.userId, userId!)); + } + + existingUserId = userId; + + // get all current user orgs + const currentUserOrgs = await trx + .select() + .from(userOrgs) + .where(eq(userOrgs.userId, userId!)); + + // Delete orgs that are no longer valid + const orgsToDelete = currentUserOrgs.filter( + (currentOrg) => + !userOrgInfo.some((newOrg) => newOrg.orgId === currentOrg.orgId) + ); + + if (orgsToDelete.length > 0) { + await trx.delete(userOrgs).where( + and( + eq(userOrgs.userId, userId!), + inArray( + userOrgs.orgId, + orgsToDelete.map((org) => org.orgId) + ) + ) + ); + } + + // Update roles for existing orgs where the role has changed + const orgsToUpdate = currentUserOrgs.filter((currentOrg) => { + const newOrg = userOrgInfo.find( + (newOrg) => newOrg.orgId === currentOrg.orgId + ); + return newOrg && newOrg.roleId !== currentOrg.roleId; + }); + + if (orgsToUpdate.length > 0) { + for (const org of orgsToUpdate) { + const newRole = userOrgInfo.find( + (newOrg) => newOrg.orgId === org.orgId + ); + if (newRole) { + await trx + .update(userOrgs) + .set({ roleId: newRole.roleId }) + .where( + and( + eq(userOrgs.userId, userId!), + eq(userOrgs.orgId, org.orgId) + ) + ); + } + } + } + + // Add new orgs that don't exist yet + const orgsToAdd = userOrgInfo.filter( + (newOrg) => + !currentUserOrgs.some( + (currentOrg) => currentOrg.orgId === newOrg.orgId + ) + ); + + if (orgsToAdd.length > 0) { + await trx.insert(userOrgs).values( + orgsToAdd.map((org) => ({ + userId: userId!, + orgId: org.orgId, + roleId: org.roleId, + dateCreated: new Date().toISOString() + })) + ); + } + }); + + const token = generateSessionToken(); + const sess = await createSession(token, existingUserId!); + const isSecure = req.protocol === "https"; + const cookie = serializeSessionCookie( + token, + isSecure, + new Date(sess.expiresAt) + ); + + res.appendHeader("Set-Cookie", cookie); +} + +function hydrateOrgMapping( + orgMapping: string | null, + orgId: string +): string | undefined { + if (!orgMapping) { + return undefined; + } + return orgMapping.split("{{orgId}}").join(orgId); +} diff --git a/server/routers/idp/updateIdpOrgPolicy.ts b/server/routers/idp/updateIdpOrgPolicy.ts new file mode 100644 index 00000000..6f8580ac --- /dev/null +++ b/server/routers/idp/updateIdpOrgPolicy.ts @@ -0,0 +1,131 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { eq, and } from "drizzle-orm"; +import { idp, idpOrg } from "@server/db/schemas"; + +const paramsSchema = z + .object({ + idpId: z.coerce.number(), + orgId: z.string() + }) + .strict(); + +const bodySchema = z + .object({ + roleMapping: z.string().optional(), + orgMapping: z.string().optional() + }) + .strict(); + +export type UpdateIdpOrgPolicyResponse = {}; + +registry.registerPath({ + method: "post", + path: "/idp/{idpId}/org/{orgId}", + description: "Update an IDP org policy.", + tags: [OpenAPITags.Idp], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: {} +}); + +export async function updateIdpOrgPolicy( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { idpId, orgId } = parsedParams.data; + const { roleMapping, orgMapping } = parsedBody.data; + + // Check if IDP and policy exist + const [existing] = await db + .select() + .from(idp) + .leftJoin( + idpOrg, + and(eq(idpOrg.orgId, orgId), eq(idpOrg.idpId, idpId)) + ) + .where(eq(idp.idpId, idpId)); + + if (!existing?.idp) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "An IDP with this ID does not exist." + ) + ); + } + + if (!existing.idpOrg) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "A policy for this IDP and org does not exist." + ) + ); + } + + // Update the policy + await db + .update(idpOrg) + .set({ + roleMapping, + orgMapping + }) + .where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId))); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Policy updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts index a495adb5..d24e319e 100644 --- a/server/routers/idp/updateOidcIdp.ts +++ b/server/routers/idp/updateOidcIdp.ts @@ -42,7 +42,7 @@ export type UpdateIdpResponse = { registry.registerPath({ method: "post", - path: "/idp/:idpId/oidc", + path: "/idp/{idpId}/oidc", description: "Update an OIDC IdP.", tags: [OpenAPITags.Idp], request: { diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index 006c14a4..7f4ff784 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -1,24 +1,30 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; +import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { idp, idpOidcConfig, users } from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; +import { and, eq, inArray } from "drizzle-orm"; import * as arctic from "arctic"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import jmespath from "jmespath"; import jsonwebtoken from "jsonwebtoken"; import config from "@server/lib/config"; -import { decrypt } from "@server/lib/crypto"; import { createSession, generateSessionToken, serializeSessionCookie } from "@server/auth/sessions/app"; -import { response } from "@server/lib"; +import { decrypt } from "@server/lib/crypto"; +import { oidcAutoProvision } from "./oidcAutoProvision"; +import license from "@server/license/license"; + +const ensureTrailingSlash = (url: string): string => { + return url.endsWith('/') ? url : `${url}/`; +}; const paramsSchema = z .object({ @@ -148,7 +154,7 @@ export async function validateOidcCallback( } const tokens = await client.validateAuthorizationCode( - existingIdp.idpOidcConfig.tokenUrl, + ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl), code, codeVerifier ); @@ -204,12 +210,24 @@ export async function validateOidcCallback( ); if (existingIdp.idp.autoProvision) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Auto provisioning is not supported" - ) - ); + if (!(await license.isUnlocked())) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Auto-provisioning is not available" + ) + ); + } + await oidcAutoProvision({ + idp: existingIdp.idp, + userIdentifier, + email, + name, + claims, + existingUser, + req, + res + }); } else { if (!existingUser) { return next( diff --git a/server/routers/integration.ts b/server/routers/integration.ts new file mode 100644 index 00000000..40ab9aa9 --- /dev/null +++ b/server/routers/integration.ts @@ -0,0 +1,499 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import * as site from "./site"; +import * as org from "./org"; +import * as resource from "./resource"; +import * as domain from "./domain"; +import * as target from "./target"; +import * as user from "./user"; +import * as role from "./role"; +// import * as client from "./client"; +import * as accessToken from "./accessToken"; +import * as apiKeys from "./apiKeys"; +import * as idp from "./idp"; +import { + verifyApiKey, + verifyApiKeyOrgAccess, + verifyApiKeyHasAction, + verifyApiKeySiteAccess, + verifyApiKeyResourceAccess, + verifyApiKeyTargetAccess, + verifyApiKeyRoleAccess, + verifyApiKeyUserAccess, + verifyApiKeySetResourceUsers, + verifyApiKeyAccessTokenAccess, + verifyApiKeyIsRoot +} from "@server/middlewares"; +import HttpCode from "@server/types/HttpCode"; +import { Router } from "express"; +import { ActionsEnum } from "@server/auth/actions"; + +export const unauthenticated = Router(); + +unauthenticated.get("/", (_, res) => { + res.status(HttpCode.OK).json({ message: "Healthy" }); +}); + +export const authenticated = Router(); +authenticated.use(verifyApiKey); + +authenticated.get( + "/org/checkId", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.checkOrgId), + org.checkId +); + +authenticated.put( + "/org", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.createOrg), + org.createOrg +); + +authenticated.get( + "/orgs", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.listOrgs), + org.listOrgs +); // TODO we need to check the orgs here + +authenticated.get( + "/org/:orgId", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.getOrg), + org.getOrg +); + +authenticated.post( + "/org/:orgId", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.updateOrg), + org.updateOrg +); + +authenticated.delete( + "/org/:orgId", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.deleteOrg), + org.deleteOrg +); + +authenticated.put( + "/org/:orgId/site", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createSite), + site.createSite +); + +authenticated.get( + "/org/:orgId/sites", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listSites), + site.listSites +); + +authenticated.get( + "/org/:orgId/site/:niceId", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.getSite), + site.getSite +); + +authenticated.get( + "/org/:orgId/pick-site-defaults", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createSite), + site.pickSiteDefaults +); + +authenticated.get( + "/site/:siteId", + verifyApiKeySiteAccess, + verifyApiKeyHasAction(ActionsEnum.getSite), + site.getSite +); + +authenticated.post( + "/site/:siteId", + verifyApiKeySiteAccess, + verifyApiKeyHasAction(ActionsEnum.updateSite), + site.updateSite +); + +authenticated.delete( + "/site/:siteId", + verifyApiKeySiteAccess, + verifyApiKeyHasAction(ActionsEnum.deleteSite), + site.deleteSite +); + +authenticated.put( + "/org/:orgId/site/:siteId/resource", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createResource), + resource.createResource +); + +authenticated.get( + "/site/:siteId/resources", + verifyApiKeySiteAccess, + verifyApiKeyHasAction(ActionsEnum.listResources), + resource.listResources +); + +authenticated.get( + "/org/:orgId/resources", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listResources), + resource.listResources +); + +authenticated.get( + "/org/:orgId/domains", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listOrgDomains), + domain.listDomains +); + +authenticated.post( + "/org/:orgId/create-invite", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.inviteUser), + user.inviteUser +); + +authenticated.get( + "/resource/:resourceId/roles", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.listResourceRoles), + resource.listResourceRoles +); + +authenticated.get( + "/resource/:resourceId/users", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.listResourceUsers), + resource.listResourceUsers +); + +authenticated.get( + "/resource/:resourceId", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.getResource), + resource.getResource +); + +authenticated.post( + "/resource/:resourceId", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.updateResource), + resource.updateResource +); + +authenticated.delete( + "/resource/:resourceId", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.deleteResource), + resource.deleteResource +); + +authenticated.put( + "/resource/:resourceId/target", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.createTarget), + target.createTarget +); + +authenticated.get( + "/resource/:resourceId/targets", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.listTargets), + target.listTargets +); + +authenticated.put( + "/resource/:resourceId/rule", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.createResourceRule), + resource.createResourceRule +); + +authenticated.get( + "/resource/:resourceId/rules", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.listResourceRules), + resource.listResourceRules +); + +authenticated.post( + "/resource/:resourceId/rule/:ruleId", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.updateResourceRule), + resource.updateResourceRule +); + +authenticated.delete( + "/resource/:resourceId/rule/:ruleId", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.deleteResourceRule), + resource.deleteResourceRule +); + +authenticated.get( + "/target/:targetId", + verifyApiKeyTargetAccess, + verifyApiKeyHasAction(ActionsEnum.getTarget), + target.getTarget +); + +authenticated.post( + "/target/:targetId", + verifyApiKeyTargetAccess, + verifyApiKeyHasAction(ActionsEnum.updateTarget), + target.updateTarget +); + +authenticated.delete( + "/target/:targetId", + verifyApiKeyTargetAccess, + verifyApiKeyHasAction(ActionsEnum.deleteTarget), + target.deleteTarget +); + +authenticated.put( + "/org/:orgId/role", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createRole), + role.createRole +); + +authenticated.get( + "/org/:orgId/roles", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listRoles), + role.listRoles +); + +authenticated.delete( + "/role/:roleId", + verifyApiKeyRoleAccess, + verifyApiKeyHasAction(ActionsEnum.deleteRole), + role.deleteRole +); + +authenticated.post( + "/role/:roleId/add/:userId", + verifyApiKeyRoleAccess, + verifyApiKeyUserAccess, + verifyApiKeyHasAction(ActionsEnum.addUserRole), + user.addUserRole +); + +authenticated.post( + "/resource/:resourceId/roles", + verifyApiKeyResourceAccess, + verifyApiKeyRoleAccess, + verifyApiKeyHasAction(ActionsEnum.setResourceRoles), + resource.setResourceRoles +); + +authenticated.post( + "/resource/:resourceId/users", + verifyApiKeyResourceAccess, + verifyApiKeySetResourceUsers, + verifyApiKeyHasAction(ActionsEnum.setResourceUsers), + resource.setResourceUsers +); + +authenticated.post( + `/resource/:resourceId/password`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.setResourcePassword), + resource.setResourcePassword +); + +authenticated.post( + `/resource/:resourceId/pincode`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.setResourcePincode), + resource.setResourcePincode +); + +authenticated.post( + `/resource/:resourceId/whitelist`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist), + resource.setResourceWhitelist +); + +authenticated.get( + `/resource/:resourceId/whitelist`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.getResourceWhitelist), + resource.getResourceWhitelist +); + +authenticated.post( + `/resource/:resourceId/transfer`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.updateResource), + resource.transferResource +); + +authenticated.post( + `/resource/:resourceId/access-token`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.generateAccessToken), + accessToken.generateAccessToken +); + +authenticated.delete( + `/access-token/:accessTokenId`, + verifyApiKeyAccessTokenAccess, + verifyApiKeyHasAction(ActionsEnum.deleteAcessToken), + accessToken.deleteAccessToken +); + +authenticated.get( + `/org/:orgId/access-tokens`, + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listAccessTokens), + accessToken.listAccessTokens +); + +authenticated.get( + `/resource/:resourceId/access-tokens`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.listAccessTokens), + accessToken.listAccessTokens +); + +authenticated.get( + "/org/:orgId/user/:userId", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.getOrgUser), + user.getOrgUser +); + +authenticated.get( + "/org/:orgId/users", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listUsers), + user.listUsers +); + +authenticated.delete( + "/org/:orgId/user/:userId", + verifyApiKeyOrgAccess, + verifyApiKeyUserAccess, + verifyApiKeyHasAction(ActionsEnum.removeUser), + user.removeUserOrg +); + +// authenticated.put( +// "/newt", +// verifyApiKeyHasAction(ActionsEnum.createNewt), +// newt.createNewt +// ); + +authenticated.get( + `/org/:orgId/api-keys`, + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.listApiKeys), + apiKeys.listOrgApiKeys +); + +authenticated.post( + `/org/:orgId/api-key/:apiKeyId/actions`, + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.setApiKeyActions), + apiKeys.setApiKeyActions +); + +authenticated.get( + `/org/:orgId/api-key/:apiKeyId/actions`, + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.listApiKeyActions), + apiKeys.listApiKeyActions +); + +authenticated.put( + `/org/:orgId/api-key`, + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.createApiKey), + apiKeys.createOrgApiKey +); + +authenticated.delete( + `/org/:orgId/api-key/:apiKeyId`, + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.deleteApiKey), + apiKeys.deleteApiKey +); + +authenticated.put( + "/idp/oidc", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.createIdp), + idp.createOidcIdp +); + +authenticated.post( + "/idp/:idpId/oidc", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.updateIdp), + idp.updateOidcIdp +); + +authenticated.delete( + "/idp/:idpId", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.deleteIdp), + idp.deleteIdp +); + +authenticated.get( + "/idp", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.listIdps), + idp.listIdps +); + +authenticated.get( + "/idp/:idpId", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.getIdp), + idp.getIdp +); + +authenticated.put( + "/idp/:idpId/org/:orgId", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.createIdpOrg), + idp.createIdpOrgPolicy +); + +authenticated.post( + "/idp/:idpId/org/:orgId", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.updateIdpOrg), + idp.updateIdpOrgPolicy +); + +authenticated.delete( + "/idp/:idpId/org/:orgId", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.deleteIdpOrg), + idp.deleteIdpOrgPolicy +); + +authenticated.get( + "/idp/:idpId/org", + verifyApiKeyIsRoot, + verifyApiKeyHasAction(ActionsEnum.listIdpOrgs), + idp.listIdpOrgPolicies +); diff --git a/server/routers/license/deleteLicenseKey.ts b/server/routers/license/deleteLicenseKey.ts index db98e78a..bea7f9ad 100644 --- a/server/routers/license/deleteLicenseKey.ts +++ b/server/routers/license/deleteLicenseKey.ts @@ -14,6 +14,8 @@ import db from "@server/db"; import { eq } from "drizzle-orm"; import { licenseKey } from "@server/db/schemas"; import license, { LicenseStatus } from "@server/license/license"; +import { encrypt } from "@server/lib/crypto"; +import config from "@server/lib/config"; const paramsSchema = z .object({ diff --git a/server/routers/org/listUserOrgs.ts b/server/routers/org/listUserOrgs.ts index 64d0871f..43650480 100644 --- a/server/routers/org/listUserOrgs.ts +++ b/server/routers/org/listUserOrgs.ts @@ -31,7 +31,7 @@ const listOrgsSchema = z.object({ registry.registerPath({ method: "get", - path: "/user/:userId/orgs", + path: "/user/{userId}/orgs", description: "List all organizations for a user.", tags: [OpenAPITags.Org, OpenAPITags.User], request: { diff --git a/server/setup/index.ts b/server/setup/index.ts index 51cb358d..b93af2aa 100644 --- a/server/setup/index.ts +++ b/server/setup/index.ts @@ -3,11 +3,9 @@ import { copyInConfig } from "./copyInConfig"; import { setupServerAdmin } from "./setupServerAdmin"; import logger from "@server/logger"; import { clearStaleData } from "./clearStaleData"; -import { setHostMeta } from "./setHostMeta"; export async function runSetupFunctions() { try { - await setHostMeta(); await copyInConfig(); // copy in the config to the db as needed await setupServerAdmin(); await ensureActions(); // make sure all of the actions are in the db and the roles diff --git a/server/setup/migrations.ts b/server/setup/migrations.ts index dbeaeea2..753ed6a7 100644 --- a/server/setup/migrations.ts +++ b/server/setup/migrations.ts @@ -20,6 +20,7 @@ import m16 from "./scripts/1.0.0"; import m17 from "./scripts/1.1.0"; import m18 from "./scripts/1.2.0"; import m19 from "./scripts/1.3.0"; +import { setHostMeta } from "./setHostMeta"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA diff --git a/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx b/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx new file mode 100644 index 00000000..69fe7176 --- /dev/null +++ b/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx @@ -0,0 +1,33 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { DataTable } from "@app/components/ui/data-table"; +import { ColumnDef } from "@tanstack/react-table"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + addApiKey?: () => void; +} + +export function OrgApiKeysDataTable({ + addApiKey, + columns, + data +}: DataTableProps) { + return ( + + ); +} diff --git a/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx b/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx new file mode 100644 index 00000000..89e47842 --- /dev/null +++ b/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx @@ -0,0 +1,204 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { OrgApiKeysDataTable } from "./OrgApiKeysDataTable"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; +import { Button } from "@app/components/ui/button"; +import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import { toast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import moment from "moment"; + +export type OrgApiKeyRow = { + id: string; + key: string; + name: string; + createdAt: string; +}; + +type OrgApiKeyTableProps = { + apiKeys: OrgApiKeyRow[]; + orgId: string; +}; + +export default function OrgApiKeysTable({ + apiKeys, + orgId +}: OrgApiKeyTableProps) { + const router = useRouter(); + + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selected, setSelected] = useState(null); + const [rows, setRows] = useState(apiKeys); + + const api = createApiClient(useEnvContext()); + + const deleteSite = (apiKeyId: string) => { + api.delete(`/org/${orgId}/api-key/${apiKeyId}`) + .catch((e) => { + console.error("Error deleting API key", e); + toast({ + variant: "destructive", + title: "Error deleting API key", + description: formatAxiosError(e, "Error deleting API key") + }); + }) + .then(() => { + router.refresh(); + setIsDeleteModalOpen(false); + + const newRows = rows.filter((row) => row.id !== apiKeyId); + + setRows(newRows); + }); + }; + + const columns: ColumnDef[] = [ + { + id: "dots", + cell: ({ row }) => { + const apiKeyROw = row.original; + const router = useRouter(); + + return ( + + + + + + { + setSelected(apiKeyROw); + }} + > + View settings + + { + setSelected(apiKeyROw); + setIsDeleteModalOpen(true); + }} + > + Delete + + + + ); + } + }, + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "key", + header: "Key", + cell: ({ row }) => { + const r = row.original; + return {r.key}; + } + }, + { + accessorKey: "createdAt", + header: "Created At", + cell: ({ row }) => { + const r = row.original; + return {moment(r.createdAt).format("lll")} ; + } + }, + { + id: "actions", + cell: ({ row }) => { + const r = row.original; + return ( +
+ + + +
+ ); + } + } + ]; + + return ( + <> + {selected && ( + { + setIsDeleteModalOpen(val); + setSelected(null); + }} + dialog={ +
+

+ Are you sure you want to remove the API key{" "} + {selected?.name || selected?.id} from the + organization? +

+ +

+ + Once removed, the API key will no longer be + able to be used. + +

+ +

+ To confirm, please type the name of the API key + below. +

+
+ } + buttonText="Confirm Delete API Key" + onConfirm={async () => deleteSite(selected!.id)} + string={selected.name} + title="Delete API Key" + /> + )} + + { + router.push(`/${orgId}/settings/api-keys/create`); + }} + /> + + ); +} diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx new file mode 100644 index 00000000..a4c13c9a --- /dev/null +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx @@ -0,0 +1,62 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { internal } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { redirect } from "next/navigation"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { SidebarSettings } from "@app/components/SidebarSettings"; +import Link from "next/link"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from "@app/components/ui/breadcrumb"; +import { GetApiKeyResponse } from "@server/routers/apiKeys"; +import ApiKeyProvider from "@app/providers/ApiKeyProvider"; +import { HorizontalTabs } from "@app/components/HorizontalTabs"; + +interface SettingsLayoutProps { + children: React.ReactNode; + params: Promise<{ apiKeyId: string; orgId: string }>; +} + +export default async function SettingsLayout(props: SettingsLayoutProps) { + const params = await props.params; + + const { children } = props; + + let apiKey = null; + try { + const res = await internal.get>( + `/org/${params.orgId}/api-key/${params.apiKeyId}`, + await authCookieHeader() + ); + apiKey = res.data.data; + } catch (e) { + console.log(e); + redirect(`/${params.orgId}/settings/api-keys`); + } + + const navItems = [ + { + title: "Permissions", + href: "/{orgId}/settings/api-keys/{apiKeyId}/permissions" + } + ]; + + return ( + <> + + + + {children} + + + ); +} diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx new file mode 100644 index 00000000..7df37cd6 --- /dev/null +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx @@ -0,0 +1,13 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { redirect } from "next/navigation"; + +export default async function ApiKeysPage(props: { + params: Promise<{ orgId: string; apiKeyId: string }>; +}) { + const params = await props.params; + redirect(`/${params.orgId}/settings/api-keys/${params.apiKeyId}/permissions`); +} diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx new file mode 100644 index 00000000..d1e6f518 --- /dev/null +++ b/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx @@ -0,0 +1,138 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionFooter, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { Button } from "@app/components/ui/button"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { ListApiKeyActionsResponse } from "@server/routers/apiKeys"; +import { AxiosResponse } from "axios"; +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; + +export default function Page() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { orgId, apiKeyId } = useParams(); + + const [loadingPage, setLoadingPage] = useState(true); + const [selectedPermissions, setSelectedPermissions] = useState< + Record + >({}); + const [loadingSavePermissions, setLoadingSavePermissions] = + useState(false); + + useEffect(() => { + async function load() { + setLoadingPage(true); + + const res = await api + .get< + AxiosResponse + >(`/org/${orgId}/api-key/${apiKeyId}/actions`) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error loading API key actions", + description: formatAxiosError( + e, + "Error loading API key actions" + ) + }); + }); + + if (res && res.status === 200) { + const data = res.data.data; + for (const action of data.actions) { + setSelectedPermissions((prev) => ({ + ...prev, + [action.actionId]: true + })); + } + } + + setLoadingPage(false); + } + + load(); + }, []); + + async function savePermissions() { + setLoadingSavePermissions(true); + + const actionsRes = await api + .post(`/org/${orgId}/api-key/${apiKeyId}/actions`, { + actionIds: Object.keys(selectedPermissions).filter( + (key) => selectedPermissions[key] + ) + }) + .catch((e) => { + console.error("Error setting permissions", e); + toast({ + variant: "destructive", + title: "Error setting permissions", + description: formatAxiosError(e) + }); + }); + + if (actionsRes && actionsRes.status === 200) { + toast({ + title: "Permissions updated", + description: "The permissions have been updated." + }); + } + + setLoadingSavePermissions(false); + } + + return ( + <> + {!loadingPage && ( + + + + + Permissions + + + Determine what this API key can do + + + + + + + + + + + + )} + + ); +} diff --git a/src/app/[orgId]/settings/api-keys/create/page.tsx b/src/app/[orgId]/settings/api-keys/create/page.tsx new file mode 100644 index 00000000..3ede2ac0 --- /dev/null +++ b/src/app/[orgId]/settings/api-keys/create/page.tsx @@ -0,0 +1,412 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import HeaderTitle from "@app/components/SettingsSectionTitle"; +import { z } from "zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Input } from "@app/components/ui/input"; +import { InfoIcon } from "lucide-react"; +import { Button } from "@app/components/ui/button"; +import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { AxiosResponse } from "axios"; +import { useParams, useRouter } from "next/navigation"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from "@app/components/ui/breadcrumb"; +import Link from "next/link"; +import { + CreateOrgApiKeyBody, + CreateOrgApiKeyResponse +} from "@server/routers/apiKeys"; +import { ApiKey } from "@server/db/schemas"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import moment from "moment"; +import CopyCodeBox from "@server/emails/templates/components/CopyCodeBox"; +import CopyTextBox from "@app/components/CopyTextBox"; +import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; + +const createFormSchema = z.object({ + name: z + .string() + .min(2, { + message: "Name must be at least 2 characters." + }) + .max(255, { + message: "Name must not be longer than 255 characters." + }) +}); + +type CreateFormValues = z.infer; + +const copiedFormSchema = z + .object({ + copied: z.boolean() + }) + .refine( + (data) => { + return data.copied; + }, + { + message: "You must confirm that you have copied the API key.", + path: ["copied"] + } + ); + +type CopiedFormValues = z.infer; + +export default function Page() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { orgId } = useParams(); + const router = useRouter(); + + const [loadingPage, setLoadingPage] = useState(true); + const [createLoading, setCreateLoading] = useState(false); + const [apiKey, setApiKey] = useState(null); + const [selectedPermissions, setSelectedPermissions] = useState< + Record + >({}); + + const form = useForm({ + resolver: zodResolver(createFormSchema), + defaultValues: { + name: "" + } + }); + + const copiedForm = useForm({ + resolver: zodResolver(copiedFormSchema), + defaultValues: { + copied: false + } + }); + + async function onSubmit(data: CreateFormValues) { + setCreateLoading(true); + + let payload: CreateOrgApiKeyBody = { + name: data.name + }; + + const res = await api + .put< + AxiosResponse + >(`/org/${orgId}/api-key/`, payload) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error creating API key", + description: formatAxiosError(e) + }); + }); + + if (res && res.status === 201) { + const data = res.data.data; + + console.log({ + actionIds: Object.keys(selectedPermissions).filter( + (key) => selectedPermissions[key] + ) + }); + + const actionsRes = await api + .post(`/org/${orgId}/api-key/${data.apiKeyId}/actions`, { + actionIds: Object.keys(selectedPermissions).filter( + (key) => selectedPermissions[key] + ) + }) + .catch((e) => { + console.error("Error setting permissions", e); + toast({ + variant: "destructive", + title: "Error setting permissions", + description: formatAxiosError(e) + }); + }); + + if (actionsRes) { + setApiKey(data); + } + } + + setCreateLoading(false); + } + + async function onCopiedSubmit(data: CopiedFormValues) { + if (!data.copied) { + return; + } + + router.push(`/${orgId}/settings/api-keys`); + } + + const formatLabel = (str: string) => { + return str + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .replace(/^./, (char) => char.toUpperCase()); + }; + + useEffect(() => { + const load = async () => { + setLoadingPage(false); + }; + + load(); + }, []); + + return ( + <> +
+ + +
+ + {!loadingPage && ( +
+ + {!apiKey && ( + <> + + + + API Key Information + + + + +
+ + ( + + + Name + + + + + + + )} + /> + + +
+
+
+ + + + + Permissions + + + Determine what this API key can do + + + + + + + + )} + + {apiKey && ( + + + + Your API Key + + + + + + + Name + + + + + + + + Created + + + {moment( + apiKey.createdAt + ).format("lll")} + + + + + + + + Save Your API Key + + + You will only be able to see this + once. Make sure to copy it to a + secure place. + + + +

+ Your API key is: +

+ + + +
+ + ( + +
+ { + copiedForm.setValue( + "copied", + e as boolean + ); + }} + /> + +
+ +
+ )} + /> + + +
+
+ )} +
+ +
+ {!apiKey && ( + + )} + {!apiKey && ( + + )} + + {apiKey && ( + + )} +
+
+ )} + + ); +} diff --git a/src/app/[orgId]/settings/api-keys/page.tsx b/src/app/[orgId]/settings/api-keys/page.tsx new file mode 100644 index 00000000..ef1e3dd1 --- /dev/null +++ b/src/app/[orgId]/settings/api-keys/page.tsx @@ -0,0 +1,49 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { AxiosResponse } from "axios"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import OrgApiKeysTable, { OrgApiKeyRow } from "./OrgApiKeysTable"; +import { ListOrgApiKeysResponse } from "@server/routers/apiKeys"; + +type ApiKeyPageProps = { + params: Promise<{ orgId: string }>; +}; + +export const dynamic = "force-dynamic"; + +export default async function ApiKeysPage(props: ApiKeyPageProps) { + const params = await props.params; + let apiKeys: ListOrgApiKeysResponse["apiKeys"] = []; + try { + const res = await internal.get>( + `/org/${params.orgId}/api-keys`, + await authCookieHeader() + ); + apiKeys = res.data.data.apiKeys; + } catch (e) {} + + const rows: OrgApiKeyRow[] = apiKeys.map((key) => { + return { + name: key.name, + id: key.apiKeyId, + key: `${key.apiKeyId}••••••••••••••••••••${key.lastChars}`, + createdAt: key.createdAt + }; + }); + + return ( + <> + + + + + ); +} diff --git a/src/app/admin/api-keys/ApiKeysDataTable.tsx b/src/app/admin/api-keys/ApiKeysDataTable.tsx new file mode 100644 index 00000000..f65949a4 --- /dev/null +++ b/src/app/admin/api-keys/ApiKeysDataTable.tsx @@ -0,0 +1,58 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + SortingState, + getSortedRowModel, + ColumnFiltersState, + getFilteredRowModel +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Button } from "@app/components/ui/button"; +import { useState } from "react"; +import { Input } from "@app/components/ui/input"; +import { DataTablePagination } from "@app/components/DataTablePagination"; +import { Plus, Search } from "lucide-react"; +import { DataTable } from "@app/components/ui/data-table"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + addApiKey?: () => void; +} + +export function ApiKeysDataTable({ + addApiKey, + columns, + data +}: DataTableProps) { + return ( + + ); +} diff --git a/src/app/admin/api-keys/ApiKeysTable.tsx b/src/app/admin/api-keys/ApiKeysTable.tsx new file mode 100644 index 00000000..c44d43f3 --- /dev/null +++ b/src/app/admin/api-keys/ApiKeysTable.tsx @@ -0,0 +1,199 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; +import { Button } from "@app/components/ui/button"; +import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import { toast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import moment from "moment"; +import { ApiKeysDataTable } from "./ApiKeysDataTable"; + +export type ApiKeyRow = { + id: string; + key: string; + name: string; + createdAt: string; +}; + +type ApiKeyTableProps = { + apiKeys: ApiKeyRow[]; +}; + +export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { + const router = useRouter(); + + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selected, setSelected] = useState(null); + const [rows, setRows] = useState(apiKeys); + + const api = createApiClient(useEnvContext()); + + const deleteSite = (apiKeyId: string) => { + api.delete(`/api-key/${apiKeyId}`) + .catch((e) => { + console.error("Error deleting API key", e); + toast({ + variant: "destructive", + title: "Error deleting API key", + description: formatAxiosError(e, "Error deleting API key") + }); + }) + .then(() => { + router.refresh(); + setIsDeleteModalOpen(false); + + const newRows = rows.filter((row) => row.id !== apiKeyId); + + setRows(newRows); + }); + }; + + const columns: ColumnDef[] = [ + { + id: "dots", + cell: ({ row }) => { + const apiKeyROw = row.original; + const router = useRouter(); + + return ( + + + + + + { + setSelected(apiKeyROw); + }} + > + View settings + + { + setSelected(apiKeyROw); + setIsDeleteModalOpen(true); + }} + > + Delete + + + + ); + } + }, + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "key", + header: "Key", + cell: ({ row }) => { + const r = row.original; + return {r.key}; + } + }, + { + accessorKey: "createdAt", + header: "Created At", + cell: ({ row }) => { + const r = row.original; + return {moment(r.createdAt).format("lll")} ; + } + }, + { + id: "actions", + cell: ({ row }) => { + const r = row.original; + return ( +
+ + + +
+ ); + } + } + ]; + + return ( + <> + {selected && ( + { + setIsDeleteModalOpen(val); + setSelected(null); + }} + dialog={ +
+

+ Are you sure you want to remove the API key{" "} + {selected?.name || selected?.id}? +

+ +

+ + Once removed, the API key will no longer be + able to be used. + +

+ +

+ To confirm, please type the name of the API key + below. +

+
+ } + buttonText="Confirm Delete API Key" + onConfirm={async () => deleteSite(selected!.id)} + string={selected.name} + title="Delete API Key" + /> + )} + + { + router.push(`/admin/api-keys/create`); + }} + /> + + ); +} diff --git a/src/app/admin/api-keys/[apiKeyId]/layout.tsx b/src/app/admin/api-keys/[apiKeyId]/layout.tsx new file mode 100644 index 00000000..be3147ea --- /dev/null +++ b/src/app/admin/api-keys/[apiKeyId]/layout.tsx @@ -0,0 +1,62 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { internal } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { redirect } from "next/navigation"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { SidebarSettings } from "@app/components/SidebarSettings"; +import Link from "next/link"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from "@app/components/ui/breadcrumb"; +import { GetApiKeyResponse } from "@server/routers/apiKeys"; +import ApiKeyProvider from "@app/providers/ApiKeyProvider"; +import { HorizontalTabs } from "@app/components/HorizontalTabs"; + +interface SettingsLayoutProps { + children: React.ReactNode; + params: Promise<{ apiKeyId: string }>; +} + +export default async function SettingsLayout(props: SettingsLayoutProps) { + const params = await props.params; + + const { children } = props; + + let apiKey = null; + try { + const res = await internal.get>( + `/api-key/${params.apiKeyId}`, + await authCookieHeader() + ); + apiKey = res.data.data; + } catch (e) { + console.error(e); + redirect(`/admin/api-keys`); + } + + const navItems = [ + { + title: "Permissions", + href: "/admin/api-keys/{apiKeyId}/permissions" + } + ]; + + return ( + <> + + + + {children} + + + ); +} diff --git a/src/app/admin/api-keys/[apiKeyId]/page.tsx b/src/app/admin/api-keys/[apiKeyId]/page.tsx new file mode 100644 index 00000000..b0e4c3e5 --- /dev/null +++ b/src/app/admin/api-keys/[apiKeyId]/page.tsx @@ -0,0 +1,13 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { redirect } from "next/navigation"; + +export default async function ApiKeysPage(props: { + params: Promise<{ apiKeyId: string }>; +}) { + const params = await props.params; + redirect(`/admin/api-keys/${params.apiKeyId}/permissions`); +} diff --git a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx new file mode 100644 index 00000000..c468c139 --- /dev/null +++ b/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx @@ -0,0 +1,139 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionFooter, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { Button } from "@app/components/ui/button"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { ListApiKeyActionsResponse } from "@server/routers/apiKeys"; +import { AxiosResponse } from "axios"; +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; + +export default function Page() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { apiKeyId } = useParams(); + + const [loadingPage, setLoadingPage] = useState(true); + const [selectedPermissions, setSelectedPermissions] = useState< + Record + >({}); + const [loadingSavePermissions, setLoadingSavePermissions] = + useState(false); + + useEffect(() => { + async function load() { + setLoadingPage(true); + + const res = await api + .get< + AxiosResponse + >(`/api-key/${apiKeyId}/actions`) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error loading API key actions", + description: formatAxiosError( + e, + "Error loading API key actions" + ) + }); + }); + + if (res && res.status === 200) { + const data = res.data.data; + for (const action of data.actions) { + setSelectedPermissions((prev) => ({ + ...prev, + [action.actionId]: true + })); + } + } + + setLoadingPage(false); + } + + load(); + }, []); + + async function savePermissions() { + setLoadingSavePermissions(true); + + const actionsRes = await api + .post(`/api-key/${apiKeyId}/actions`, { + actionIds: Object.keys(selectedPermissions).filter( + (key) => selectedPermissions[key] + ) + }) + .catch((e) => { + console.error("Error setting permissions", e); + toast({ + variant: "destructive", + title: "Error setting permissions", + description: formatAxiosError(e) + }); + }); + + if (actionsRes && actionsRes.status === 200) { + toast({ + title: "Permissions updated", + description: "The permissions have been updated." + }); + } + + setLoadingSavePermissions(false); + } + + return ( + <> + {!loadingPage && ( + + + + + Permissions + + + Determine what this API key can do + + + + + + + + + + + + )} + + ); +} diff --git a/src/app/admin/api-keys/create/page.tsx b/src/app/admin/api-keys/create/page.tsx new file mode 100644 index 00000000..c76b1859 --- /dev/null +++ b/src/app/admin/api-keys/create/page.tsx @@ -0,0 +1,402 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import HeaderTitle from "@app/components/SettingsSectionTitle"; +import { z } from "zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Input } from "@app/components/ui/input"; +import { InfoIcon } from "lucide-react"; +import { Button } from "@app/components/ui/button"; +import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { AxiosResponse } from "axios"; +import { useParams, useRouter } from "next/navigation"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from "@app/components/ui/breadcrumb"; +import Link from "next/link"; +import { + CreateOrgApiKeyBody, + CreateOrgApiKeyResponse +} from "@server/routers/apiKeys"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import moment from "moment"; +import CopyTextBox from "@app/components/CopyTextBox"; +import PermissionsSelectBox from "@app/components/PermissionsSelectBox"; + +const createFormSchema = z.object({ + name: z + .string() + .min(2, { + message: "Name must be at least 2 characters." + }) + .max(255, { + message: "Name must not be longer than 255 characters." + }) +}); + +type CreateFormValues = z.infer; + +const copiedFormSchema = z + .object({ + copied: z.boolean() + }) + .refine( + (data) => { + return data.copied; + }, + { + message: "You must confirm that you have copied the API key.", + path: ["copied"] + } + ); + +type CopiedFormValues = z.infer; + +export default function Page() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const router = useRouter(); + + const [loadingPage, setLoadingPage] = useState(true); + const [createLoading, setCreateLoading] = useState(false); + const [apiKey, setApiKey] = useState(null); + const [selectedPermissions, setSelectedPermissions] = useState< + Record + >({}); + + const form = useForm({ + resolver: zodResolver(createFormSchema), + defaultValues: { + name: "" + } + }); + + const copiedForm = useForm({ + resolver: zodResolver(copiedFormSchema), + defaultValues: { + copied: false + } + }); + + async function onSubmit(data: CreateFormValues) { + setCreateLoading(true); + + let payload: CreateOrgApiKeyBody = { + name: data.name + }; + + const res = await api + .put>(`/api-key`, payload) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error creating API key", + description: formatAxiosError(e) + }); + }); + + if (res && res.status === 201) { + const data = res.data.data; + + console.log({ + actionIds: Object.keys(selectedPermissions).filter( + (key) => selectedPermissions[key] + ) + }); + + const actionsRes = await api + .post(`/api-key/${data.apiKeyId}/actions`, { + actionIds: Object.keys(selectedPermissions).filter( + (key) => selectedPermissions[key] + ) + }) + .catch((e) => { + console.error("Error setting permissions", e); + toast({ + variant: "destructive", + title: "Error setting permissions", + description: formatAxiosError(e) + }); + }); + + if (actionsRes) { + setApiKey(data); + } + } + + setCreateLoading(false); + } + + async function onCopiedSubmit(data: CopiedFormValues) { + if (!data.copied) { + return; + } + + router.push(`/admin/api-keys`); + } + + useEffect(() => { + const load = async () => { + setLoadingPage(false); + }; + + load(); + }, []); + + return ( + <> +
+ + +
+ + {!loadingPage && ( +
+ + {!apiKey && ( + <> + + + + API Key Information + + + + +
+ + ( + + + Name + + + + + + + )} + /> + + +
+
+
+ + + + + Permissions + + + Determine what this API key can do + + + + + + + + )} + + {apiKey && ( + + + + Your API Key + + + + + + + Name + + + + + + + + Created + + + {moment( + apiKey.createdAt + ).format("lll")} + + + + + + + + Save Your API Key + + + You will only be able to see this + once. Make sure to copy it to a + secure place. + + + +

+ Your API key is: +

+ + + +
+ + ( + +
+ { + copiedForm.setValue( + "copied", + e as boolean + ); + }} + /> + +
+ +
+ )} + /> + + +
+
+ )} +
+ +
+ {!apiKey && ( + + )} + {!apiKey && ( + + )} + + {apiKey && ( + + )} +
+
+ )} + + ); +} diff --git a/src/app/admin/api-keys/page.tsx b/src/app/admin/api-keys/page.tsx new file mode 100644 index 00000000..b4a00806 --- /dev/null +++ b/src/app/admin/api-keys/page.tsx @@ -0,0 +1,46 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { AxiosResponse } from "axios"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { ListRootApiKeysResponse } from "@server/routers/apiKeys"; +import ApiKeysTable, { ApiKeyRow } from "./ApiKeysTable"; + +type ApiKeyPageProps = {}; + +export const dynamic = "force-dynamic"; + +export default async function ApiKeysPage(props: ApiKeyPageProps) { + let apiKeys: ListRootApiKeysResponse["apiKeys"] = []; + try { + const res = await internal.get>( + `/api-keys`, + await authCookieHeader() + ); + apiKeys = res.data.data.apiKeys; + } catch (e) {} + + const rows: ApiKeyRow[] = apiKeys.map((key) => { + return { + name: key.name, + id: key.apiKeyId, + key: `${key.apiKeyId}••••••••••••••••••••${key.lastChars}`, + createdAt: key.createdAt + }; + }); + + return ( + <> + + + + + ); +} diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index ebda31a1..d244e13d 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -40,6 +40,11 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { { title: "General", href: `/admin/idp/${params.idpId}/general` + }, + { + title: "Organization Policies", + href: `/admin/idp/${params.idpId}/policies`, + showProfessional: true } ]; diff --git a/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx new file mode 100644 index 00000000..222e98eb --- /dev/null +++ b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx @@ -0,0 +1,33 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { DataTable } from "@app/components/ui/data-table"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + onAdd: () => void; +} + +export function PolicyDataTable({ + columns, + data, + onAdd +}: DataTableProps) { + return ( + + ); +} diff --git a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx new file mode 100644 index 00000000..df78c648 --- /dev/null +++ b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx @@ -0,0 +1,159 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { Button } from "@app/components/ui/button"; +import { + ArrowUpDown, + Trash2, + MoreHorizontal, + Pencil, + ArrowRight +} from "lucide-react"; +import { PolicyDataTable } from "./PolicyDataTable"; +import { Badge } from "@app/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; +import Link from "next/link"; +import { InfoPopup } from "@app/components/ui/info-popup"; + +export interface PolicyRow { + orgId: string; + roleMapping?: string; + orgMapping?: string; +} + +interface Props { + policies: PolicyRow[]; + onDelete: (orgId: string) => void; + onAdd: () => void; + onEdit: (policy: PolicyRow) => void; +} + +export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props) { + const columns: ColumnDef[] = [ + { + id: "dots", + cell: ({ row }) => { + const r = row.original; + + return ( + + + + + + { + onDelete(r.orgId); + }} + > + Delete + + + + ); + } + }, + { + accessorKey: "orgId", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "roleMapping", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const mapping = row.original.roleMapping; + return mapping ? ( + 50 ? `${mapping.substring(0, 50)}...` : mapping} + info={mapping} + /> + ) : ( + "--" + ); + } + }, + { + accessorKey: "orgMapping", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const mapping = row.original.orgMapping; + return mapping ? ( + 50 ? `${mapping.substring(0, 50)}...` : mapping} + info={mapping} + /> + ) : ( + "--" + ); + } + }, + { + id: "actions", + cell: ({ row }) => { + const policy = row.original; + return ( +
+ +
+ ); + } + } + ]; + + return ; +} diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx new file mode 100644 index 00000000..9fb9b49b --- /dev/null +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -0,0 +1,645 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { Button } from "@app/components/ui/button"; +import { Input } from "@app/components/ui/input"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon, ExternalLink, CheckIcon } from "lucide-react"; +import PolicyTable, { PolicyRow } from "./PolicyTable"; +import { AxiosResponse } from "axios"; +import { ListOrgsResponse } from "@server/routers/org"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { cn } from "@app/lib/cn"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { CaretSortIcon } from "@radix-ui/react-icons"; +import Link from "next/link"; +import { Textarea } from "@app/components/ui/textarea"; +import { InfoPopup } from "@app/components/ui/info-popup"; +import { GetIdpResponse } from "@server/routers/idp"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionHeader, + SettingsSectionTitle, + SettingsSectionDescription, + SettingsSectionBody, + SettingsSectionFooter, + SettingsSectionForm +} from "@app/components/Settings"; + +type Organization = { + orgId: string; + name: string; +}; + +const policyFormSchema = z.object({ + orgId: z.string().min(1, { message: "Organization is required" }), + roleMapping: z.string().optional(), + orgMapping: z.string().optional() +}); + +const defaultMappingsSchema = z.object({ + defaultRoleMapping: z.string().optional(), + defaultOrgMapping: z.string().optional() +}); + +type PolicyFormValues = z.infer; +type DefaultMappingsValues = z.infer; + +export default function PoliciesPage() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const router = useRouter(); + const { idpId } = useParams(); + + const [pageLoading, setPageLoading] = useState(true); + const [addPolicyLoading, setAddPolicyLoading] = useState(false); + const [editPolicyLoading, setEditPolicyLoading] = useState(false); + const [deletePolicyLoading, setDeletePolicyLoading] = useState(false); + const [updateDefaultMappingsLoading, setUpdateDefaultMappingsLoading] = + useState(false); + const [policies, setPolicies] = useState([]); + const [organizations, setOrganizations] = useState([]); + const [showAddDialog, setShowAddDialog] = useState(false); + const [editingPolicy, setEditingPolicy] = useState(null); + + const form = useForm({ + resolver: zodResolver(policyFormSchema), + defaultValues: { + orgId: "", + roleMapping: "", + orgMapping: "" + } + }); + + const defaultMappingsForm = useForm({ + resolver: zodResolver(defaultMappingsSchema), + defaultValues: { + defaultRoleMapping: "", + defaultOrgMapping: "" + } + }); + + const loadIdp = async () => { + try { + const res = await api.get>( + `/idp/${idpId}` + ); + if (res.status === 200) { + const data = res.data.data; + defaultMappingsForm.reset({ + defaultRoleMapping: data.idp.defaultRoleMapping || "", + defaultOrgMapping: data.idp.defaultOrgMapping || "" + }); + } + } catch (e) { + toast({ + title: "Error", + description: formatAxiosError(e), + variant: "destructive" + }); + } + }; + + const loadPolicies = async () => { + try { + const res = await api.get(`/idp/${idpId}/org`); + if (res.status === 200) { + setPolicies(res.data.data.policies); + } + } catch (e) { + toast({ + title: "Error", + description: formatAxiosError(e), + variant: "destructive" + }); + } + }; + + const loadOrganizations = async () => { + try { + const res = await api.get>("/orgs"); + if (res.status === 200) { + const existingOrgIds = policies.map((p) => p.orgId); + const availableOrgs = res.data.data.orgs.filter( + (org) => !existingOrgIds.includes(org.orgId) + ); + setOrganizations(availableOrgs); + } + } catch (e) { + toast({ + title: "Error", + description: formatAxiosError(e), + variant: "destructive" + }); + } + }; + + useEffect(() => { + async function load() { + setPageLoading(true); + await loadPolicies(); + await loadIdp(); + setPageLoading(false); + } + load(); + }, [idpId]); + + const onAddPolicy = async (data: PolicyFormValues) => { + setAddPolicyLoading(true); + try { + const res = await api.put(`/idp/${idpId}/org/${data.orgId}`, { + roleMapping: data.roleMapping, + orgMapping: data.orgMapping + }); + if (res.status === 201) { + const newPolicy = { + orgId: data.orgId, + name: + organizations.find((org) => org.orgId === data.orgId) + ?.name || "", + roleMapping: data.roleMapping, + orgMapping: data.orgMapping + }; + setPolicies([...policies, newPolicy]); + toast({ + title: "Success", + description: "Policy added successfully" + }); + setShowAddDialog(false); + form.reset(); + } + } catch (e) { + toast({ + title: "Error", + description: formatAxiosError(e), + variant: "destructive" + }); + } finally { + setAddPolicyLoading(false); + } + }; + + const onEditPolicy = async (data: PolicyFormValues) => { + if (!editingPolicy) return; + + setEditPolicyLoading(true); + try { + const res = await api.post( + `/idp/${idpId}/org/${editingPolicy.orgId}`, + { + roleMapping: data.roleMapping, + orgMapping: data.orgMapping + } + ); + if (res.status === 200) { + setPolicies( + policies.map((policy) => + policy.orgId === editingPolicy.orgId + ? { + ...policy, + roleMapping: data.roleMapping, + orgMapping: data.orgMapping + } + : policy + ) + ); + toast({ + title: "Success", + description: "Policy updated successfully" + }); + setShowAddDialog(false); + setEditingPolicy(null); + form.reset(); + } + } catch (e) { + toast({ + title: "Error", + description: formatAxiosError(e), + variant: "destructive" + }); + } finally { + setEditPolicyLoading(false); + } + }; + + const onDeletePolicy = async (orgId: string) => { + setDeletePolicyLoading(true); + try { + const res = await api.delete(`/idp/${idpId}/org/${orgId}`); + if (res.status === 200) { + setPolicies( + policies.filter((policy) => policy.orgId !== orgId) + ); + toast({ + title: "Success", + description: "Policy deleted successfully" + }); + } + } catch (e) { + toast({ + title: "Error", + description: formatAxiosError(e), + variant: "destructive" + }); + } finally { + setDeletePolicyLoading(false); + } + }; + + const onUpdateDefaultMappings = async (data: DefaultMappingsValues) => { + setUpdateDefaultMappingsLoading(true); + try { + const res = await api.post(`/idp/${idpId}/oidc`, { + defaultRoleMapping: data.defaultRoleMapping, + defaultOrgMapping: data.defaultOrgMapping + }); + if (res.status === 200) { + toast({ + title: "Success", + description: "Default mappings updated successfully" + }); + } + } catch (e) { + toast({ + title: "Error", + description: formatAxiosError(e), + variant: "destructive" + }); + } finally { + setUpdateDefaultMappingsLoading(false); + } + }; + + if (pageLoading) { + return null; + } + + return ( + <> + + + + + About Organization Policies + + + Organization policies are used to control access to + organizations based on the user's ID token. You can + specify JMESPath expressions to extract role and + organization information from the ID token. For more + information, see{" "} + + the documentation + + + + + + + + + Default Mappings (Optional) + + + The default mappings are used when when there is not + an organization policy defined for an organization. + You can specify the default role and organization + mappings to fall back to here. + + + +
+ +
+ ( + + + Default Role Mapping + + + + + + JMESPath to extract role + information from the ID + token. The result of this + expression must return the + role name as defined in the + organization as a string. + + + + )} + /> + + ( + + + Default Organization Mapping + + + + + + JMESPath to extract + organization information + from the ID token. This + expression must return thr + org ID or true for the user + to be allowed to access the + organization. + + + + )} + /> +
+
+ + + + +
+
+ + { + loadOrganizations(); + form.reset({ + orgId: "", + roleMapping: "", + orgMapping: "" + }); + setEditingPolicy(null); + setShowAddDialog(true); + }} + onEdit={(policy) => { + setEditingPolicy(policy); + form.reset({ + orgId: policy.orgId, + roleMapping: policy.roleMapping || "", + orgMapping: policy.orgMapping || "" + }); + setShowAddDialog(true); + }} + /> +
+ + { + setShowAddDialog(val); + setEditingPolicy(null); + form.reset(); + }} + > + + + + {editingPolicy + ? "Edit Organization Policy" + : "Add Organization Policy"} + + + Configure access for an organization + + + +
+ + ( + + Organization + {editingPolicy ? ( + + ) : ( + + + + + + + + + + + + No org + found. + + + {organizations.map( + ( + org + ) => ( + { + form.setValue( + "orgId", + org.orgId + ); + }} + > + + { + org.name + } + + ) + )} + + + + + + )} + + + )} + /> + + ( + + + Role Mapping Path (Optional) + + + + + + JMESPath to extract role + information from the ID token. + The result of this expression + must return the role name as + defined in the organization as a + string. + + + + )} + /> + + ( + + + Organization Mapping Path + (Optional) + + + + + + JMESPath to extract organization + information from the ID token. + This expression must return the + org ID or true for the user to + be allowed to access the + organization. + + + + )} + /> + + +
+ + + + + + +
+
+ + ); +} diff --git a/src/app/admin/license/LicenseKeysDataTable.tsx b/src/app/admin/license/LicenseKeysDataTable.tsx index dea3f38d..98ed814a 100644 --- a/src/app/admin/license/LicenseKeysDataTable.tsx +++ b/src/app/admin/license/LicenseKeysDataTable.tsx @@ -16,7 +16,7 @@ import CopyToClipboard from "@app/components/CopyToClipboard"; type LicenseKeysDataTableProps = { licenseKeys: LicenseKeyCache[]; - onDelete: (key: string) => void; + onDelete: (key: LicenseKeyCache) => void; onCreate: () => void; }; @@ -124,7 +124,7 @@ export function LicenseKeysDataTable({
diff --git a/src/app/admin/license/components/SitePriceCalculator.tsx b/src/app/admin/license/components/SitePriceCalculator.tsx index cee00a44..f0ff460c 100644 --- a/src/app/admin/license/components/SitePriceCalculator.tsx +++ b/src/app/admin/license/components/SitePriceCalculator.tsx @@ -40,19 +40,25 @@ export function SitePriceCalculator({ setSiteCount((prev) => (prev > 1 ? prev - 1 : 1)); }; - const totalCost = mode === "license" - ? licenseFlatRate + (siteCount * pricePerSite) - : siteCount * pricePerSite; + const totalCost = + mode === "license" + ? licenseFlatRate + siteCount * pricePerSite + : siteCount * pricePerSite; return ( - {mode === "license" ? "Purchase License" : "Purchase Additional Sites"} + {mode === "license" + ? "Purchase License" + : "Purchase Additional Sites"} - Choose how many sites you want to {mode === "license" ? "purchase a license for" : "add to your existing license"}. + Choose how many sites you want to{" "} + {mode === "license" + ? "purchase a license for. You can always add more sites later." + : "add to your existing license."} @@ -108,14 +114,26 @@ export function SitePriceCalculator({ Number of sites: - - {siteCount} - + {siteCount}
Total: ${totalCost.toFixed(2)} / mo
+ +

+ For the most up-to-date pricing, please visit + our{" "} + + pricing page + + . +

diff --git a/src/app/admin/license/page.tsx b/src/app/admin/license/page.tsx index 153fe7a6..74f86c96 100644 --- a/src/app/admin/license/page.tsx +++ b/src/app/admin/license/page.tsx @@ -55,6 +55,7 @@ import { Progress } from "@app/components/ui/progress"; import { MinusCircle, PlusCircle } from "lucide-react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { SitePriceCalculator } from "./components/SitePriceCalculator"; +import Link from "next/link"; const formSchema = z.object({ licenseKey: z @@ -75,9 +76,8 @@ export default function LicensePage() { const [rows, setRows] = useState([]); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selectedLicenseKey, setSelectedLicenseKey] = useState( - null - ); + const [selectedLicenseKey, setSelectedLicenseKey] = + useState(null); const router = useRouter(); const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext(); const [hostLicense, setHostLicense] = useState(null); @@ -136,7 +136,8 @@ export default function LicensePage() { async function deleteLicenseKey(key: string) { try { setIsDeletingLicense(true); - const res = await api.delete(`/license/${key}`); + const encodedKey = encodeURIComponent(key); + const res = await api.delete(`/license/${encodedKey}`); if (res.data.data) { updateLicenseStatus(res.data.data); } @@ -294,7 +295,11 @@ export default function LicensePage() {

Are you sure you want to delete the license key{" "} - {obfuscateLicenseKey(selectedLicenseKey)} + + {obfuscateLicenseKey( + selectedLicenseKey.licenseKey + )} + ?

@@ -310,8 +315,10 @@ export default function LicensePage() {

} buttonText="Confirm Delete License Key" - onConfirm={async () => deleteLicenseKey(selectedLicenseKey)} - string={selectedLicenseKey} + onConfirm={async () => + deleteLicenseKey(selectedLicenseKey.licenseKeyEncrypted) + } + string={selectedLicenseKey.licenseKey} title="Delete License Key" /> )} @@ -428,12 +435,6 @@ export default function LicensePage() { {!licenseStatus?.isHostLicensed ? ( <> - ) : ( - + <> + + )}
diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index c8b98453..b05bf30b 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -86,7 +86,7 @@ export const adminNavItems: SidebarNavItem[] = [ }, { title: "API Keys", - href: "/{orgId}/settings/api-keys", + href: "/admin/api-keys", icon: , showProfessional: true }, diff --git a/src/components/HorizontalTabs.tsx b/src/components/HorizontalTabs.tsx index b354fe26..eb590eb0 100644 --- a/src/components/HorizontalTabs.tsx +++ b/src/components/HorizontalTabs.tsx @@ -35,7 +35,8 @@ export function HorizontalTabs({ .replace("{orgId}", params.orgId as string) .replace("{resourceId}", params.resourceId as string) .replace("{niceId}", params.niceId as string) - .replace("{userId}", params.userId as string); + .replace("{userId}", params.userId as string) + .replace("{apiKeyId}", params.apiKeyId as string); } return ( diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx new file mode 100644 index 00000000..18bb11d4 --- /dev/null +++ b/src/components/PermissionsSelectBox.tsx @@ -0,0 +1,238 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import { CheckboxWithLabel } from "@app/components/ui/checkbox"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; + +type PermissionsSelectBoxProps = { + root?: boolean; + selectedPermissions: Record; + onChange: (updated: Record) => void; +}; + +function getActionsCategories(root: boolean) { + const actionsByCategory: Record> = { + Organization: { + "Get Organization": "getOrg", + "Update Organization": "updateOrg", + "Get Organization User": "getOrgUser", + "List Organization Domains": "listOrgDomains", + "Check Org ID": "checkOrgId", + "List Orgs": "listOrgs" + }, + + Site: { + "Create Site": "createSite", + "Delete Site": "deleteSite", + "Get Site": "getSite", + "List Sites": "listSites", + "Update Site": "updateSite", + "List Allowed Site Roles": "listSiteRoles" + }, + + Resource: { + "Create Resource": "createResource", + "Delete Resource": "deleteResource", + "Get Resource": "getResource", + "List Resources": "listResources", + "Update Resource": "updateResource", + "List Resource Users": "listResourceUsers", + "Set Resource Users": "setResourceUsers", + "Set Allowed Resource Roles": "setResourceRoles", + "List Allowed Resource Roles": "listResourceRoles", + "Set Resource Password": "setResourcePassword", + "Set Resource Pincode": "setResourcePincode", + "Set Resource Email Whitelist": "setResourceWhitelist", + "Get Resource Email Whitelist": "getResourceWhitelist" + }, + + Target: { + "Create Target": "createTarget", + "Delete Target": "deleteTarget", + "Get Target": "getTarget", + "List Targets": "listTargets", + "Update Target": "updateTarget" + }, + + Role: { + "Create Role": "createRole", + "Delete Role": "deleteRole", + "Get Role": "getRole", + "List Roles": "listRoles", + "Update Role": "updateRole", + "List Allowed Role Resources": "listRoleResources" + }, + + User: { + "Invite User": "inviteUser", + "Remove User": "removeUser", + "List Users": "listUsers", + "Add User Role": "addUserRole" + }, + + "Access Token": { + "Generate Access Token": "generateAccessToken", + "Delete Access Token": "deleteAcessToken", + "List Access Tokens": "listAccessTokens" + }, + + "Resource Rule": { + "Create Resource Rule": "createResourceRule", + "Delete Resource Rule": "deleteResourceRule", + "List Resource Rules": "listResourceRules", + "Update Resource Rule": "updateResourceRule" + } + + // "Newt": { + // "Create Newt": "createNewt" + // }, + }; + + if (root) { + actionsByCategory["Organization"] = { + "Create Organization": "createOrg", + "Delete Organization": "deleteOrg", + "List API Keys": "listApiKeys", + "List API Key Actions": "listApiKeyActions", + "Set API Key Allowed Actions": "setApiKeyActions", + "Create API Key": "createApiKey", + "Delete API Key": "deleteApiKey", + ...actionsByCategory["Organization"] + }; + + actionsByCategory["Identity Provider (IDP)"] = { + "Create IDP": "createIdp", + "Update IDP": "updateIdp", + "Delete IDP": "deleteIdp", + "List IDP": "listIdps", + "Get IDP": "getIdp", + "Create IDP Org Policy": "createIdpOrg", + "Delete IDP Org Policy": "deleteIdpOrg", + "List IDP Orgs": "listIdpOrgs", + "Update IDP Org": "updateIdpOrg" + }; + } + + return actionsByCategory; +} + +export default function PermissionsSelectBox({ + root, + selectedPermissions, + onChange +}: PermissionsSelectBoxProps) { + const actionsByCategory = getActionsCategories(root ?? false); + + const togglePermission = (key: string, checked: boolean) => { + onChange({ + ...selectedPermissions, + [key]: checked + }); + }; + + const areAllCheckedInCategory = (actions: Record) => { + return Object.values(actions).every( + (action) => selectedPermissions[action] + ); + }; + + const toggleAllInCategory = ( + actions: Record, + value: boolean + ) => { + const updated = { ...selectedPermissions }; + Object.values(actions).forEach((action) => { + updated[action] = value; + }); + onChange(updated); + }; + + const allActions = Object.values(actionsByCategory).flatMap(Object.values); + const allPermissionsChecked = allActions.every( + (action) => selectedPermissions[action] + ); + + const toggleAllPermissions = (checked: boolean) => { + const updated: Record = {}; + allActions.forEach((action) => { + updated[action] = checked; + }); + onChange(updated); + }; + + return ( + <> +
+ + toggleAllPermissions(checked as boolean) + } + /> +
+ + {Object.entries(actionsByCategory).map( + ([category, actions]) => { + const allChecked = areAllCheckedInCategory(actions); + return ( + + {category} + +
+ + toggleAllInCategory( + actions, + checked as boolean + ) + } + /> + {Object.entries(actions).map( + ([label, value]) => ( + + togglePermission( + value, + checked as boolean + ) + } + /> + ) + )} +
+
+
+ ); + } + )} +
+ + ); +} diff --git a/src/contexts/apiKeyContext.ts b/src/contexts/apiKeyContext.ts new file mode 100644 index 00000000..dd6c9b83 --- /dev/null +++ b/src/contexts/apiKeyContext.ts @@ -0,0 +1,16 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import { GetApiKeyResponse } from "@server/routers/apiKeys"; +import { createContext } from "react"; + +interface ApiKeyContextType { + apiKey: GetApiKeyResponse; + updateApiKey: (updatedApiKey: Partial) => void; +} + +const ApiKeyContext = createContext(undefined); + +export default ApiKeyContext; diff --git a/src/hooks/useApikeyContext.ts b/src/hooks/useApikeyContext.ts new file mode 100644 index 00000000..3ebcbddc --- /dev/null +++ b/src/hooks/useApikeyContext.ts @@ -0,0 +1,17 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +import ApiKeyContext from "@app/contexts/apiKeyContext"; +import { useContext } from "react"; + +export function useApiKeyContext() { + const context = useContext(ApiKeyContext); + if (context === undefined) { + throw new Error( + "useApiKeyContext must be used within a ApiKeyProvider" + ); + } + return context; +} diff --git a/src/providers/ApiKeyProvider.tsx b/src/providers/ApiKeyProvider.tsx new file mode 100644 index 00000000..13061da3 --- /dev/null +++ b/src/providers/ApiKeyProvider.tsx @@ -0,0 +1,42 @@ +// This file is licensed under the Fossorial Commercial License. +// Unauthorized use, copying, modification, or distribution is strictly prohibited. +// +// Copyright (c) 2025 Fossorial LLC. All rights reserved. + +"use client"; + +import ApiKeyContext from "@app/contexts/apiKeyContext"; +import { GetApiKeyResponse } from "@server/routers/apiKeys"; +import { useState } from "react"; + +interface ApiKeyProviderProps { + children: React.ReactNode; + apiKey: GetApiKeyResponse; +} + +export function ApiKeyProvider({ children, apiKey: ak }: ApiKeyProviderProps) { + const [apiKey, setApiKey] = useState(ak); + + const updateApiKey = (updatedApiKey: Partial) => { + if (!apiKey) { + throw new Error("No API key to update"); + } + setApiKey((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + ...updatedApiKey + }; + }); + }; + + return ( + + {children} + + ); +} + +export default ApiKeyProvider; From 18e6f16ce7448be0472619158e0354a2857c9c1a Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 28 Apr 2025 21:45:43 -0400 Subject: [PATCH 47/53] add snippets to create resource --- server/license/license.ts | 3 +- .../resources/[resourceId]/proxy/page.tsx | 78 +- .../settings/resources/create/page.tsx | 1030 +++++++++-------- .../share-links/CreateShareLinkForm.tsx | 12 +- src/app/components/LicenseViolation.tsx | 7 +- 5 files changed, 634 insertions(+), 496 deletions(-) diff --git a/server/license/license.ts b/server/license/license.ts index b1398d13..3f01effd 100644 --- a/server/license/license.ts +++ b/server/license/license.ts @@ -137,7 +137,8 @@ LQIDAQAB hostId: this.hostId, isHostLicensed: true, isLicenseValid: false, - maxSites: undefined + maxSites: undefined, + usedSites: siteCount.value }; try { diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index 8c027d30..ea388176 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -67,6 +67,12 @@ import { SwitchInput } from "@app/components/SwitchInput"; import { useRouter } from "next/navigation"; import { isTargetValid } from "@server/lib/validators"; import { tlsNameSchema } from "@server/lib/schemas"; +import { ChevronsUpDown } from "lucide-react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from "@app/components/ui/collapsible"; const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), @@ -145,6 +151,7 @@ export default function ReverseProxyTargets(props: { const [proxySettingsLoading, setProxySettingsLoading] = useState(false); const [pageLoading, setPageLoading] = useState(true); + const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); const router = useRouter(); const addTargetForm = useForm({ @@ -589,26 +596,57 @@ export default function ReverseProxyTargets(props: { )} /> - ( - - - TLS Server Name (SNI) - - - - - - The TLS Server Name to use - for SNI. Leave empty to use - the default. - - - - )} - /> + +
+ + + +
+ + ( + + + TLS Server Name + (SNI) + + + + + + The TLS Server Name + to use for SNI. + Leave empty to use + the default. + + + + )} + /> + +
diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index d500a809..704a1947 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -59,6 +59,9 @@ import { } from "@app/components/ui/popover"; import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; import { cn } from "@app/lib/cn"; +import { SquareArrowOutUpRight } from "lucide-react"; +import CopyTextBox from "@app/components/CopyTextBox"; +import Link from "next/link"; const baseResourceFormSchema = z.object({ name: z.string().min(1).max(255), @@ -108,6 +111,8 @@ export default function Page() { { domainId: string; baseDomain: string }[] >([]); const [createLoading, setCreateLoading] = useState(false); + const [showSnippets, setShowSnippets] = useState(false); + const [resourceId, setResourceId] = useState(null); const resourceTypes: ReadonlyArray = [ { @@ -202,7 +207,14 @@ export default function Page() { if (res && res.status === 201) { const id = res.data.data.resourceId; - router.push(`/${orgId}/settings/resources/${id}`); + setResourceId(id); + + if (isHttp) { + router.push(`/${orgId}/settings/resources/${id}`); + } else { + setShowSnippets(true); + router.refresh(); + } } } catch (e) { console.error("Error creating resource:", e); @@ -301,466 +313,132 @@ export default function Page() { {!loadingPage && (
- - - - - Resource Information - - - - -
- - ( - - - Name - - - - - - - This is the display - name for the - resource. - - - )} - /> - - ( - - - Site - - - - - - - - - - - - - No - site - found. - - - {sites.map( - ( - site - ) => ( - { - baseForm.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - - This site will - provide connectivity - to the resource. - - - )} - /> - - -
-
-
- - - - - Resource Type - - - Determine how you want to access your - resource - - - - { - baseForm.setValue( - "http", - value === "http" - ); - }} - cols={2} - /> - - - - {baseForm.watch("http") ? ( + {!showSnippets ? ( + - HTTPS Settings + Resource Information - - Configure how your resource will be - accessed over HTTPS - -
+ - {env.flags - .allowBaseDomainResources && ( - ( - - - Domain Type - - - - - )} - /> - )} - - {!httpForm.watch( - "isBaseDomain" - ) && ( - - - Subdomain - -
-
- ( - - - - - - - )} - /> -
-
- ( - - - - - )} - /> -
-
- - The subdomain where - your resource will - be accessible. - -
- )} - - {httpForm.watch( - "isBaseDomain" - ) && ( - ( - - - Base Domain - - - - - )} - /> - )} - - -
-
-
- ) : ( - - - - TCP/UDP Settings - - - Configure how your resource will be - accessed over TCP/UDP - - - - -
- - ( - Protocol + Name - + + + + + This is the + display name for + the resource. + )} /> ( - + - Port Number + Site - - - field.onChange( - e - .target - .value - ? parseInt( - e - .target - .value + + + + + + + + + + + + No + site + found. + + + {sites.map( + ( + site + ) => ( + { + baseForm.setValue( + "siteId", + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + - The external - port number to - proxy requests. + This site will + provide + connectivity to + the resource. )} @@ -770,37 +448,461 @@ export default function Page() { - )} - -
- - -
+ {baseForm.watch("http") ? ( + + + + HTTPS Settings + + + Configure how your resource will be + accessed over HTTPS + + + + + + + {env.flags + .allowBaseDomainResources && ( + ( + + + Domain + Type + + + + + )} + /> + )} + + {!httpForm.watch( + "isBaseDomain" + ) && ( + + + Subdomain + +
+
+ ( + + + + + + + )} + /> +
+
+ ( + + + + + )} + /> +
+
+ + The subdomain + where your + resource will be + accessible. + +
+ )} + + {httpForm.watch( + "isBaseDomain" + ) && ( + ( + + + Base + Domain + + + + + )} + /> + )} + + +
+
+
+ ) : ( + + + + TCP/UDP Settings + + + Configure how your resource will be + accessed over TCP/UDP + + + + +
+ + ( + + + Protocol + + + + + )} + /> + + ( + + + Port Number + + + + field.onChange( + e + .target + .value + ? parseInt( + e + .target + .value + ) + : undefined + ) + } + /> + + + + The external + port number + to proxy + requests. + + + )} + /> + + +
+
+
+ )} + +
+ + +
+
+ ) : ( + + + + + Configuration Snippets + + + Copy and paste these configuration snippets to set up your TCP/UDP resource + + + +
+
+

+ Traefik: Add Entrypoints +

+ +
+ +
+

+ Gerbil: Expose Ports in Docker Compose +

+ +
+ + + + Learn how to configure TCP/UDP resources + + + +
+
+
+ +
+ + +
+
+ )}
)} diff --git a/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx b/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx index bc8701ad..871f0ca0 100644 --- a/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx +++ b/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx @@ -57,9 +57,7 @@ import { import { CheckIcon, ChevronsUpDown } from "lucide-react"; import { Checkbox } from "@app/components/ui/checkbox"; import { GenerateAccessTokenResponse } from "@server/routers/accessToken"; -import { - constructShareLink -} from "@app/lib/shareLinks"; +import { constructShareLink } from "@app/lib/shareLinks"; import { ShareLinkRow } from "./ShareLinksTable"; import { QRCodeCanvas, QRCodeSVG } from "qrcode.react"; import { @@ -528,11 +526,9 @@ export default function CreateShareLinkForm({ accessTokenId } token={accessToken} - resourceUrl={ - form.getValues( - "resourceUrl" - ) - } + resourceUrl={form.getValues( + "resourceUrl" + )} /> diff --git a/src/app/components/LicenseViolation.tsx b/src/app/components/LicenseViolation.tsx index 6e1a58be..1771475c 100644 --- a/src/app/components/LicenseViolation.tsx +++ b/src/app/components/LicenseViolation.tsx @@ -33,9 +33,10 @@ export default function LicenseViolation() { return (

- License Violation: Using {licenseStatus.usedSites} sites - exceeds your licensed limit of {licenseStatus.maxSites}{" "} - sites. Follow license terms to continue using all features. + License Violation: This server is using{" "} + {licenseStatus.usedSites} sites which exceeds its licensed + limit of {licenseStatus.maxSites} sites. Follow license + terms to continue using all features.

); From b3026ba66383be9259d2fb23fe743357808003ec Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 28 Apr 2025 21:48:04 -0400 Subject: [PATCH 48/53] update package-lock --- package-lock.json | 3805 +-------------------------------------------- 1 file changed, 1 insertion(+), 3804 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4801202c..f7682c5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13159,3807 +13159,4 @@ "node_modules/object.assign": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openapi3-ts": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", - "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", - "license": "MIT", - "dependencies": { - "yaml": "^2.5.0" - } - }, - "node_modules/optimist": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==", - "license": "MIT/X11", - "dependencies": { - "wordwrap": "~0.0.2" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/oslo": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/oslo/-/oslo-1.2.1.tgz", - "integrity": "sha512-HfIhB5ruTdQv0XX2XlncWQiJ5SIHZ7NHZhVyHth0CSZ/xzge00etRyYy/3wp/Dsu+PkxMC+6+B2lS/GcKoewkA==", - "deprecated": "Package is no longer supported. Please see https://oslojs.dev for the successor project.", - "license": "MIT", - "dependencies": { - "@node-rs/argon2": "1.7.0", - "@node-rs/bcrypt": "1.9.0" - } - }, - "node_modules/oslo/node_modules/@emnapi/core": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", - "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/oslo/node_modules/@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-1.7.0.tgz", - "integrity": "sha512-zfULc+/tmcWcxn+nHkbyY8vP3+MpEqKORbszt4UkpqZgBgDAAIYvuDN/zukfTgdmo6tmJKKVfzigZOPk4LlIog==", - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@node-rs/argon2-android-arm-eabi": "1.7.0", - "@node-rs/argon2-android-arm64": "1.7.0", - "@node-rs/argon2-darwin-arm64": "1.7.0", - "@node-rs/argon2-darwin-x64": "1.7.0", - "@node-rs/argon2-freebsd-x64": "1.7.0", - "@node-rs/argon2-linux-arm-gnueabihf": "1.7.0", - "@node-rs/argon2-linux-arm64-gnu": "1.7.0", - "@node-rs/argon2-linux-arm64-musl": "1.7.0", - "@node-rs/argon2-linux-x64-gnu": "1.7.0", - "@node-rs/argon2-linux-x64-musl": "1.7.0", - "@node-rs/argon2-wasm32-wasi": "1.7.0", - "@node-rs/argon2-win32-arm64-msvc": "1.7.0", - "@node-rs/argon2-win32-ia32-msvc": "1.7.0", - "@node-rs/argon2-win32-x64-msvc": "1.7.0" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-android-arm-eabi": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.7.0.tgz", - "integrity": "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-android-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.7.0.tgz", - "integrity": "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-darwin-arm64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-1.7.0.tgz", - "integrity": "sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-darwin-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.7.0.tgz", - "integrity": "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-freebsd-x64": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.7.0.tgz", - "integrity": "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm-gnueabihf": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.7.0.tgz", - "integrity": "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-gnu": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.7.0.tgz", - "integrity": "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.7.0.tgz", - "integrity": "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-gnu": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-1.7.0.tgz", - "integrity": "sha512-EmgqZOlf4Jurk/szW1iTsVISx25bKksVC5uttJDUloTgsAgIGReCpUUO1R24pBhu9ESJa47iv8NSf3yAfGv6jQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-musl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.7.0.tgz", - "integrity": "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-wasm32-wasi": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz", - "integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^0.45.0", - "@emnapi/runtime": "^0.45.0", - "@tybys/wasm-util": "^0.8.1", - "memfs-browser": "^3.4.13000" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-win32-arm64-msvc": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz", - "integrity": "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-win32-ia32-msvc": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.7.0.tgz", - "integrity": "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@node-rs/argon2-win32-x64-msvc": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.7.0.tgz", - "integrity": "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/oslo/node_modules/@tybys/wasm-util": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", - "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parseley": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", - "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", - "license": "MIT", - "dependencies": { - "leac": "^0.6.0", - "peberminta": "^0.9.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/peberminta": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", - "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", - "license": "MIT", - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plimit-lit": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", - "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "queue-lit": "^1.5.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qrcode.react": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", - "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", - "license": "ISC", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-lit": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", - "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.25.0" - }, - "peerDependencies": { - "react": "^19.0.0" - } - }, - "node_modules/react-easy-sort": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/react-easy-sort/-/react-easy-sort-1.6.0.tgz", - "integrity": "sha512-zd9Nn90wVlZPEwJrpqElN87sf9GZnFR1StfjgNQVbSpR5QTSzCHjEYK6REuwq49Ip+76KOMSln9tg/ST2KLelg==", - "license": "MIT", - "dependencies": { - "array-move": "^3.0.1", - "tslib": "2.0.1" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "react": ">=16.4.0", - "react-dom": ">=16.4.0" - } - }, - "node_modules/react-easy-sort/node_modules/tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", - "license": "0BSD" - }, - "node_modules/react-email": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.0.6.tgz", - "integrity": "sha512-RzMDZCRd2JFFkGljhBWNWGH2ti4Qnhcx03nR1uPW1vNBptqDJx/fxSJqzCDYEEpTkWPaEe2unHM4CdzRAI7awg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "7.24.5", - "@babel/traverse": "7.25.6", - "chalk": "4.1.2", - "chokidar": "4.0.3", - "commander": "11.1.0", - "debounce": "2.0.0", - "esbuild": "0.25.0", - "glob": "10.3.4", - "log-symbols": "4.1.0", - "mime-types": "2.1.35", - "next": "15.2.4", - "normalize-path": "3.0.0", - "ora": "5.4.1", - "socket.io": "4.8.1" - }, - "bin": { - "email": "dist/cli/index.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/react-email/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/react-email/node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" - } - }, - "node_modules/react-email/node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/react-email/node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/react-email/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/react-email/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/react-email/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/react-hook-form": { - "version": "7.54.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", - "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" - } - }, - "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-promise-suspense": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", - "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^2.0.1" - } - }, - "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "license": "MIT" - }, - "node_modules/react-remove-scroll": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", - "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rebuild": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/rebuild/-/rebuild-0.1.2.tgz", - "integrity": "sha512-EtDZ5IapND57htCrOOcfH7MzXCQKivzSZUIZIuc8H0xDHfmi9HDBZIyjT7Neh5GcUoxQ6hfsXluC+UrYLgGbZg==", - "dependencies": { - "optimist": "0.3.x" - }, - "bin": { - "rebuild": "cli.js" - }, - "engines": { - "node": ">=0.8.8" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", - "license": "MIT" - }, - "node_modules/selderee": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", - "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", - "license": "MIT", - "dependencies": { - "parseley": "^0.12.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", - "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "license": "MIT", - "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 - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", - "license": "MIT" - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.includes": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", - "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/swagger-ui-dist": { - "version": "5.20.8", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.20.8.tgz", - "integrity": "sha512-VXVhdRh5vuKVpkegw0n0wCXuFFG+pxNmXa0vDcf76r1yP2cYKqcpOE5g8l5crbtLGx+j2EiDnG4/EU5T0jyN1w==", - "license": "Apache-2.0", - "dependencies": { - "@scarf/scarf": "=1.4.0" - } - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "license": "MIT", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, - "node_modules/tailwind-merge": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", - "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz", - "integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==", - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", - "license": "MIT", - "dependencies": { - "fdir": "^6.4.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tsc-alias": { - "version": "1.8.10", - "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.10.tgz", - "integrity": "sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.3", - "commander": "^9.0.0", - "globby": "^11.0.4", - "mylas": "^2.1.9", - "normalize-path": "^3.0.0", - "plimit-lit": "^1.2.6" - }, - "bin": { - "tsc-alias": "dist/bin/index.js" - } - }, - "node_modules/tsc-alias/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/tsc-alias/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/tsc-alias/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tsc-alias/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/tsc-alias/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "license": "MIT", - "engines": { - "node": ">=0.6.x" - } - }, - "node_modules/tsx": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", - "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tw-animate-css": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz", - "integrity": "sha512-ABzjfgVo+fDbhRREGL4KQZUqqdPgvc5zVrLyeW9/6mVqvaDepXc7EvedA+pYmMnIOsUAQMwcWzNvom26J2qYvQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Wombosvideo" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unrs-resolver": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.5.0.tgz", - "integrity": "sha512-6aia3Oy7SEe0MuUGQm2nsyob0L2+g57w178K5SE/3pvSGAIp28BB2O921fKx424Ahc/gQ6v0DXFbhcpyhGZdOA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/JounQin" - }, - "optionalDependencies": { - "@unrs/resolver-binding-darwin-arm64": "1.5.0", - "@unrs/resolver-binding-darwin-x64": "1.5.0", - "@unrs/resolver-binding-freebsd-x64": "1.5.0", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.5.0", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.5.0", - "@unrs/resolver-binding-linux-arm64-gnu": "1.5.0", - "@unrs/resolver-binding-linux-arm64-musl": "1.5.0", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.5.0", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.5.0", - "@unrs/resolver-binding-linux-s390x-gnu": "1.5.0", - "@unrs/resolver-binding-linux-x64-gnu": "1.5.0", - "@unrs/resolver-binding-linux-x64-musl": "1.5.0", - "@unrs/resolver-binding-wasm32-wasi": "1.5.0", - "@unrs/resolver-binding-win32-arm64-msvc": "1.5.0", - "@unrs/resolver-binding-win32-ia32-msvc": "1.5.0", - "@unrs/resolver-binding-win32-x64-msvc": "1.5.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vaul": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", - "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-dialog": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-daily-rotate-file": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", - "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", - "license": "MIT", - "dependencies": { - "file-stream-rotator": "^0.6.1", - "object-hash": "^3.0.0", - "triple-beam": "^1.4.1", - "winston-transport": "^4.7.0" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "winston": "^3" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "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 - } - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", - "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.18.0" - } - } - } -} + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw \ No newline at end of file From e2363641246f3f0f18318beae08d5a44489135e7 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 28 Apr 2025 21:50:48 -0400 Subject: [PATCH 49/53] Change api --- server/lib/config.ts | 2 +- server/license/license.ts | 4 ++-- server/routers/supporterKey/validateSupporterKey.ts | 2 +- src/components/SupporterStatus.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/lib/config.ts b/server/lib/config.ts index f2ae7afc..a19b4a2a 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -323,7 +323,7 @@ export class Config { try { const response = await fetch( - "https://api.dev.fossorial.io/api/v1/license/validate", + "https://api.fossorial.io/api/v1/license/validate", { method: "POST", headers: { diff --git a/server/license/license.ts b/server/license/license.ts index 3f01effd..15941679 100644 --- a/server/license/license.ts +++ b/server/license/license.ts @@ -63,9 +63,9 @@ type TokenPayload = { export class License { private phoneHomeInterval = 6 * 60 * 60; // 6 hours = 6 * 60 * 60 = 21600 seconds private validationServerUrl = - "https://api.dev.fossorial.io/api/v1/license/professional/validate"; + "https://api.fossorial.io/api/v1/license/professional/validate"; private activationServerUrl = - "https://api.dev.fossorial.io/api/v1/license/professional/activate"; + "https://api.fossorial.io/api/v1/license/professional/activate"; private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval }); private licenseKeyCache = new NodeCache(); diff --git a/server/routers/supporterKey/validateSupporterKey.ts b/server/routers/supporterKey/validateSupporterKey.ts index 0f023ea6..fadcdc39 100644 --- a/server/routers/supporterKey/validateSupporterKey.ts +++ b/server/routers/supporterKey/validateSupporterKey.ts @@ -44,7 +44,7 @@ export async function validateSupporterKey( const { githubUsername, key } = parsedBody.data; const response = await fetch( - "https://api.dev.fossorial.io/api/v1/license/validate", + "https://api.fossorial.io/api/v1/license/validate", { method: "POST", headers: { diff --git a/src/components/SupporterStatus.tsx b/src/components/SupporterStatus.tsx index baeeb545..25da9b5f 100644 --- a/src/components/SupporterStatus.tsx +++ b/src/components/SupporterStatus.tsx @@ -204,7 +204,7 @@ export default function SupporterStatus() { Payments are processed via GitHub. Afterward, you can retrieve your key on{" "} Date: Mon, 28 Apr 2025 21:55:57 -0400 Subject: [PATCH 50/53] Add secret --- install/config/config.yml | 1 + install/main.go | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/install/config/config.yml b/install/config/config.yml index de406ee9..f7d4552d 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -22,6 +22,7 @@ server: id: "P-Access-Token-Id" token: "P-Access-Token" resource_session_request_param: "p_session_request" + secret: {{.Secret}} cors: origins: ["https://{{.DashboardDomain}}"] methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] diff --git a/install/main.go b/install/main.go index 8411e304..82f7902e 100644 --- a/install/main.go +++ b/install/main.go @@ -16,6 +16,7 @@ import ( "text/template" "time" "unicode" + "math/rand" "golang.org/x/term" ) @@ -50,6 +51,7 @@ type Config struct { InstallGerbil bool TraefikBouncerKey string DoCrowdsecInstall bool + Secret string } func main() { @@ -63,6 +65,7 @@ func main() { var config Config config.DoCrowdsecInstall = false + config.Secret = generateRandomSecretKey() // check if there is already a config file if _, err := os.Stat("config/config.yml"); err != nil { @@ -603,3 +606,17 @@ func waitForContainer(containerName string) error { return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds())) } + +func generateRandomSecretKey() string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + const length = 32 + + var seededRand *rand.Rand = rand.New( + rand.NewSource(time.Now().UnixNano())) + + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} \ No newline at end of file From cffc156cf6c2103b16bb7127694d97c8c4ca889f Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 28 Apr 2025 22:05:04 -0400 Subject: [PATCH 51/53] manually merge wg qr code --- .../[orgId]/settings/sites/create/page.tsx | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 23b917ba..38c8a772 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -29,14 +29,29 @@ import { InfoIcon, Terminal } from "lucide-react"; import { Button } from "@app/components/ui/button"; import CopyTextBox from "@app/components/CopyTextBox"; import CopyToClipboard from "@app/components/CopyToClipboard"; -import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; -import { FaApple, FaCubes, FaDocker, FaFreebsd, FaWindows } from "react-icons/fa"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import { + FaApple, + FaCubes, + FaDocker, + FaFreebsd, + FaWindows +} from "react-icons/fa"; import { Checkbox } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { generateKeypair } from "../[niceId]/wireguardConfig"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { CreateSiteBody, CreateSiteResponse, PickSiteDefaultsResponse } from "@server/routers/site"; +import { + CreateSiteBody, + CreateSiteResponse, + PickSiteDefaultsResponse +} from "@server/routers/site"; import { toast } from "@app/hooks/useToast"; import { AxiosResponse } from "axios"; import { useParams, useRouter } from "next/navigation"; @@ -48,6 +63,7 @@ import { BreadcrumbSeparator } from "@app/components/ui/breadcrumb"; import Link from "next/link"; +import { QRCodeCanvas } from "qrcode.react"; const createSiteFormSchema = z .object({ @@ -101,7 +117,7 @@ const platforms = [ "freebsd" ] as const; -type Platform = typeof platforms[number]; +type Platform = (typeof platforms)[number]; export default function Page() { const { env } = useEnvContext(); @@ -725,7 +741,9 @@ WantedBy=default.target`

- {["docker", "podman"].includes(platform) + {["docker", "podman"].includes( + platform + ) ? "Method" : "Architecture"}

@@ -783,8 +801,20 @@ WantedBy=default.target` - - +
+ +
+
+ +
+
+
From 81adcd9234ed6343641bee7579b75d01900ce56d Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 28 Apr 2025 22:06:13 -0400 Subject: [PATCH 52/53] add async --- server/license/license.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/license/license.ts b/server/license/license.ts index 15941679..7887f451 100644 --- a/server/license/license.ts +++ b/server/license/license.ts @@ -90,8 +90,8 @@ LQIDAQAB ).toString("base64"); setInterval( - () => { - this.check(); + async () => { + await this.check(); }, 1000 * 60 * 60 ); // 1 hour = 60 * 60 = 3600 seconds From 3ebc01df8cf93f93c2571818787783aacc2e872c Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Mon, 28 Apr 2025 23:07:11 -0400 Subject: [PATCH 53/53] add migration --- package-lock.json | 5357 ++++++++++++----- server/db/schemas/schema.ts | 2 +- server/setup/scripts/1.3.0.ts | 204 +- .../components/SitePriceCalculator.tsx | 2 +- src/app/setup/layout.tsx | 24 +- 5 files changed, 4128 insertions(+), 1461 deletions(-) diff --git a/package-lock.json b/package-lock.json index f7682c5a..c6da9176 100644 --- a/package-lock.json +++ b/package-lock.json @@ -369,9 +369,9 @@ "license": "Apache-2.0" }, "node_modules/@ecies/ciphers": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.3.tgz", - "integrity": "sha512-tapn6XhOueMwht3E2UzY0ZZjYokdaw9XtL9kEyjhQ/Fb9vL9xTFbOaI+fV0AWvTpYu4BNloC6getKW6NtSg4mA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.2.tgz", + "integrity": "sha512-ylfGR7PyTd+Rm2PqQowG08BCKA22QuX8NzrL+LxAAvazN10DMwdJ2fWwAzRj05FI/M8vNFGm3cv9Wq/GFWCBLg==", "dev": true, "license": "MIT", "engines": { @@ -383,31 +383,11 @@ "@noble/ciphers": "^1.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.1.tgz", - "integrity": "sha512-4JFstCTaToCFrPqrGzgkF8N2NHjtsaY4uRh6brZQ5L9e4wbMieX8oDT8N7qfVFTQecHFEtkj4ve49VIZ3mKVqw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.1", - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/runtime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.1.tgz", - "integrity": "sha512-LMshMVP0ZhACNjQNYXiU1iZJ6QCcv0lUdPDPugqGvCGXt5xtRVBPdtA0qU12pEXZzpWAhWlZYptfdAFq10DOVQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", - "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1292,9 +1272,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz", - "integrity": "sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -1331,12 +1311,12 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.5", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -1357,9 +1337,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -1389,21 +1369,21 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.10.0", "levn": "^0.4.1" }, "engines": { @@ -1411,9 +1391,9 @@ } }, "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -1518,9 +1498,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -1552,28 +1532,6 @@ "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", @@ -1590,307 +1548,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1961,18 +1618,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz", - "integrity": "sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.0", - "@emnapi/runtime": "^1.4.0", - "@tybys/wasm-util": "^0.9.0" - } - }, "node_modules/@next/env": { "version": "15.2.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", @@ -2183,38 +1828,6 @@ "@node-rs/argon2-win32-x64-msvc": "2.0.2" } }, - "node_modules/@node-rs/argon2-android-arm-eabi": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz", - "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-android-arm64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz", - "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@node-rs/argon2-darwin-arm64": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz", @@ -2231,182 +1844,6 @@ "node": ">= 10" } }, - "node_modules/@node-rs/argon2-darwin-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz", - "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-freebsd-x64": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz", - "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-linux-arm-gnueabihf": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz", - "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-linux-arm64-gnu": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz", - "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-linux-arm64-musl": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz", - "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-linux-x64-gnu": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz", - "integrity": "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-linux-x64-musl": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz", - "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-wasm32-wasi": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz", - "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@node-rs/argon2-win32-arm64-msvc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz", - "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-win32-ia32-msvc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz", - "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/argon2-win32-x64-msvc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz", - "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@node-rs/bcrypt": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz", @@ -2436,38 +1873,6 @@ "@node-rs/bcrypt-win32-x64-msvc": "1.9.0" } }, - "node_modules/@node-rs/bcrypt-android-arm-eabi": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm-eabi/-/bcrypt-android-arm-eabi-1.9.0.tgz", - "integrity": "sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-android-arm64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm64/-/bcrypt-android-arm64-1.9.0.tgz", - "integrity": "sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@node-rs/bcrypt-darwin-arm64": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-arm64/-/bcrypt-darwin-arm64-1.9.0.tgz", @@ -2484,215 +1889,6 @@ "node": ">= 10" } }, - "node_modules/@node-rs/bcrypt-darwin-x64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-x64/-/bcrypt-darwin-x64-1.9.0.tgz", - "integrity": "sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-freebsd-x64": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-freebsd-x64/-/bcrypt-freebsd-x64-1.9.0.tgz", - "integrity": "sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm-gnueabihf": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm-gnueabihf/-/bcrypt-linux-arm-gnueabihf-1.9.0.tgz", - "integrity": "sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm64-gnu": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-gnu/-/bcrypt-linux-arm64-gnu-1.9.0.tgz", - "integrity": "sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-arm64-musl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-musl/-/bcrypt-linux-arm64-musl-1.9.0.tgz", - "integrity": "sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-x64-gnu": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-gnu/-/bcrypt-linux-x64-gnu-1.9.0.tgz", - "integrity": "sha512-DyyhDHDsLBsCKz1tZ1hLvUZSc1DK0FU0v52jK6IBQxrj24WscSU9zZe7ie/V9kdmA4Ep57BfpWX8Dsa2JxGdgQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-linux-x64-musl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-musl/-/bcrypt-linux-x64-musl-1.9.0.tgz", - "integrity": "sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-wasm32-wasi/-/bcrypt-wasm32-wasi-1.9.0.tgz", - "integrity": "sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^0.45.0", - "@emnapi/runtime": "^0.45.0", - "@tybys/wasm-util": "^0.8.1", - "memfs-browser": "^3.4.13000" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/core": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", - "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", - "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@node-rs/bcrypt-win32-arm64-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-arm64-msvc/-/bcrypt-win32-arm64-msvc-1.9.0.tgz", - "integrity": "sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-win32-ia32-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-ia32-msvc/-/bcrypt-win32-ia32-msvc-1.9.0.tgz", - "integrity": "sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@node-rs/bcrypt-win32-x64-msvc": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-x64-msvc/-/bcrypt-win32-x64-msvc-1.9.0.tgz", - "integrity": "sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4123,9 +3319,9 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", - "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.5.tgz", + "integrity": "sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==", "license": "MIT" }, "node_modules/@scarf/scarf": { @@ -4183,52 +3379,46 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.3.tgz", - "integrity": "sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz", + "integrity": "sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw==", "dev": true, "license": "MIT", "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", - "tailwindcss": "4.1.3" + "tailwindcss": "4.1.4" } }, - "node_modules/@tailwindcss/node/node_modules/tailwindcss": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.3.tgz", - "integrity": "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==", - "dev": true, - "license": "MIT" - }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.3.tgz", - "integrity": "sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.4.tgz", + "integrity": "sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ==", "dev": true, "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.3", - "@tailwindcss/oxide-darwin-arm64": "4.1.3", - "@tailwindcss/oxide-darwin-x64": "4.1.3", - "@tailwindcss/oxide-freebsd-x64": "4.1.3", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.3", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.3", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.3", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.3", - "@tailwindcss/oxide-linux-x64-musl": "4.1.3", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.3", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.3" + "@tailwindcss/oxide-android-arm64": "4.1.4", + "@tailwindcss/oxide-darwin-arm64": "4.1.4", + "@tailwindcss/oxide-darwin-x64": "4.1.4", + "@tailwindcss/oxide-freebsd-x64": "4.1.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.4", + "@tailwindcss/oxide-linux-x64-musl": "4.1.4", + "@tailwindcss/oxide-wasm32-wasi": "4.1.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.4" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.3.tgz", - "integrity": "sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.4.tgz", + "integrity": "sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==", "cpu": [ "arm64" ], @@ -4243,9 +3433,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.3.tgz", - "integrity": "sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.4.tgz", + "integrity": "sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg==", "cpu": [ "arm64" ], @@ -4260,9 +3450,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.3.tgz", - "integrity": "sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.4.tgz", + "integrity": "sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA==", "cpu": [ "x64" ], @@ -4277,9 +3467,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.3.tgz", - "integrity": "sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.4.tgz", + "integrity": "sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA==", "cpu": [ "x64" ], @@ -4294,9 +3484,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.3.tgz", - "integrity": "sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.4.tgz", + "integrity": "sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg==", "cpu": [ "arm" ], @@ -4311,9 +3501,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.3.tgz", - "integrity": "sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.4.tgz", + "integrity": "sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww==", "cpu": [ "arm64" ], @@ -4328,9 +3518,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.3.tgz", - "integrity": "sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.4.tgz", + "integrity": "sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw==", "cpu": [ "arm64" ], @@ -4345,9 +3535,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.3.tgz", - "integrity": "sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.4.tgz", + "integrity": "sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ==", "cpu": [ "x64" ], @@ -4362,9 +3552,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.3.tgz", - "integrity": "sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.4.tgz", + "integrity": "sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ==", "cpu": [ "x64" ], @@ -4378,10 +3568,40 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.4.tgz", + "integrity": "sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@emnapi/wasi-threads": "^1.0.1", + "@napi-rs/wasm-runtime": "^0.2.8", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.3.tgz", - "integrity": "sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.4.tgz", + "integrity": "sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng==", "cpu": [ "arm64" ], @@ -4396,9 +3616,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.3.tgz", - "integrity": "sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.4.tgz", + "integrity": "sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw==", "cpu": [ "x64" ], @@ -4413,26 +3633,19 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.3.tgz", - "integrity": "sha512-6s5nJODm98F++QT49qn8xJKHQRamhYHfMi3X7/ltxiSQ9dyRsaFSfFkfaMsanWzf+TMYQtbk8mt5f6cCVXJwfg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.4.tgz", + "integrity": "sha512-bjV6sqycCEa+AQSt2Kr7wpGF1bOZJ5wsqnLEkqSbM/JEHxx/yhMH8wHmdkPyApF9xhHeMSwnnkDUUMMM/hYnXw==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.3", - "@tailwindcss/oxide": "4.1.3", + "@tailwindcss/node": "4.1.4", + "@tailwindcss/oxide": "4.1.4", "postcss": "^8.4.41", - "tailwindcss": "4.1.3" + "tailwindcss": "4.1.4" } }, - "node_modules/@tailwindcss/postcss/node_modules/tailwindcss": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.3.tgz", - "integrity": "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==", - "dev": true, - "license": "MIT" - }, "node_modules/@tanstack/react-table": { "version": "8.20.6", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz", @@ -4466,16 +3679,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/better-sqlite3": { "version": "7.6.12", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", @@ -4535,9 +3738,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, "node_modules/@types/express": { @@ -4554,9 +3757,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", - "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz", + "integrity": "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==", "dev": true, "license": "MIT", "dependencies": { @@ -4625,13 +3828,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "version": "22.10.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", + "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~6.20.0" } }, "node_modules/@types/nodemailer": { @@ -4753,20 +3956,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz", - "integrity": "sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz", + "integrity": "sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==", "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.29.1", - "@typescript-eslint/type-utils": "8.29.1", - "@typescript-eslint/utils": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/type-utils": "8.21.0", + "@typescript-eslint/utils": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4778,19 +3981,19 @@ "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.1.tgz", - "integrity": "sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.21.0.tgz", + "integrity": "sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==", "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.29.1", - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/typescript-estree": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1", + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "debug": "^4.3.4" }, "engines": { @@ -4802,17 +4005,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz", - "integrity": "sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz", + "integrity": "sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1" + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4823,15 +4026,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz", - "integrity": "sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz", + "integrity": "sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.29.1", - "@typescript-eslint/utils": "8.29.1", + "@typescript-eslint/typescript-estree": "8.21.0", + "@typescript-eslint/utils": "8.21.0", "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4842,13 +4045,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.1.tgz", - "integrity": "sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.21.0.tgz", + "integrity": "sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4859,19 +4062,19 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz", - "integrity": "sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz", + "integrity": "sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/visitor-keys": "8.29.1", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/visitor-keys": "8.21.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4881,7 +4084,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -4937,15 +4140,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.1.tgz", - "integrity": "sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.21.0.tgz", + "integrity": "sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.29.1", - "@typescript-eslint/types": "8.29.1", - "@typescript-eslint/typescript-estree": "8.29.1" + "@typescript-eslint/scope-manager": "8.21.0", + "@typescript-eslint/types": "8.21.0", + "@typescript-eslint/typescript-estree": "8.21.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4956,16 +4159,16 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.29.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz", - "integrity": "sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz", + "integrity": "sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.1", + "@typescript-eslint/types": "8.21.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -4976,217 +4179,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.5.0.tgz", - "integrity": "sha512-YmocNlEcX/AgJv8gI41bhjMOTcKcea4D2nRIbZj+MhRtSH5+vEU8r/pFuTuoF+JjVplLsBueU+CILfBPVISyGQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.5.0.tgz", - "integrity": "sha512-qpUrXgH4e/0xu1LOhPEdfgSY3vIXOxDQv370NEL8npN8h40HcQDA+Pl2r4HBW6tTXezWIjxUFcP7tj529RZtDw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.5.0.tgz", - "integrity": "sha512-3tX8r8vgjvZzaJZB4jvxUaaFCDCb3aWDCpZN3EjhGnnwhztslI05KSG5NY/jNjlcZ5QWZ7dEZZ/rNBFsmTaSPw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.5.0.tgz", - "integrity": "sha512-FH+ixzBKaUU9fWOj3TYO+Yn/eO6kYvMLV9eNJlJlkU7OgrxkCmiMS6wUbyT0KA3FOZGxnEQ2z3/BHgYm2jqeLA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.5.0.tgz", - "integrity": "sha512-pxCgXMgwB/4PfqFQg73lMhmWwcC0j5L+dNXhZoz/0ek0iS/oAWl65fxZeT/OnU7fVs52MgdP2q02EipqJJXHSg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.5.0.tgz", - "integrity": "sha512-FX2FV7vpLE/+Z0NZX9/1pwWud5Wocm/2PgpUXbT5aSV3QEB10kBPJAzssOQylvdj8mOHoKl5pVkXpbCwww/T2g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.5.0.tgz", - "integrity": "sha512-+gF97xst1BZb28T3nwwzEtq2ewCoMDGKsenYsZuvpmNrW0019G1iUAunZN+FG55L21y+uP7zsGX06OXDQ/viKw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.5.0.tgz", - "integrity": "sha512-5bEmVcQw9js8JYM2LkUBw5SeELSIxX+qKf9bFrfFINKAp4noZ//hUxLpbF7u/3gTBN1GsER6xOzIZlw/VTdXtA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.5.0.tgz", - "integrity": "sha512-GGk/8TPUsf1Q99F+lzMdjE6sGL26uJCwQ9TlvBs8zR3cLQNw/MIumPN7zrs3GFGySjnwXc8gA6J3HKbejywmqA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.5.0.tgz", - "integrity": "sha512-5uRkFYYVNAeVaA4W/CwugjFN3iDOHCPqsBLCCOoJiMfFMMz4evBRsg+498OFa9w6VcTn2bD5aI+RRayaIgk2Sw==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.5.0.tgz", - "integrity": "sha512-j905CZH3nehYy6NimNqC2B14pxn4Ltd7guKMyPTzKehbFXTUgihQS/ZfHQTdojkMzbSwBOSgq1dOrY+IpgxDsA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.5.0.tgz", - "integrity": "sha512-dmLevQTuzQRwu5A+mvj54R5aye5I4PVKiWqGxg8tTaYP2k2oTs/3Mo8mgnhPk28VoYCi0fdFYpgzCd4AJndQvQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.5.0.tgz", - "integrity": "sha512-LtJMhwu7avhoi+kKfAZOKN773RtzLBVVF90YJbB0wyMpUj9yQPeA+mteVUI9P70OG/opH47FeV5AWeaNWWgqJg==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.8" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.5.0.tgz", - "integrity": "sha512-FTZBxLL4SO1mgIM86KykzJmPeTPisBDHQV6xtfDXbTMrentuZ6SdQKJUV5BWaoUK3p8kIULlrCcucqdCnk8Npg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.5.0.tgz", - "integrity": "sha512-i5bB7vJ1waUsFciU/FKLd4Zw0VnAkvhiJ4//jYQXyDUuiLKodmtQZVTcOPU7pp97RrNgCFtXfC1gnvj/DHPJTw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.5.0.tgz", - "integrity": "sha512-wAvXp4k7jhioi4SebXW/yfzzYwsUCr9kIX4gCsUFKpCTUf8Mi7vScJXI3S+kupSUf0LbVHudR8qBbe2wFMSNUw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -5201,9 +4193,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5414,18 +4406,17 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", + "es-abstract": "^1.23.2", "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5550,9 +4541,9 @@ } }, "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", "license": "MPL-2.0", "engines": { "node": ">=4" @@ -5795,9 +4786,9 @@ } }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5808,13 +4799,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -5833,9 +4824,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001713", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001713.tgz", - "integrity": "sha512-wCIWIg+A4Xr7NfhTuHdX+/FKh3+Op3LBbSp2N5Pfx6T/LhdQy3GTyoTg48BReaW/MyMNZAkTadsBtai3ldWK0Q==", + "version": "1.0.30001695", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", + "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", "funding": [ { "type": "opencollective", @@ -6023,9 +5014,10 @@ } }, "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.8" @@ -6441,16 +5433,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defaults/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -6609,9 +5591,9 @@ } }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -7223,9 +6205,9 @@ } }, "node_modules/eciesjs": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.14.tgz", - "integrity": "sha512-eJAgf9pdv214Hn98FlUzclRMYWF7WfoLlkS9nWMTm1qcCwn6Ad4EGD9lr9HXMBfSrZhYQujRE+p0adPRkctC6A==", + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.13.tgz", + "integrity": "sha512-zBdtR4K+wbj10bWPpIOF9DW+eFYQu8miU5ypunh0t4Bvt83ZPlEWgT5Dq/0G6uwEXumZKjfb5BZxYUZQ2Hzn/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7361,7 +6343,6 @@ "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -7534,15 +6515,12 @@ } }, "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", "license": "MIT", "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" + "hasown": "^2.0.0" } }, "node_modules/es-to-primitive": { @@ -7767,24 +6745,25 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.0.tgz", - "integrity": "sha512-aV3/dVsT0/H9BtpNwbaqvl+0xGMRGzncLyhm793NFGvbwGGvzyAykqWZ8oZlZuGwuHkwJjhWJkG1cM3ynvd2pQ==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", "license": "ISC", "dependencies": { "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.12", - "unrs-resolver": "^1.3.2" + "debug": "^4.3.7", + "enhanced-resolve": "^5.15.0", + "fast-glob": "^3.3.2", + "get-tsconfig": "^4.7.5", + "is-bun-module": "^1.0.2", + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" }, "peerDependencies": { "eslint": "*", @@ -7800,6 +6779,34 @@ } } }, + "node_modules/eslint-import-resolver-typescript/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/eslint-module-utils": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", @@ -7907,9 +6914,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", "license": "MIT", "dependencies": { "array-includes": "^3.1.8", @@ -7922,7 +6929,7 @@ "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.9", + "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", @@ -7939,9 +6946,9 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", + "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", "license": "MIT", "engines": { "node": ">=10" @@ -7977,9 +6984,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -8237,9 +7244,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -8249,6 +7256,7 @@ "version": "6.4.3", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -8390,9 +7398,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "license": "ISC" }, "node_modules/fn.name": { @@ -8422,9 +7430,9 @@ } }, "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", + "integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==", "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -8437,12 +7445,12 @@ } }, "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.6", + "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" }, "engines": { @@ -8465,14 +7473,13 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { @@ -8515,13 +7522,6 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/fs-monkey": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", - "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", - "license": "Unlicense", - "optional": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -8607,17 +7607,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", + "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "get-proto": "^1.0.1", + "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -8824,7 +7824,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -9040,9 +8039,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -9180,12 +8179,12 @@ } }, "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", + "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", + "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" }, "engines": { @@ -9196,24 +8195,12 @@ } }, "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", "license": "MIT", "dependencies": { - "semver": "^7.7.1" - } - }, - "node_modules/is-bun-module/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "semver": "^7.6.3" } }, "node_modules/is-callable": { @@ -9517,12 +8504,12 @@ } }, "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", + "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "call-bound": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -9581,9 +8568,9 @@ } }, "node_modules/jackspeak": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", - "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -9663,18 +8650,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "license": "MIT" }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -10149,9 +9124,9 @@ } }, "node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", "license": "ISC", "engines": { "node": "20 || >=22" @@ -10208,29 +9183,6 @@ "node": ">= 0.6" } }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "license": "Unlicense", - "optional": true, - "dependencies": { - "fs-monkey": "^1.0.4" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/memfs-browser": { - "version": "3.5.10302", - "resolved": "https://registry.npmjs.org/memfs-browser/-/memfs-browser-3.5.10302.tgz", - "integrity": "sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw==", - "license": "Unlicense", - "optional": true, - "dependencies": { - "memfs": "3.5.3" - } - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -10420,9 +9372,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -10551,9 +9503,9 @@ } }, "node_modules/node-abi": { - "version": "3.74.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", - "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "version": "3.73.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.73.0.tgz", + "integrity": "sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -10574,6 +9526,15 @@ "node": ">= 8.0.0" } }, + "node_modules/node-cache/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -10631,9 +9592,9 @@ } }, "node_modules/npm": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.3.0.tgz", - "integrity": "sha512-luthFIP0nFX3+nTfYbWI3p4hP4CiVnKOZ5jdxnF2x7B+Shz8feiSJCLLzgJUNxQ2cDdTaVUiH6RRsMT++vIMZg==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.2.0.tgz", + "integrity": "sha512-PcnFC6gTo9VDkxVaQ1/mZAS3JoWrDjAI+a6e2NgfYQSGDwftJlbdV0jBMi2V8xQPqbGcWaa7p3UP0SKF+Bhm2g==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -10712,20 +9673,20 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.0.2", - "@npmcli/config": "^10.2.0", + "@npmcli/arborist": "^9.0.1", + "@npmcli/config": "^10.1.0", "@npmcli/fs": "^4.0.0", "@npmcli/map-workspaces": "^4.0.2", "@npmcli/package-json": "^6.1.1", "@npmcli/promise-spawn": "^8.0.2", "@npmcli/redact": "^3.1.1", - "@npmcli/run-script": "^9.1.0", + "@npmcli/run-script": "^9.0.1", "@sigstore/tuf": "^3.0.0", "abbrev": "^3.0.0", "archy": "~1.0.0", "cacache": "^19.0.1", "chalk": "^5.4.1", - "ci-info": "^4.2.0", + "ci-info": "^4.1.0", "cli-columns": "^4.0.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", @@ -10737,11 +9698,11 @@ "is-cidr": "^5.1.1", "json-parse-even-better-errors": "^4.0.0", "libnpmaccess": "^10.0.0", - "libnpmdiff": "^8.0.2", - "libnpmexec": "^10.1.1", - "libnpmfund": "^7.0.2", + "libnpmdiff": "^8.0.1", + "libnpmexec": "^10.1.0", + "libnpmfund": "^7.0.1", "libnpmorg": "^8.0.0", - "libnpmpack": "^9.0.2", + "libnpmpack": "^9.0.1", "libnpmpublish": "^11.0.0", "libnpmsearch": "^9.0.0", "libnpmteam": "^8.0.0", @@ -10751,7 +9712,7 @@ "minipass": "^7.1.1", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^11.2.0", + "node-gyp": "^11.1.0", "nopt": "^8.1.0", "normalize-package-data": "^7.0.0", "npm-audit-report": "^6.0.0", @@ -10893,7 +9854,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.0.2", + "version": "9.0.1", "inBundle": true, "license": "ISC", "dependencies": { @@ -10940,7 +9901,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.2.0", + "version": "10.1.0", "inBundle": true, "license": "ISC", "dependencies": { @@ -11094,7 +10055,7 @@ } }, "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "9.1.0", + "version": "9.0.2", "inBundle": true, "license": "ISC", "dependencies": { @@ -11321,11 +10282,12 @@ } }, "node_modules/npm/node_modules/cacache/node_modules/minizlib": { - "version": "3.0.2", + "version": "3.0.1", "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^7.1.2" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" }, "engines": { "node": ">= 18" @@ -11389,7 +10351,7 @@ } }, "node_modules/npm/node_modules/ci-info": { - "version": "4.2.0", + "version": "4.1.0", "funding": [ { "type": "github", @@ -11820,11 +10782,11 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.2", + "version": "8.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.0.2", + "@npmcli/arborist": "^9.0.1", "@npmcli/installed-package-contents": "^3.0.0", "binary-extensions": "^3.0.0", "diff": "^7.0.0", @@ -11838,11 +10800,11 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.1.1", + "version": "10.1.0", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.0.2", + "@npmcli/arborist": "^9.0.1", "@npmcli/package-json": "^6.1.1", "@npmcli/run-script": "^9.0.1", "ci-info": "^4.0.0", @@ -11859,11 +10821,11 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.2", + "version": "7.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.0.2" + "@npmcli/arborist": "^9.0.1" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -11882,11 +10844,11 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.2", + "version": "9.0.1", "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.0.2", + "@npmcli/arborist": "^9.0.1", "@npmcli/run-script": "^9.0.1", "npm-package-arg": "^12.0.0", "pacote": "^21.0.0" @@ -12019,7 +10981,7 @@ } }, "node_modules/npm/node_modules/minipass-fetch": { - "version": "4.0.1", + "version": "4.0.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -12035,11 +10997,12 @@ } }, "node_modules/npm/node_modules/minipass-fetch/node_modules/minizlib": { - "version": "3.0.2", + "version": "3.0.1", "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^7.1.2" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" }, "engines": { "node": ">= 18" @@ -12159,19 +11122,19 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "11.2.0", + "version": "11.1.0", "inBundle": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", - "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { @@ -12190,11 +11153,12 @@ } }, "node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { - "version": "3.0.2", + "version": "3.0.1", "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^7.1.2" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" }, "engines": { "node": ">= 18" @@ -12373,11 +11337,12 @@ } }, "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minizlib": { - "version": "3.0.2", + "version": "3.0.1", "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^7.1.2" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" }, "engines": { "node": ">= 18" @@ -12586,6 +11551,20 @@ "node": ">= 4" } }, + "node_modules/npm/node_modules/rimraf": { + "version": "5.0.10", + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/npm/node_modules/safer-buffer": { "version": "2.1.2", "inBundle": true, @@ -12854,45 +11833,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/npm/node_modules/tinyglobby": { - "version": "0.2.12", - "inBundle": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.3", - "inBundle": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/npm/node_modules/treeverse": { "version": "3.0.0", "inBundle": true, @@ -13126,9 +12066,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -13159,4 +12099,3533 @@ "node_modules/object.assign": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw \ No newline at end of file + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi3-ts": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", + "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", + "license": "MIT", + "dependencies": { + "yaml": "^2.5.0" + } + }, + "node_modules/optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==", + "license": "MIT/X11", + "dependencies": { + "wordwrap": "~0.0.2" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/oslo": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/oslo/-/oslo-1.2.1.tgz", + "integrity": "sha512-HfIhB5ruTdQv0XX2XlncWQiJ5SIHZ7NHZhVyHth0CSZ/xzge00etRyYy/3wp/Dsu+PkxMC+6+B2lS/GcKoewkA==", + "deprecated": "Package is no longer supported. Please see https://oslojs.dev for the successor project.", + "license": "MIT", + "dependencies": { + "@node-rs/argon2": "1.7.0", + "@node-rs/bcrypt": "1.9.0" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-1.7.0.tgz", + "integrity": "sha512-zfULc+/tmcWcxn+nHkbyY8vP3+MpEqKORbszt4UkpqZgBgDAAIYvuDN/zukfTgdmo6tmJKKVfzigZOPk4LlIog==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@node-rs/argon2-android-arm-eabi": "1.7.0", + "@node-rs/argon2-android-arm64": "1.7.0", + "@node-rs/argon2-darwin-arm64": "1.7.0", + "@node-rs/argon2-darwin-x64": "1.7.0", + "@node-rs/argon2-freebsd-x64": "1.7.0", + "@node-rs/argon2-linux-arm-gnueabihf": "1.7.0", + "@node-rs/argon2-linux-arm64-gnu": "1.7.0", + "@node-rs/argon2-linux-arm64-musl": "1.7.0", + "@node-rs/argon2-linux-x64-gnu": "1.7.0", + "@node-rs/argon2-linux-x64-musl": "1.7.0", + "@node-rs/argon2-wasm32-wasi": "1.7.0", + "@node-rs/argon2-win32-arm64-msvc": "1.7.0", + "@node-rs/argon2-win32-ia32-msvc": "1.7.0", + "@node-rs/argon2-win32-x64-msvc": "1.7.0" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-darwin-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-1.7.0.tgz", + "integrity": "sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-easy-sort": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-easy-sort/-/react-easy-sort-1.6.0.tgz", + "integrity": "sha512-zd9Nn90wVlZPEwJrpqElN87sf9GZnFR1StfjgNQVbSpR5QTSzCHjEYK6REuwq49Ip+76KOMSln9tg/ST2KLelg==", + "license": "MIT", + "dependencies": { + "array-move": "^3.0.1", + "tslib": "2.0.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, + "node_modules/react-easy-sort/node_modules/tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==", + "license": "0BSD" + }, + "node_modules/react-email": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.0.6.tgz", + "integrity": "sha512-RzMDZCRd2JFFkGljhBWNWGH2ti4Qnhcx03nR1uPW1vNBptqDJx/fxSJqzCDYEEpTkWPaEe2unHM4CdzRAI7awg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.5", + "@babel/traverse": "7.25.6", + "chalk": "4.1.2", + "chokidar": "4.0.3", + "commander": "11.1.0", + "debounce": "2.0.0", + "esbuild": "0.25.0", + "glob": "10.3.4", + "log-symbols": "4.1.0", + "mime-types": "2.1.35", + "next": "15.2.4", + "normalize-path": "3.0.0", + "ora": "5.4.1", + "socket.io": "4.8.1" + }, + "bin": { + "email": "dist/cli/index.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/react-email/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/react-email/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/react-email/node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/react-email/node_modules/glob": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", + "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/react-email/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/react-email/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-hook-form": { + "version": "7.54.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", + "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-promise-suspense": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", + "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rebuild": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/rebuild/-/rebuild-0.1.2.tgz", + "integrity": "sha512-EtDZ5IapND57htCrOOcfH7MzXCQKivzSZUIZIuc8H0xDHfmi9HDBZIyjT7Neh5GcUoxQ6hfsXluC+UrYLgGbZg==", + "dependencies": { + "optimist": "0.3.x" + }, + "bin": { + "rebuild": "cli.js" + }, + "engines": { + "node": ">=0.8.8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "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 + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "license": "MIT" + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz", + "integrity": "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsc-alias": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.10.tgz", + "integrity": "sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + } + }, + "node_modules/tsc-alias/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tsc-alias/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/tsc-alias/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tsc-alias/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tsc-alias/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tw-animate-css": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.8.tgz", + "integrity": "sha512-AxSnYRvyFnAiZCUndS3zQZhNfV/B77ZhJ+O7d3K6wfg/jKJY+yv6ahuyXwnyaYA9UdLqnpCwhTRv9pPTBnPR2g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "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 + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } + } + } +} diff --git a/server/db/schemas/schema.ts b/server/db/schemas/schema.ts index 4fb2416b..ebbc0ce3 100644 --- a/server/db/schemas/schema.ts +++ b/server/db/schemas/schema.ts @@ -104,7 +104,7 @@ export const exitNodes = sqliteTable("exitNodes", { name: text("name").notNull(), address: text("address").notNull(), // this is the address of the wireguard interface in gerbil endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config - publicKey: text("pubicKey").notNull(), + publicKey: text("publicKey").notNull(), listenPort: integer("listenPort").notNull(), reachableAt: text("reachableAt") // this is the internal address of the gerbil http server for command control }); diff --git a/server/setup/scripts/1.3.0.ts b/server/setup/scripts/1.3.0.ts index f837deb7..fdd1b80b 100644 --- a/server/setup/scripts/1.3.0.ts +++ b/server/setup/scripts/1.3.0.ts @@ -1,29 +1,205 @@ -import db from "@server/db"; -import { sql } from "drizzle-orm"; +import Database from "better-sqlite3"; +import path from "path"; +import fs from "fs"; +import yaml from "js-yaml"; +import { encodeBase32LowerCaseNoPadding } from "@oslojs/encoding"; +import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; const version = "1.3.0"; +const location = path.join(APP_PATH, "db", "db.sqlite"); + +await migration(); export default async function migration() { console.log(`Running setup script ${version}...`); - try { - db.transaction((trx) => { - trx.run( - sql`ALTER TABLE resources ADD stickySession integer DEFAULT false NOT NULL;` - ); - trx.run( - sql`ALTER TABLE 'resources' ADD 'tlsServerName' text;` - ); - trx.run( - sql`ALTER TABLE 'resources' ADD 'setHostHeader' text;` - ); - }); + const db = new Database(location); + try { + db.pragma("foreign_keys = OFF"); + db.transaction(() => { + db.exec(` + CREATE TABLE 'apiKeyActions' ( + 'apiKeyId' text NOT NULL, + 'actionId' text NOT NULL, + FOREIGN KEY ('apiKeyId') REFERENCES 'apiKeys'('apiKeyId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('actionId') REFERENCES 'actions'('actionId') ON UPDATE no action ON DELETE cascade + ); + + CREATE TABLE 'apiKeyOrg' ( + 'apiKeyId' text NOT NULL, + 'orgId' text NOT NULL, + FOREIGN KEY ('apiKeyId') REFERENCES 'apiKeys'('apiKeyId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + + CREATE TABLE 'apiKeys' ( + 'apiKeyId' text PRIMARY KEY NOT NULL, + 'name' text NOT NULL, + 'apiKeyHash' text NOT NULL, + 'lastChars' text NOT NULL, + 'dateCreated' text NOT NULL, + 'isRoot' integer DEFAULT false NOT NULL + ); + + CREATE TABLE 'hostMeta' ( + 'hostMetaId' text PRIMARY KEY NOT NULL, + 'createdAt' integer NOT NULL + ); + + CREATE TABLE 'idp' ( + 'idpId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'name' text NOT NULL, + 'type' text NOT NULL, + 'defaultRoleMapping' text, + 'defaultOrgMapping' text, + 'autoProvision' integer DEFAULT false NOT NULL + ); + + CREATE TABLE 'idpOidcConfig' ( + 'idpOauthConfigId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'idpId' integer NOT NULL, + 'clientId' text NOT NULL, + 'clientSecret' text NOT NULL, + 'authUrl' text NOT NULL, + 'tokenUrl' text NOT NULL, + 'identifierPath' text NOT NULL, + 'emailPath' text, + 'namePath' text, + 'scopes' text NOT NULL, + FOREIGN KEY ('idpId') REFERENCES 'idp'('idpId') ON UPDATE no action ON DELETE cascade + ); + + CREATE TABLE 'idpOrg' ( + 'idpId' integer NOT NULL, + 'orgId' text NOT NULL, + 'roleMapping' text, + 'orgMapping' text, + FOREIGN KEY ('idpId') REFERENCES 'idp'('idpId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + + CREATE TABLE 'licenseKey' ( + 'licenseKeyId' text PRIMARY KEY NOT NULL, + 'instanceId' text NOT NULL, + 'token' text NOT NULL + ); + + CREATE TABLE '__new_user' ( + 'id' text PRIMARY KEY NOT NULL, + 'email' text, + 'username' text NOT NULL, + 'name' text, + 'type' text NOT NULL, + 'idpId' integer, + 'passwordHash' text, + 'twoFactorEnabled' integer DEFAULT false NOT NULL, + 'twoFactorSecret' text, + 'emailVerified' integer DEFAULT false NOT NULL, + 'dateCreated' text NOT NULL, + 'serverAdmin' integer DEFAULT false NOT NULL, + FOREIGN KEY ('idpId') REFERENCES 'idp'('idpId') ON UPDATE no action ON DELETE cascade + ); + + INSERT INTO '__new_user'( + "id", "email", "username", "name", "type", "idpId", "passwordHash", + "twoFactorEnabled", "twoFactorSecret", "emailVerified", "dateCreated", "serverAdmin" + ) + SELECT + "id", + "email", + COALESCE("email", 'unknown'), + NULL, + 'internal', + NULL, + "passwordHash", + "twoFactorEnabled", + "twoFactorSecret", + "emailVerified", + "dateCreated", + "serverAdmin" + FROM 'user'; + + DROP TABLE 'user'; + ALTER TABLE '__new_user' RENAME TO 'user'; + + ALTER TABLE 'resources' ADD 'stickySession' integer DEFAULT false NOT NULL; + ALTER TABLE 'resources' ADD 'tlsServerName' text; + ALTER TABLE 'resources' ADD 'setHostHeader' text; + + CREATE TABLE 'exitNodes_new' ( + 'exitNodeId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'name' text NOT NULL, + 'address' text NOT NULL, + 'endpoint' text NOT NULL, + 'publicKey' text NOT NULL, + 'listenPort' integer NOT NULL, + 'reachableAt' text + ); + + INSERT INTO 'exitNodes_new' ( + 'exitNodeId', 'name', 'address', 'endpoint', 'publicKey', 'listenPort', 'reachableAt' + ) + SELECT + exitNodeId, + name, + address, + endpoint, + pubicKey, + listenPort, + reachableAt + FROM exitNodes; + + DROP TABLE 'exitNodes'; + ALTER TABLE 'exitNodes_new' RENAME TO 'exitNodes'; + `); + })(); // <-- executes the transaction immediately + db.pragma("foreign_keys = ON"); console.log(`Migrated database schema`); } catch (e) { console.log("Unable to migrate database schema"); throw e; } + // Update config file + try { + const filePaths = [configFilePath1, configFilePath2]; + let filePath = ""; + for (const path of filePaths) { + if (fs.existsSync(path)) { + filePath = path; + break; + } + } + + if (!filePath) { + throw new Error( + `No config file found (expected config.yml or config.yaml).` + ); + } + + const fileContents = fs.readFileSync(filePath, "utf8"); + let rawConfig: any = yaml.load(fileContents); + + if (!rawConfig.server.secret) { + rawConfig.server.secret = generateIdFromEntropySize(32); + } + + const updatedYaml = yaml.dump(rawConfig); + fs.writeFileSync(filePath, updatedYaml, "utf8"); + + console.log(`Added new config option: server.secret`); + } catch (e) { + console.log( + `Unable to add new config option: server.secret. Please add it manually.` + ); + console.error(e); + } + console.log(`${version} migration complete`); } + +function generateIdFromEntropySize(size: number): string { + const buffer = crypto.getRandomValues(new Uint8Array(size)); + return encodeBase32LowerCaseNoPadding(buffer); +} diff --git a/src/app/admin/license/components/SitePriceCalculator.tsx b/src/app/admin/license/components/SitePriceCalculator.tsx index f0ff460c..b27c8ada 100644 --- a/src/app/admin/license/components/SitePriceCalculator.tsx +++ b/src/app/admin/license/components/SitePriceCalculator.tsx @@ -28,7 +28,7 @@ export function SitePriceCalculator({ onOpenChange, mode }: SitePriceCalculatorProps) { - const [siteCount, setSiteCount] = useState(1); + const [siteCount, setSiteCount] = useState(3); const pricePerSite = 5; const licenseFlatRate = 125; diff --git a/src/app/setup/layout.tsx b/src/app/setup/layout.tsx index f274bcfc..e254037d 100644 --- a/src/app/setup/layout.tsx +++ b/src/app/setup/layout.tsx @@ -7,6 +7,10 @@ import { Metadata } from "next"; import { redirect } from "next/navigation"; import { cache } from "react"; import { rootNavItems } from "../navigation"; +import { ListUserOrgsResponse } from "@server/routers/org"; +import { internal } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { authCookieHeader } from "@app/lib/api/cookies"; export const metadata: Metadata = { title: `Setup - Pangolin`, @@ -33,10 +37,28 @@ export default async function SetupLayout({ redirect("/"); } + let orgs: ListUserOrgsResponse["orgs"] = []; + try { + const getOrgs = cache(async () => + internal.get>( + `/user/${user.userId}/orgs`, + await authCookieHeader() + ) + ); + const res = await getOrgs(); + if (res && res.data.data.orgs) { + orgs = res.data.data.orgs; + } + } catch (e) {} + return ( <> - +
{children}