diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b0627cc..41d43bd9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,11 +12,12 @@ on: jobs: test: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + - name: Install Node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: '22' @@ -57,8 +58,26 @@ jobs: echo "App failed to start" exit 1 + build-sqlite: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Copy config file + run: cp config/config.example.yml config/config.yml + - name: Build Docker image sqlite - run: make build-sqlite + run: make dev-build-sqlite + + build-postgres: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Copy config file + run: cp config/config.example.yml config/config.yml - name: Build Docker image pg - run: make build-pg + run: make dev-build-pg diff --git a/Dockerfile b/Dockerfile index fa2d71c0..c59490b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,23 +43,25 @@ RUN test -f dist/server.mjs RUN npm run build:cli +# Prune dev dependencies and clean up to prepare for copy to runner +RUN npm prune --omit=dev && npm cache clean --force + FROM node:24-alpine AS runner WORKDIR /app -# Curl used for the health checks -# Python and build tools needed for better-sqlite3 native compilation -RUN apk add --no-cache curl tzdata python3 make g++ +# Only curl and tzdata needed at runtime - no build tools! +RUN apk add --no-cache curl tzdata -# COPY package.json package-lock.json ./ -COPY package*.json ./ - -RUN npm ci --omit=dev && npm cache clean --force +# Copy pre-built node_modules from builder (already pruned to production only) +# This includes the compiled native modules like better-sqlite3 +COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/dist ./dist COPY --from=builder /app/init ./dist/init +COPY --from=builder /app/package.json ./package.json COPY ./cli/wrapper.sh /usr/local/bin/pangctl RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs diff --git a/Makefile b/Makefile index ffd89bb5..25e19ef4 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,12 @@ major_tag := $(shell echo $(tag) | cut -d. -f1) minor_tag := $(shell echo $(tag) | cut -d. -f1,2) -build-release: + +.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql + +build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql + +build-sqlite: @if [ -z "$(tag)" ]; then \ echo "Error: tag is required. Usage: make build-release tag="; \ exit 1; \ @@ -16,6 +21,12 @@ build-release: --tag fosrl/pangolin:$(minor_tag) \ --tag fosrl/pangolin:$(tag) \ --push . + +build-postgresql: + @if [ -z "$(tag)" ]; then \ + echo "Error: tag is required. Usage: make build-release tag="; \ + exit 1; \ + fi docker buildx build \ --build-arg BUILD=oss \ --build-arg DATABASE=pg \ @@ -25,6 +36,12 @@ build-release: --tag fosrl/pangolin:postgresql-$(minor_tag) \ --tag fosrl/pangolin:postgresql-$(tag) \ --push . + +build-ee-sqlite: + @if [ -z "$(tag)" ]; then \ + echo "Error: tag is required. Usage: make build-release tag="; \ + exit 1; \ + fi docker buildx build \ --build-arg BUILD=enterprise \ --build-arg DATABASE=sqlite \ @@ -34,6 +51,12 @@ build-release: --tag fosrl/pangolin:ee-$(minor_tag) \ --tag fosrl/pangolin:ee-$(tag) \ --push . + +build-ee-postgresql: + @if [ -z "$(tag)" ]; then \ + echo "Error: tag is required. Usage: make build-release tag="; \ + exit 1; \ + fi docker buildx build \ --build-arg BUILD=enterprise \ --build-arg DATABASE=pg \ @@ -168,10 +191,10 @@ build-arm: build-x86: docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest . -build-sqlite: +dev-build-sqlite: docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest . -build-pg: +dev-build-pg: docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest . test: diff --git a/README.md b/README.md index a842ed3b..27105c70 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ [![Slack](https://img.shields.io/badge/chat-slack-yellow?style=flat-square&logo=slack)](https://pangolin.net/slack) [![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin) ![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square) -[![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app) +[![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@pangolin-net) diff --git a/cli/commands/clearExitNodes.ts b/cli/commands/clearExitNodes.ts new file mode 100644 index 00000000..2a283203 --- /dev/null +++ b/cli/commands/clearExitNodes.ts @@ -0,0 +1,36 @@ +import { CommandModule } from "yargs"; +import { db, exitNodes } from "@server/db"; +import { eq } from "drizzle-orm"; + +type ClearExitNodesArgs = { }; + +export const clearExitNodes: CommandModule< + {}, + ClearExitNodesArgs +> = { + command: "clear-exit-nodes", + describe: + "Clear all exit nodes from the database", + // no args + builder: (yargs) => { + return yargs; + }, + handler: async (argv: {}) => { + try { + + console.log(`Clearing all exit nodes from the database`); + + // Delete all exit nodes + const deletedCount = await db + .delete(exitNodes) + .where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)) .returning();; // delete all + + console.log(`Deleted ${deletedCount.length} exit node(s) from the database`); + + process.exit(0); + } catch (error) { + console.error("Error:", error); + process.exit(1); + } + } +}; diff --git a/cli/commands/rotateServerSecret.ts b/cli/commands/rotateServerSecret.ts new file mode 100644 index 00000000..1d954af2 --- /dev/null +++ b/cli/commands/rotateServerSecret.ts @@ -0,0 +1,284 @@ +import { CommandModule } from "yargs"; +import { db, idpOidcConfig, licenseKey } from "@server/db"; +import { encrypt, decrypt } from "@server/lib/crypto"; +import { configFilePath1, configFilePath2 } from "@server/lib/consts"; +import { eq } from "drizzle-orm"; +import fs from "fs"; +import yaml from "js-yaml"; + +type RotateServerSecretArgs = { + oldSecret: string; + newSecret: string; + force?: boolean; +}; + +export const rotateServerSecret: CommandModule< + {}, + RotateServerSecretArgs +> = { + command: "rotate-server-secret", + describe: + "Rotate the server secret by decrypting all encrypted values with the old secret and re-encrypting with a new secret", + builder: (yargs) => { + return yargs + .option("oldSecret", { + type: "string", + demandOption: true, + describe: "The current server secret (for verification)" + }) + .option("newSecret", { + type: "string", + demandOption: true, + describe: "The new server secret to use" + }) + .option("force", { + type: "boolean", + default: false, + describe: + "Force rotation even if the old secret doesn't match the config file. " + + "Use this if you know the old secret is correct but the config file is out of sync. " + + "WARNING: This will attempt to decrypt all values with the provided old secret. " + + "If the old secret is incorrect, the rotation will fail or corrupt data." + }); + }, + handler: async (argv: { + oldSecret: string; + newSecret: string; + force?: boolean; + }) => { + try { + // Determine which config file exists + const configPath = fs.existsSync(configFilePath1) + ? configFilePath1 + : fs.existsSync(configFilePath2) + ? configFilePath2 + : null; + + if (!configPath) { + console.error( + "Error: Config file not found. Expected config.yml or config.yaml in the config directory." + ); + process.exit(1); + } + + // Read current config + const configContent = fs.readFileSync(configPath, "utf8"); + const config = yaml.load(configContent) as any; + + if (!config?.server?.secret) { + console.error( + "Error: No server secret found in config file. Cannot rotate." + ); + process.exit(1); + } + + const configSecret = config.server.secret; + const oldSecret = argv.oldSecret; + const newSecret = argv.newSecret; + const force = argv.force || false; + + // Verify that the provided old secret matches the one in config + if (configSecret !== oldSecret) { + if (!force) { + console.error( + "Error: The provided old secret does not match the secret in the config file." + ); + console.error( + "\nIf you are certain the old secret is correct and the config file is out of sync," + ); + console.error( + "you can use the --force flag to bypass this check." + ); + console.error( + "\nWARNING: Using --force with an incorrect old secret will cause the rotation to fail" + ); + console.error( + "or corrupt encrypted data. Only use --force if you are absolutely certain." + ); + process.exit(1); + } else { + console.warn( + "\nWARNING: Using --force flag. Bypassing old secret verification." + ); + console.warn( + "The provided old secret does not match the config file, but proceeding anyway." + ); + console.warn( + "If the old secret is incorrect, this operation will fail or corrupt data.\n" + ); + } + } + + // Validate new secret + if (newSecret.length < 8) { + console.error( + "Error: New secret must be at least 8 characters long" + ); + process.exit(1); + } + + if (oldSecret === newSecret) { + console.error("Error: New secret must be different from old secret"); + process.exit(1); + } + + console.log("Starting server secret rotation..."); + console.log("This will decrypt and re-encrypt all encrypted values in the database."); + + // Read all data first + console.log("\nReading encrypted data from database..."); + const idpConfigs = await db.select().from(idpOidcConfig); + const licenseKeys = await db.select().from(licenseKey); + + console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`); + console.log(`Found ${licenseKeys.length} license key(s)`); + + // Prepare all decrypted and re-encrypted values + console.log("\nDecrypting and re-encrypting values..."); + + type IdpUpdate = { + idpOauthConfigId: number; + encryptedClientId: string; + encryptedClientSecret: string; + }; + + type LicenseKeyUpdate = { + oldLicenseKeyId: string; + newLicenseKeyId: string; + encryptedToken: string; + encryptedInstanceId: string; + }; + + const idpUpdates: IdpUpdate[] = []; + const licenseKeyUpdates: LicenseKeyUpdate[] = []; + + // Process idpOidcConfig entries + for (const idpConfig of idpConfigs) { + try { + // Decrypt with old secret + const decryptedClientId = decrypt(idpConfig.clientId, oldSecret); + const decryptedClientSecret = decrypt( + idpConfig.clientSecret, + oldSecret + ); + + // Re-encrypt with new secret + const encryptedClientId = encrypt(decryptedClientId, newSecret); + const encryptedClientSecret = encrypt( + decryptedClientSecret, + newSecret + ); + + idpUpdates.push({ + idpOauthConfigId: idpConfig.idpOauthConfigId, + encryptedClientId, + encryptedClientSecret + }); + } catch (error) { + console.error( + `Error processing IdP config ${idpConfig.idpOauthConfigId}:`, + error + ); + throw error; + } + } + + // Process licenseKey entries + for (const key of licenseKeys) { + try { + // Decrypt with old secret + const decryptedLicenseKeyId = decrypt(key.licenseKeyId, oldSecret); + const decryptedToken = decrypt(key.token, oldSecret); + const decryptedInstanceId = decrypt(key.instanceId, oldSecret); + + // Re-encrypt with new secret + const encryptedLicenseKeyId = encrypt( + decryptedLicenseKeyId, + newSecret + ); + const encryptedToken = encrypt(decryptedToken, newSecret); + const encryptedInstanceId = encrypt( + decryptedInstanceId, + newSecret + ); + + licenseKeyUpdates.push({ + oldLicenseKeyId: key.licenseKeyId, + newLicenseKeyId: encryptedLicenseKeyId, + encryptedToken, + encryptedInstanceId + }); + } catch (error) { + console.error( + `Error processing license key ${key.licenseKeyId}:`, + error + ); + throw error; + } + } + + // Perform all database updates in a single transaction + console.log("\nUpdating database in transaction..."); + await db.transaction(async (trx) => { + // Update idpOidcConfig entries + for (const update of idpUpdates) { + await trx + .update(idpOidcConfig) + .set({ + clientId: update.encryptedClientId, + clientSecret: update.encryptedClientSecret + }) + .where( + eq( + idpOidcConfig.idpOauthConfigId, + update.idpOauthConfigId + ) + ); + } + + // Update licenseKey entries (delete old, insert new) + for (const update of licenseKeyUpdates) { + // Delete old entry + await trx + .delete(licenseKey) + .where(eq(licenseKey.licenseKeyId, update.oldLicenseKeyId)); + + // Insert new entry with re-encrypted values + await trx.insert(licenseKey).values({ + licenseKeyId: update.newLicenseKeyId, + token: update.encryptedToken, + instanceId: update.encryptedInstanceId + }); + } + }); + + console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`); + console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`); + + // Update config file with new secret + console.log("\nUpdating config file..."); + config.server.secret = newSecret; + const newConfigContent = yaml.dump(config, { + indent: 2, + lineWidth: -1 + }); + fs.writeFileSync(configPath, newConfigContent, "utf8"); + + console.log(`Updated config file: ${configPath}`); + + console.log("\nServer secret rotation completed successfully!"); + console.log(`\nSummary:`); + console.log(` - OIDC IdP configurations: ${idpUpdates.length}`); + console.log(` - License keys: ${licenseKeyUpdates.length}`); + console.log( + `\n IMPORTANT: Restart the server for the new secret to take effect.` + ); + + process.exit(0); + } catch (error) { + console.error("Error rotating server secret:", error); + process.exit(1); + } + } +}; + diff --git a/cli/index.ts b/cli/index.ts index f9e884cc..f44f41ba 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -4,10 +4,14 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { setAdminCredentials } from "@cli/commands/setAdminCredentials"; import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys"; +import { clearExitNodes } from "./commands/clearExitNodes"; +import { rotateServerSecret } from "./commands/rotateServerSecret"; yargs(hideBin(process.argv)) .scriptName("pangctl") .command(setAdminCredentials) .command(resetUserSecurityKeys) + .command(clearExitNodes) + .command(rotateServerSecret) .demandCommand() .help().argv; diff --git a/install/config/crowdsec/docker-compose.yml b/install/config/crowdsec/docker-compose.yml index 17289ef2..0fb95109 100644 --- a/install/config/crowdsec/docker-compose.yml +++ b/install/config/crowdsec/docker-compose.yml @@ -9,10 +9,15 @@ services: PARSERS: crowdsecurity/whitelists ENROLL_TAGS: docker healthcheck: - interval: 10s - retries: 15 - timeout: 10s - test: ["CMD", "cscli", "capi", "status"] + test: + - CMD + - cscli + - lapi + - status + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s labels: - "traefik.enable=false" # Disable traefik for crowdsec volumes: diff --git a/install/config/crowdsec/dynamic_config.yml b/install/config/crowdsec/dynamic_config.yml index cac5fa6e..c58d5670 100644 --- a/install/config/crowdsec/dynamic_config.yml +++ b/install/config/crowdsec/dynamic_config.yml @@ -44,7 +44,7 @@ http: crowdsecAppsecUnreachableBlock: true # Block on unreachable crowdsecAppsecBodyLimit: 10485760 crowdsecLapiKey: "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK" # CrowdSec API key which you noted down later - crowdsecLapiHost: crowdsec:8080 # CrowdSec + crowdsecLapiHost: crowdsec:8080 # CrowdSec crowdsecLapiScheme: http # CrowdSec API scheme forwardedHeadersTrustedIPs: # Forwarded headers trusted IPs - "0.0.0.0/0" # All IP addresses are trusted for forwarded headers (CHANGE MADE HERE) @@ -106,4 +106,13 @@ http: api-service: loadBalancer: servers: - - url: "http://pangolin:3000" # API/WebSocket server \ No newline at end of file + - url: "http://pangolin:3000" # API/WebSocket server + +tcp: + serversTransports: + pp-transport-v1: + proxyProtocol: + version: 1 + pp-transport-v2: + proxyProtocol: + version: 2 diff --git a/install/containers.go b/install/containers.go index 9993e117..464186c2 100644 --- a/install/containers.go +++ b/install/containers.go @@ -73,7 +73,7 @@ func installDocker() error { case strings.Contains(osRelease, "ID=ubuntu"): installCmd = exec.Command("bash", "-c", fmt.Sprintf(` apt-get update && - apt-get install -y apt-transport-https ca-certificates curl && + apt-get install -y apt-transport-https ca-certificates curl gpg && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && apt-get update && @@ -82,7 +82,7 @@ func installDocker() error { case strings.Contains(osRelease, "ID=debian"): installCmd = exec.Command("bash", "-c", fmt.Sprintf(` apt-get update && - apt-get install -y apt-transport-https ca-certificates curl && + apt-get install -y apt-transport-https ca-certificates curl gpg && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && apt-get update && diff --git a/messages/en-US.json b/messages/en-US.json index 7d5deded..3b5fc9ea 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -33,7 +33,7 @@ "password": "Password", "confirmPassword": "Confirm Password", "createAccount": "Create Account", - "viewSettings": "View settings", + "viewSettings": "View Settings", "delete": "Delete", "name": "Name", "online": "Online", @@ -51,6 +51,9 @@ "siteQuestionRemove": "Are you sure you want to remove the site from the organization?", "siteManageSites": "Manage Sites", "siteDescription": "Create and manage sites to enable connectivity to private networks", + "sitesBannerTitle": "Connect Any Network", + "sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.", + "sitesBannerButtonText": "Install Site", "siteCreate": "Create Site", "siteCreateDescription2": "Follow the steps below to create and connect a new site", "siteCreateDescription": "Create a new site to start connecting resources", @@ -100,6 +103,7 @@ "siteTunnelDescription": "Determine how you want to connect to the site", "siteNewtCredentials": "Credentials", "siteNewtCredentialsDescription": "This is how the site will authenticate with the server", + "remoteNodeCredentialsDescription": "This is how the remote node will authenticate with the server", "siteCredentialsSave": "Save the Credentials", "siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.", "siteInfo": "Site Information", @@ -146,8 +150,12 @@ "shareErrorSelectResource": "Please select a resource", "proxyResourceTitle": "Manage Public Resources", "proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser", + "proxyResourcesBannerTitle": "Web-based Public Access", + "proxyResourcesBannerDescription": "Public resources are HTTPS or TCP/UDP proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.", "clientResourceTitle": "Manage Private Resources", "clientResourceDescription": "Create and manage resources that are only accessible through a connected client", + "privateResourcesBannerTitle": "Zero-Trust Private Access", + "privateResourcesBannerDescription": "Private resources use zero-trust security, ensuring users and machines can only access resources you explicitly grant. Connect user devices or machine clients to access these resources over a secure virtual private network.", "resourcesSearch": "Search resources...", "resourceAdd": "Add Resource", "resourceErrorDelte": "Error deleting resource", @@ -157,9 +165,9 @@ "resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.", "resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?", "resourceHTTP": "HTTPS Resource", - "resourceHTTPDescription": "Proxy requests to the app over HTTPS using a subdomain or base domain.", + "resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.", "resourceRaw": "Raw TCP/UDP Resource", - "resourceRawDescription": "Proxy requests to the app over TCP/UDP using a port number. This only works when sites are connected to nodes.", + "resourceRawDescription": "Proxy requests over raw TCP/UDP using a port number.", "resourceCreate": "Create Resource", "resourceCreateDescription": "Follow the steps below to create a new resource", "resourceSeeAll": "See All Resources", @@ -419,7 +427,7 @@ "userErrorExistsDescription": "This user is already a member of the organization.", "inviteError": "Failed to invite user", "inviteErrorDescription": "An error occurred while inviting the user", - "userInvited": "User invited", + "userInvited": "User Invited", "userInvitedDescription": "The user has been successfully invited.", "userErrorCreate": "Failed to create user", "userErrorCreateDescription": "An error occurred while creating the user", @@ -687,7 +695,7 @@ "resourceRoleDescription": "Admins can always access this resource.", "resourceUsersRoles": "Access Controls", "resourceUsersRolesDescription": "Configure which users and roles can visit this resource", - "resourceUsersRolesSubmit": "Save Users & Roles", + "resourceUsersRolesSubmit": "Save Access Controls", "resourceWhitelistSave": "Saved successfully", "resourceWhitelistSaveDescription": "Whitelist settings have been saved", "ssoUse": "Use Platform SSO", @@ -945,7 +953,7 @@ "pincodeAuth": "Authenticator Code", "pincodeSubmit2": "Submit Code", "passwordResetSubmit": "Request Reset", - "passwordResetAlreadyHaveCode": "Enter Password Reset Code", + "passwordResetAlreadyHaveCode": "Enter Code", "passwordResetSmtpRequired": "Please contact your administrator", "passwordResetSmtpRequiredDescription": "A password reset code is required to reset your password. Please contact your administrator for assistance.", "passwordBack": "Back to Password", @@ -1035,6 +1043,7 @@ "updateOrgUser": "Update Org User", "createOrgUser": "Create Org User", "actionUpdateOrg": "Update Organization", + "actionRemoveInvitation": "Remove Invitation", "actionUpdateUser": "Update User", "actionGetUser": "Get User", "actionGetOrgUser": "Get Organization User", @@ -1044,6 +1053,8 @@ "actionGetSite": "Get Site", "actionListSites": "List Sites", "actionApplyBlueprint": "Apply Blueprint", + "actionListBlueprints": "List Blueprints", + "actionGetBlueprint": "Get Blueprint", "setupToken": "Setup Token", "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", @@ -1194,7 +1205,7 @@ "sidebarUserDevices": "Users", "sidebarMachineClients": "Machines", "sidebarDomains": "Domains", - "sidebarGeneral": "General", + "sidebarGeneral": "Manage", "sidebarLogAndAnalytics": "Log & Analytics", "sidebarBluePrints": "Blueprints", "sidebarOrganization": "Organization", @@ -1308,8 +1319,11 @@ "accountSetupSuccess": "Account setup completed! Welcome to Pangolin!", "documentation": "Documentation", "saveAllSettings": "Save All Settings", + "saveResourceTargets": "Save Targets", + "saveResourceHttp": "Save Proxy Settings", + "saveProxyProtocol": "Save Proxy protocol settings", "settingsUpdated": "Settings updated", - "settingsUpdatedDescription": "All settings have been updated successfully", + "settingsUpdatedDescription": "Settings updated successfully", "settingsErrorUpdate": "Failed to update settings", "settingsErrorUpdateDescription": "An error occurred while updating settings", "sidebarCollapse": "Collapse", @@ -1616,9 +1630,8 @@ "createInternalResourceDialogResourceProperties": "Resource Properties", "createInternalResourceDialogName": "Name", "createInternalResourceDialogSite": "Site", - "createInternalResourceDialogSelectSite": "Select site...", - "createInternalResourceDialogSearchSites": "Search sites...", - "createInternalResourceDialogNoSitesFound": "No sites found.", + "selectSite": "Select site...", + "noSitesFound": "No sites found.", "createInternalResourceDialogProtocol": "Protocol", "createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogUdp": "UDP", @@ -1658,7 +1671,7 @@ "siteAddressDescription": "The internal address of the site. Must fall within the organization's subnet.", "siteNameDescription": "The display name of the site that can be changed later.", "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external identity provider for authentication.", "selectIdp": "Select IDP", "selectIdpPlaceholder": "Choose an IDP...", "selectIdpRequired": "Please select an IDP when auto login is enabled.", @@ -1670,7 +1683,7 @@ "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.", "remoteExitNodeManageRemoteExitNodes": "Remote Nodes", - "remoteExitNodeDescription": "Self-host one or more remote nodes to extend network connectivity and reduce reliance on the cloud", + "remoteExitNodeDescription": "Self-host your own remote relay and proxy server nodes", "remoteExitNodes": "Nodes", "searchRemoteExitNodes": "Search nodes...", "remoteExitNodeAdd": "Add Node", @@ -1680,20 +1693,22 @@ "remoteExitNodeConfirmDelete": "Confirm Delete Node", "remoteExitNodeDelete": "Delete Node", "sidebarRemoteExitNodes": "Remote Nodes", + "remoteExitNodeId": "ID", + "remoteExitNodeSecretKey": "Secret", "remoteExitNodeCreate": { - "title": "Create Node", - "description": "Create a new node to extend network connectivity", + "title": "Create Remote Node", + "description": "Create a new self-hosted remote relay and proxy server node", "viewAllButton": "View All Nodes", "strategy": { "title": "Creation Strategy", - "description": "Choose this to manually configure the node or generate new credentials.", + "description": "Select how you want to create the remote node", "adopt": { "title": "Adopt Node", "description": "Choose this if you already have the credentials for the node." }, "generate": { "title": "Generate Keys", - "description": "Choose this if you want to generate new keys for the node" + "description": "Choose this if you want to generate new keys for the node." } }, "adopt": { @@ -1806,9 +1821,30 @@ "idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "subnet": "Subnet", "subnetDescription": "The subnet for this organization's network configuration.", - "authPage": "Auth Page", - "authPageDescription": "Configure the auth page for the organization", + "customDomain": "Custom Domain", + "authPage": "Authentication Pages", + "authPageDescription": "Set a custom domain for the organization's authentication pages", "authPageDomain": "Auth Page Domain", + "authPageBranding": "Custom Branding", + "authPageBrandingDescription": "Configure the branding that appears on authentication pages for this organization", + "authPageBrandingUpdated": "Auth page Branding updated successfully", + "authPageBrandingRemoved": "Auth page Branding removed successfully", + "authPageBrandingRemoveTitle": "Remove Auth Page Branding", + "authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?", + "authPageBrandingDeleteConfirm": "Confirm Delete Branding", + "brandingLogoURL": "Logo URL", + "brandingPrimaryColor": "Primary Color", + "brandingLogoWidth": "Width (px)", + "brandingLogoHeight": "Height (px)", + "brandingOrgTitle": "Title for Organization Auth Page", + "brandingOrgDescription": "{orgName} will be replaced with the organization's name", + "brandingOrgSubtitle": "Subtitle for Organization Auth Page", + "brandingResourceTitle": "Title for Resource Auth Page", + "brandingResourceSubtitle": "Subtitle for Resource Auth Page", + "brandingResourceDescription": "{resourceName} will be replaced with the organization's name", + "saveAuthPageDomain": "Save Domain", + "saveAuthPageBranding": "Save Branding", + "removeAuthPageBranding": "Remove Branding", "noDomainSet": "No domain set", "changeDomain": "Change Domain", "selectDomain": "Select Domain", @@ -1817,7 +1853,7 @@ "setAuthPageDomain": "Set Auth Page Domain", "failedToFetchCertificate": "Failed to fetch certificate", "failedToRestartCertificate": "Failed to restart certificate", - "addDomainToEnableCustomAuthPages": "Add a domain to enable custom authentication pages for the organization", + "addDomainToEnableCustomAuthPages": "Users will be able to access the organization's login page and complete resource authentication using this domain.", "selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page", "domainPickerProvidedDomain": "Provided Domain", "domainPickerFreeProvidedDomain": "Free Provided Domain", @@ -1832,10 +1868,19 @@ "domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.", "domainPickerSubdomainSanitized": "Subdomain sanitized", "domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"", - "orgAuthSignInTitle": "Sign in to the organization", + "orgAuthSignInTitle": "Organization Sign In", "orgAuthChooseIdpDescription": "Choose your identity provider to continue", "orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.", "orgAuthSignInWithPangolin": "Sign in with Pangolin", + "orgAuthSignInToOrg": "Sign in to an organization", + "orgAuthSelectOrgTitle": "Organization Sign In", + "orgAuthSelectOrgDescription": "Enter your organization ID to continue", + "orgAuthOrgIdPlaceholder": "your-organization", + "orgAuthOrgIdHelp": "Enter your organization's unique identifier", + "orgAuthSelectOrgHelp": "After entering your organization ID, you'll be taken to your organization's sign-in page where you can use SSO or your organization credentials.", + "orgAuthRememberOrgId": "Remember this organization ID", + "orgAuthBackToSignIn": "Back to standard sign in", + "orgAuthNoAccount": "Don't have an account?", "subscriptionRequiredToUse": "A subscription is required to use this feature.", "idpDisabled": "Identity providers are disabled.", "orgAuthPageDisabled": "Organization auth page is disabled.", @@ -1850,6 +1895,8 @@ "enableTwoFactorAuthentication": "Enable two-factor authentication", "completeSecuritySteps": "Complete Security Steps", "securitySettings": "Security Settings", + "dangerSection": "Danger Zone", + "dangerSectionDescription": "Permanently delete all data associated with this organization", "securitySettingsDescription": "Configure security policies for the organization", "requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users", "requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.", @@ -1887,7 +1934,7 @@ "securityPolicyChangeWarningText": "This will affect all users in the organization", "authPageErrorUpdateMessage": "An error occurred while updating the auth page settings", "authPageErrorUpdate": "Unable to update auth page", - "authPageUpdated": "Auth page updated successfully", + "authPageDomainUpdated": "Auth page Domain updated successfully", "healthCheckNotAvailable": "Local", "rewritePath": "Rewrite Path", "rewritePathDescription": "Optionally rewrite the path before forwarding to the target.", @@ -1915,8 +1962,15 @@ "beta": "Beta", "manageUserDevices": "User Devices", "manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources", + "downloadClientBannerTitle": "Download Pangolin Client", + "downloadClientBannerDescription": "Download the Pangolin client for your system to connect to the Pangolin network and access resources privately.", "manageMachineClients": "Manage Machine Clients", "manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources", + "machineClientsBannerTitle": "Servers & Automated Systems", + "machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.", + "machineClientsBannerPangolinCLI": "Pangolin CLI", + "machineClientsBannerOlmCLI": "Olm CLI", + "machineClientsBannerOlmContainer": "Olm Container", "clientsTableUserClients": "User", "clientsTableMachineClients": "Machine", "licenseTableValidUntil": "Valid Until", @@ -2060,7 +2114,7 @@ "request": "Request", "requests": "Requests", "logs": "Logs", - "logsSettingsDescription": "Monitor logs collected from this orginization", + "logsSettingsDescription": "Monitor logs collected from this organization", "searchLogs": "Search logs...", "action": "Action", "actor": "Actor", @@ -2122,7 +2176,7 @@ "unverified": "Unverified", "domainSetting": "Domain Settings", "domainSettingDescription": "Configure settings for the domain", - "preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver).", + "preferWildcardCertDescription": "Attempt to generate a wildcard certificate (requires a properly configured certificate resolver).", "recordName": "Record Name", "auto": "Auto", "TTL": "TTL", @@ -2257,6 +2311,8 @@ "setupFailedToFetchSubnet": "Failed to fetch default subnet", "setupSubnetAdvanced": "Subnet (Advanced)", "setupSubnetDescription": "The subnet for this organization's internal network.", + "setupUtilitySubnet": "Utility Subnet (Advanced)", + "setupUtilitySubnetDescription": "The subnet for this organization's alias addresses and DNS server.", "siteRegenerateAndDisconnect": "Regenerate and Disconnect", "siteRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this site?", "siteRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the site. The site will need to be restarted with the new credentials.", @@ -2272,5 +2328,40 @@ "remoteExitNodeRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the remote exit node. The remote exit node will need to be restarted with the new credentials.", "remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?", "remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.", - "agent": "Agent" + "agent": "Agent", + "personalUseOnly": "Personal Use Only", + "loginPageLicenseWatermark": "This instance is licensed for personal use only.", + "instanceIsUnlicensed": "This instance is unlicensed.", + "portRestrictions": "Port Restrictions", + "allPorts": "All", + "custom": "Custom", + "allPortsAllowed": "All Ports Allowed", + "allPortsBlocked": "All Ports Blocked", + "tcpPortsDescription": "Specify which TCP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 80,443,8000-9000).", + "udpPortsDescription": "Specify which UDP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 53,123,500-600).", + "organizationLoginPageTitle": "Organization Login Page", + "organizationLoginPageDescription": "Customize the login page for this organization", + "resourceLoginPageTitle": "Resource Login Page", + "resourceLoginPageDescription": "Customize the login page for individual resources", + "enterConfirmation": "Enter confirmation", + "blueprintViewDetails": "Details", + "defaultIdentityProvider": "Default Identity Provider", + "editInternalResourceDialogNetworkSettings": "Network Settings", + "editInternalResourceDialogAccessPolicy": "Access Policy", + "editInternalResourceDialogAddRoles": "Add Roles", + "editInternalResourceDialogAddUsers": "Add Users", + "editInternalResourceDialogAddClients": "Add Clients", + "editInternalResourceDialogDestinationLabel": "Destination", + "editInternalResourceDialogDestinationDescription": "Specify the destination address for the internal resource. This can be a hostname, IP address, or CIDR range depending on the selected mode. Optionally set an internal DNS alias for easier identification.", + "editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.", + "editInternalResourceDialogTcp": "TCP", + "editInternalResourceDialogUdp": "UDP", + "editInternalResourceDialogIcmp": "ICMP", + "editInternalResourceDialogAccessControl": "Access Control", + "editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.", + "editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535.", + "orgAuthWhatsThis": "Where can I find my organization ID?", + "learnMore": "Learn more", + "backToHome": "Go back to home", + "needToSignInToOrg": "Need to use your organization's identity provider?" } diff --git a/messages/zh-TW.json b/messages/zh-TW.json index a7e11f60..43f2de47 100644 --- a/messages/zh-TW.json +++ b/messages/zh-TW.json @@ -1022,6 +1022,8 @@ "actionGetSite": "獲取站點", "actionListSites": "站點列表", "actionApplyBlueprint": "應用藍圖", + "actionListBlueprints": "藍圖列表", + "actionGetBlueprint": "獲取藍圖", "setupToken": "設置令牌", "setupTokenDescription": "從伺服器控制台輸入設定令牌。", "setupTokenRequired": "需要設置令牌", diff --git a/next.config.ts b/next.config.ts index 05ed8e62..630a3416 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,7 @@ import createNextIntlPlugin from "next-intl/plugin"; const withNextIntl = createNextIntlPlugin(); const nextConfig: NextConfig = { + reactStrictMode: false, eslint: { ignoreDuringBuilds: true }, diff --git a/package-lock.json b/package-lock.json index c6e60cef..8cdfda25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "SEE LICENSE IN LICENSE AND README.md", "dependencies": { "@asteasolutions/zod-to-openapi": "8.2.0", - "@aws-sdk/client-s3": "3.948.0", + "@aws-sdk/client-s3": "3.955.0", "@faker-js/faker": "10.1.0", "@headlessui/react": "2.2.9", "@hookform/resolvers": "5.2.2", @@ -36,12 +36,12 @@ "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-tooltip": "1.2.8", - "@react-email/components": "1.0.1", + "@react-email/components": "1.0.2", "@react-email/render": "2.0.0", - "@react-email/tailwind": "2.0.1", + "@react-email/tailwind": "2.0.2", "@simplewebauthn/browser": "13.2.2", "@simplewebauthn/server": "13.2.2", - "@tailwindcss/forms": "0.5.10", + "@tailwindcss/forms": "0.5.11", "@tanstack/react-query": "5.90.12", "@tanstack/react-table": "8.21.3", "arctic": "3.7.0", @@ -58,9 +58,9 @@ "crypto-js": "4.2.0", "d3": "7.9.0", "date-fns": "4.1.0", - "drizzle-orm": "0.45.0", - "eslint": "9.39.1", - "eslint-config-next": "16.0.8", + "drizzle-orm": "0.45.1", + "eslint": "9.39.2", + "eslint-config-next": "16.1.0", "express": "5.2.1", "express-rate-limit": "8.2.1", "glob": "13.0.0", @@ -72,11 +72,11 @@ "jmespath": "0.16.0", "js-yaml": "4.1.1", "jsonwebtoken": "9.0.3", - "lucide-react": "0.559.0", + "lucide-react": "0.562.0", "maxmind": "5.0.1", "moment": "2.30.1", "next": "15.5.9", - "next-intl": "4.5.8", + "next-intl": "4.6.1", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", @@ -86,11 +86,11 @@ "nprogress": "0.2.0", "oslo": "1.2.1", "pg": "8.16.3", - "posthog-node": "5.17.2", + "posthog-node": "5.17.4", "qrcode.react": "4.2.0", "react": "19.2.3", - "react-day-picker": "9.12.0", - "react-dom": "19.2.1", + "react-day-picker": "9.13.0", + "react-dom": "19.2.3", "react-easy-sort": "1.8.0", "react-hook-form": "7.68.0", "react-icons": "5.5.0", @@ -99,7 +99,7 @@ "reodotdev": "1.0.0", "resend": "6.6.0", "semver": "7.7.3", - "stripe": "20.0.0", + "stripe": "20.1.0", "swagger-ui-express": "5.0.1", "tailwind-merge": "3.4.0", "topojson-client": "3.1.0", @@ -112,13 +112,13 @@ "ws": "8.18.3", "yaml": "2.8.2", "yargs": "18.0.0", - "zod": "4.1.13", + "zod": "4.2.1", "zod-validation-error": "5.0.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.51.1", + "@dotenvx/dotenvx": "1.51.2", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@tailwindcss/postcss": "4.1.17", + "@tailwindcss/postcss": "4.1.18", "@tanstack/react-query-devtools": "5.91.1", "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", @@ -143,12 +143,12 @@ "@types/yargs": "17.0.35", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.8", - "esbuild": "0.27.1", + "esbuild": "0.27.2", "esbuild-node-externals": "1.20.1", "postcss": "8.5.6", "prettier": "3.7.4", "react-email": "5.0.7", - "tailwindcss": "4.1.17", + "tailwindcss": "4.1.18", "tsc-alias": "1.8.16", "tsx": "4.21.0", "typescript": "5.9.3", @@ -396,65 +396,65 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.948.0.tgz", - "integrity": "sha512-uvEjds8aYA9SzhBS8RKDtsDUhNV9VhqKiHTcmvhM7gJO92q0WTn8/QeFTdNyLc6RxpiDyz+uBxS7PcdNiZzqfA==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.955.0.tgz", + "integrity": "sha512-bFvSM6UB0R5hpWfXzHI3BlKwT2qYHto9JoDtzSr5FxVguTMzJyr+an11VT1Hi5wgO03luXEeXeloURFvaMs6TQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-node": "3.948.0", - "@aws-sdk/middleware-bucket-endpoint": "3.936.0", - "@aws-sdk/middleware-expect-continue": "3.936.0", - "@aws-sdk/middleware-flexible-checksums": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-location-constraint": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-sdk-s3": "3.947.0", - "@aws-sdk/middleware-ssec": "3.936.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/signature-v4-multi-region": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/eventstream-serde-browser": "^4.2.5", - "@smithy/eventstream-serde-config-resolver": "^4.3.5", - "@smithy/eventstream-serde-node": "^4.2.5", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-blob-browser": "^4.2.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/hash-stream-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/md5-js": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/credential-provider-node": "3.955.0", + "@aws-sdk/middleware-bucket-endpoint": "3.953.0", + "@aws-sdk/middleware-expect-continue": "3.953.0", + "@aws-sdk/middleware-flexible-checksums": "3.954.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-location-constraint": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-sdk-s3": "3.954.0", + "@aws-sdk/middleware-ssec": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.954.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/signature-v4-multi-region": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.954.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/eventstream-serde-browser": "^4.2.6", + "@smithy/eventstream-serde-config-resolver": "^4.3.6", + "@smithy/eventstream-serde-node": "^4.2.6", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-blob-browser": "^4.2.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/hash-stream-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/md5-js": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.4.0", + "@smithy/middleware-retry": "^4.4.16", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/util-defaults-mode-browser": "^4.3.15", + "@smithy/util-defaults-mode-node": "^4.2.18", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", + "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.5", + "@smithy/util-waiter": "^4.2.6", "tslib": "^2.6.2" }, "engines": { @@ -462,47 +462,47 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.948.0.tgz", - "integrity": "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.955.0.tgz", + "integrity": "sha512-+nym5boDFt2ksba0fElocMKxCFJbJcd31PI3502hoI1N5VK7HyxkQeBtQJ64JYomvw8eARjWWC13hkB0LtZILw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.954.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.954.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.4.0", + "@smithy/middleware-retry": "^4.4.16", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.15", + "@smithy/util-defaults-mode-node": "^4.2.18", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -511,22 +511,22 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", - "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.954.0.tgz", + "integrity": "sha512-5oYO5RP+mvCNXNj8XnF9jZo0EP0LTseYOJVNQYcii1D9DJqzHL3HJWurYh7cXxz7G7eDyvVYA01O9Xpt34TdoA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.7", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/xml-builder": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/signature-v4": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-middleware": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -535,15 +535,15 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.947.0.tgz", - "integrity": "sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.954.0.tgz", + "integrity": "sha512-2HNkqBjfsvyoRuPAiFh86JBFMFyaCNhL4VyH6XqwTGKZffjG7hdBmzXPy7AT7G3oFh1k/1Zc27v0qxaKoK7mBA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -551,20 +551,20 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.947.0.tgz", - "integrity": "sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.954.0.tgz", + "integrity": "sha512-CrWD5300+NE1OYRnSVDxoG7G0b5cLIZb7yp+rNQ5Jq/kqnTmyJXpVAsivq+bQIDaGzPXhadzpAMIoo7K/aHaag==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", + "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" }, "engines": { @@ -572,24 +572,24 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.948.0.tgz", - "integrity": "sha512-Cl//Qh88e8HBL7yYkJNpF5eq76IO6rq8GsatKcfVBm7RFVxCqYEPSSBtkHdbtNwQdRQqAMXc6E/lEB/CZUDxnA==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.955.0.tgz", + "integrity": "sha512-90isLovxsPzaaSx3IIUZuxym6VXrsRetnQ3AuHr2kiTFk2pIzyIwmi+gDcUaLXQ5nNBoSj1Z/4+i1vhxa1n2DQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-env": "3.947.0", - "@aws-sdk/credential-provider-http": "3.947.0", - "@aws-sdk/credential-provider-login": "3.948.0", - "@aws-sdk/credential-provider-process": "3.947.0", - "@aws-sdk/credential-provider-sso": "3.948.0", - "@aws-sdk/credential-provider-web-identity": "3.948.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/credential-provider-env": "3.954.0", + "@aws-sdk/credential-provider-http": "3.954.0", + "@aws-sdk/credential-provider-login": "3.955.0", + "@aws-sdk/credential-provider-process": "3.954.0", + "@aws-sdk/credential-provider-sso": "3.955.0", + "@aws-sdk/credential-provider-web-identity": "3.955.0", + "@aws-sdk/nested-clients": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/credential-provider-imds": "^4.2.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -597,18 +597,18 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-login": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.948.0.tgz", - "integrity": "sha512-gcKO2b6eeTuZGp3Vvgr/9OxajMrD3W+FZ2FCyJox363ZgMoYJsyNid1vuZrEuAGkx0jvveLXfwiVS0UXyPkgtw==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.955.0.tgz", + "integrity": "sha512-xlkmSvg8oDN5LIxLAq3N1QWK8F8gUAsBWZlp1IX8Lr5XhcKI3GVarIIUcZrvCy1NjzCd/LDXYdNL6MRlNP4bAw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/nested-clients": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -616,22 +616,22 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.948.0.tgz", - "integrity": "sha512-ep5vRLnrRdcsP17Ef31sNN4g8Nqk/4JBydcUJuFRbGuyQtrZZrVT81UeH2xhz6d0BK6ejafDB9+ZpBjXuWT5/Q==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.955.0.tgz", + "integrity": "sha512-XIL4QB+dPOJA6DRTmYZL52wFcLTslb7V1ydS4FCNT2DVLhkO4ExkPP+pe5YmIpzt/Our1ugS+XxAs3e6BtyFjA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.947.0", - "@aws-sdk/credential-provider-http": "3.947.0", - "@aws-sdk/credential-provider-ini": "3.948.0", - "@aws-sdk/credential-provider-process": "3.947.0", - "@aws-sdk/credential-provider-sso": "3.948.0", - "@aws-sdk/credential-provider-web-identity": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/credential-provider-env": "3.954.0", + "@aws-sdk/credential-provider-http": "3.954.0", + "@aws-sdk/credential-provider-ini": "3.955.0", + "@aws-sdk/credential-provider-process": "3.954.0", + "@aws-sdk/credential-provider-sso": "3.955.0", + "@aws-sdk/credential-provider-web-identity": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/credential-provider-imds": "^4.2.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -639,16 +639,16 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.947.0.tgz", - "integrity": "sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.954.0.tgz", + "integrity": "sha512-Y1/0O2LgbKM8iIgcVj/GNEQW6p90LVTCOzF2CI1pouoKqxmZ/1F7F66WHoa6XUOfKaCRj/R6nuMR3om9ThaM5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -656,18 +656,18 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.948.0.tgz", - "integrity": "sha512-gqLhX1L+zb/ZDnnYbILQqJ46j735StfWV5PbDjxRzBKS7GzsiYoaf6MyHseEopmWrez5zl5l6aWzig7UpzSeQQ==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.955.0.tgz", + "integrity": "sha512-Y99KI73Fn8JnB4RY5Ls6j7rd5jmFFwnY9WLHIWeJdc+vfwL6Bb1uWKW3+m/B9+RC4Xoz2nQgtefBcdWq5Xx8iw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.948.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/token-providers": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/client-sso": "3.955.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/token-providers": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -675,17 +675,46 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.948.0.tgz", - "integrity": "sha512-MvYQlXVoJyfF3/SmnNzOVEtANRAiJIObEUYYyjTqKZTmcRIVVky0tPuG26XnB8LmTYgtESwJIZJj/Eyyc9WURQ==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.955.0.tgz", + "integrity": "sha512-+lFxkZ2Vz3qp/T68ZONKzWVTQvomTu7E6tts1dfAbEcDt62Y/nPCByq/C2hQj+TiN05HrUx+yTJaGHBklhkbqA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/nested-clients": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.953.0.tgz", + "integrity": "sha512-jTGhfkONav+r4E6HLOrl5SzBqDmPByUYCkyB/c/3TVb8jX3wAZx8/q9bphKpCh+G5ARi3IdbSisgkZrJYqQ19Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.953.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.953.0.tgz", + "integrity": "sha512-PlWdVYgcuptkIC0ZKqVUhWNtSHXJSx7U9V8J7dJjRmsXC40X7zpEycvrkzDMJjeTDGcCceYbyYAg/4X1lkcIMw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -693,15 +722,15 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.948.0.tgz", - "integrity": "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.953.0.tgz", + "integrity": "sha512-cmIJx0gWeesUKK4YwgE+VQL3mpACr3/J24fbwnc1Z5tntC86b+HQFzU5vsBDw6lLwyD46dBgWdsXFh1jL+ZaFw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", + "@aws-sdk/types": "3.953.0", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -709,23 +738,23 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.947.0.tgz", - "integrity": "sha512-DS2tm5YBKhPW2PthrRBDr6eufChbwXe0NjtTZcYDfUCXf0OR+W6cIqyKguwHMJ+IyYdey30AfVw9/Lb5KB8U8A==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.954.0.tgz", + "integrity": "sha512-274CNmnRjknmfFb2o0Azxic54fnujaA8AYSeRUOho3lN48TVzx85eAFWj2kLgvUJO88pE3jBDPWboKQiQdXeUQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/core": "^3.18.7", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-arn-parser": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/signature-v4": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -734,17 +763,17 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.947.0.tgz", - "integrity": "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.954.0.tgz", + "integrity": "sha512-5PX8JDe3dB2+MqXeGIhmgFnm2rbVsSxhz+Xyuu1oxLtbOn+a9UDA+sNBufEBjt3UxWy5qwEEY1fxdbXXayjlGg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@smithy/core": "^3.18.7", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -752,47 +781,47 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.948.0.tgz", - "integrity": "sha512-zcbJfBsB6h254o3NuoEkf0+UY1GpE9ioiQdENWv7odo69s8iaGBEQ4BDpsIMqcuiiUXw1uKIVNxCB1gUGYz8lw==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.955.0.tgz", + "integrity": "sha512-RBi6CQHbPF09kqXAoiEOOPkVnSoU5YppKoOt/cgsWfoMHwC+7itIrEv+yRD62h14jIjF3KngVIQIrBRbX3o3/Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.954.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.954.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.4.0", + "@smithy/middleware-retry": "^4.4.16", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.15", + "@smithy/util-defaults-mode-node": "^4.2.18", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -800,17 +829,33 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.947.0.tgz", - "integrity": "sha512-UaYmzoxf9q3mabIA2hc4T6x5YSFUG2BpNjAZ207EA1bnQMiK+d6vZvb83t7dIWL/U1de1sGV19c1C81Jf14rrA==", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.953.0.tgz", + "integrity": "sha512-5MJgnsc+HLO+le0EK1cy92yrC7kyhGZSpaq8PcQvKs9qtXCXT5Tb6tMdkr5Y07JxYsYOV1omWBynvL6PWh08tQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.954.0.tgz", + "integrity": "sha512-GJJbUaSlGrMSRWui3Oz8ByygpQlzDGm195yTKirgGyu4tfYrFr/QWrWT42EUktY/L4Irev1pdHTuLS+AGHO1gw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/signature-v4": "^5.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -818,33 +863,86 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.948.0.tgz", - "integrity": "sha512-V487/kM4Teq5dcr1t5K6eoUKuqlGr9FRWL3MIMukMERJXHZvio6kox60FZ/YtciRHRI75u14YUqm2Dzddcu3+A==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.955.0.tgz", + "integrity": "sha512-LVpWkxXvMPgZofP2Gc8XBfQhsyecBMVARDHWMvks6vPbCLSTM7dw6H1HI9qbGNCurYcyc2xBRAkEDhChQlbPPg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/nested-clients": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.947.0.tgz", - "integrity": "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.953.0.tgz", + "integrity": "sha512-9hqdKkn4OvYzzaLryq2xnwcrPc8ziY34i9szUdgBfSqEC6pBxbY9/lLXmrgzfwMSL2Z7/v2go4Od0p5eukKLMQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.953.0.tgz", + "integrity": "sha512-rjaS6jrFksopXvNg6YeN+D1lYwhcByORNlFuYesFvaQNtPOufbE5tJL4GJ3TMXyaY0uFR28N5BHHITPyWWfH/g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", + "@smithy/util-endpoints": "^3.2.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.953.0.tgz", + "integrity": "sha512-UF5NeqYesWuFao+u7LJvpV1SJCaLml5BtFZKUdTnNNMeN6jvV+dW/eQoFGpXF94RCqguX0XESmRuRRPQp+/rzQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.954.0.tgz", + "integrity": "sha512-fB5S5VOu7OFkeNzcblQlez4AjO5hgDFaa7phYt7716YWisY3RjAaQPlxgv+G3GltHHDJIfzEC5aRxdf62B9zMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -859,6 +957,20 @@ } } }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/xml-builder": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.953.0.tgz", + "integrity": "sha512-Zmrj21jQ2OeOJGr9spPiN00aQvXa/WUqRXcTVENhrMt+OFoSOfDFpYhUj9NQ09QmQ8KMWFoWuWW6iKurNqLvAA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-sesv2": { "version": "3.946.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.946.0.tgz", @@ -1153,16 +1265,16 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.936.0.tgz", - "integrity": "sha512-XLSVVfAorUxZh6dzF+HTOp4R1B5EQcdpGcPliWr0KUj2jukgjZEcqbBmjyMF/p9bmyQsONX80iURF1HLAlW0qg==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.953.0.tgz", + "integrity": "sha512-YHVRIOowtGIl/L2WuS83FgRlm31tU0aL1yryWaFtF+AFjA5BIeiFkxIZqaRGxJpJvFEBdohsyq6Ipv5mgWfezg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-arn-parser": "3.953.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" }, @@ -1170,15 +1282,53 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.936.0.tgz", - "integrity": "sha512-Eb4ELAC23bEQLJmUMYnPWcjD3FZIsmz2svDiXEcxRkQU9r7NRID7pM7C5NPH94wOfiCk0b2Y8rVyFXW0lGQwbA==", + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.953.0.tgz", + "integrity": "sha512-9hqdKkn4OvYzzaLryq2xnwcrPc8ziY34i9szUdgBfSqEC6pBxbY9/lLXmrgzfwMSL2Z7/v2go4Od0p5eukKLMQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.953.0.tgz", + "integrity": "sha512-BQTVXrypQ0rbb7au/Hk4IS5GaJZlwk6O44Rjk6Kxb0IvGQhSurNTuesFiJx1sLbf+w+T31saPtODcfQQERqhCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.953.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -1186,22 +1336,22 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.947.0.tgz", - "integrity": "sha512-kXXxS2raNESNO+zR0L4YInVjhcGGNI2Mx0AE1ThRhDkAt2se3a+rGf9equ9YvOqA1m8Jl/GSI8cXYvSxXmS9Ag==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.954.0.tgz", + "integrity": "sha512-hHOPDJyxucNodkgapLhA0VdwDBwVYN9DX20aA6j+3nwutAlZ5skaV7Bw0W3YC7Fh/ieDKKhcSZulONd4lVTwMg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", "@smithy/is-array-buffer": "^4.2.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -1210,22 +1360,22 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", - "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.954.0.tgz", + "integrity": "sha512-5oYO5RP+mvCNXNj8XnF9jZo0EP0LTseYOJVNQYcii1D9DJqzHL3HJWurYh7cXxz7G7eDyvVYA01O9Xpt34TdoA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.7", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/xml-builder": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/signature-v4": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-middleware": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -1233,10 +1383,38 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/xml-builder": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.953.0.tgz", + "integrity": "sha512-Zmrj21jQ2OeOJGr9spPiN00aQvXa/WUqRXcTVENhrMt+OFoSOfDFpYhUj9NQ09QmQ8KMWFoWuWW6iKurNqLvAA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1249,13 +1427,26 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.936.0.tgz", - "integrity": "sha512-SCMPenDtQMd9o5da9JzkHz838w3327iqXk3cbNnXWqnNRx6unyW8FL0DZ84gIY12kAyVHz5WEqlWuekc15ehfw==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.953.0.tgz", + "integrity": "sha512-h0urrbteIQEybyIISaJfQLZ/+/lJPRzPWAQT4epvzfgv/4MKZI7K83dK7SfTwAooVKFBHiCMok2Cf0iHDt07Kw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -1266,6 +1457,7 @@ "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1320,13 +1512,26 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.936.0.tgz", - "integrity": "sha512-/GLC9lZdVp05ozRik5KsuODR/N7j+W+2TbfdFL3iS+7un+gnP6hC8RDOZd6WhpZp7drXQ9guKiTAxkZQwzS8DA==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.953.0.tgz", + "integrity": "sha512-OrhG1kcQ9zZh3NS3RovR028N0+UndQ957zF1k5HPLeFLwFwQN1uPOufzzPzAyXIIKtR69ARFsQI4mstZS4DMvw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -1406,6 +1611,7 @@ "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1472,6 +1678,7 @@ "version": "3.893.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1484,6 +1691,7 @@ "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1512,6 +1720,7 @@ "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1549,6 +1758,7 @@ "version": "3.930.0", "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.9.0", @@ -1943,9 +2153,9 @@ "license": "MIT" }, "node_modules/@dotenvx/dotenvx": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.1.tgz", - "integrity": "sha512-fqcQxcxC4LOaUlW8IkyWw8x0yirlLUkbxohz9OnWvVWjf73J5yyw7jxWnkOJaUKXZotcGEScDox9MU6rSkcDgg==", + "version": "1.51.2", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.2.tgz", + "integrity": "sha512-+693mNflujDZxudSEqSNGpn92QgFhJlBn9q2mDQ9yGWyHuz3hZ8B5g3EXCwdAz4DMJAI+OFCIbfEFZS+YRdrEA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2472,9 +2682,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -2489,9 +2699,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -2506,9 +2716,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -2523,9 +2733,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -2540,9 +2750,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -2557,9 +2767,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -2574,9 +2784,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -2591,9 +2801,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -2608,9 +2818,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -2625,9 +2835,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -2642,9 +2852,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -2659,9 +2869,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -2676,9 +2886,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -2693,9 +2903,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -2710,9 +2920,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -2727,9 +2937,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -2744,9 +2954,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -2761,9 +2971,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -2778,9 +2988,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -2795,9 +3005,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -2812,9 +3022,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -2829,9 +3039,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -2846,9 +3056,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -2863,9 +3073,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -2880,9 +3090,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -2897,9 +3107,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -3026,9 +3236,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3841,9 +4051,9 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.8.tgz", - "integrity": "sha512-1miV0qXDcLUaOdHridVPCh4i39ElRIAraseVIbb3BEqyZ5ol9sPyjTP/GNTPV5rBxqxjF6/vv5zQTVbhiNaLqA==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.0.tgz", + "integrity": "sha512-sooC/k0LCF4/jLXYHpgfzJot04lZQqsttn8XJpTguP8N3GhqXN3wSkh68no2OcZzS/qeGwKDFTqhZ8WofdXmmQ==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" @@ -4645,6 +4855,313 @@ "integrity": "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==", "license": "MIT" }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/@peculiar/asn1-android": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.6.0.tgz", @@ -4805,9 +5322,9 @@ } }, "node_modules/@posthog/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.7.1.tgz", - "integrity": "sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.8.1.tgz", + "integrity": "sha512-jfzBtQIk9auRi/biO+G/gumK5KxqsD5wOr7XpYMROE/I3pazjP4zIziinp21iQuIQJMXrDvwt9Af3njgOGwtew==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6" @@ -7109,9 +7626,9 @@ } }, "node_modules/@react-email/components": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.1.tgz", - "integrity": "sha512-HnL0Y/up61sOBQT2cQg9N/kCoW0bP727gDs2MkFWQYELg6+iIHidMDvENXFC0f1ZE6hTB+4t7sszptvTcJWsDA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.2.tgz", + "integrity": "sha512-VKQR/motrySQMvy+ZUwPjdeD9iI9mCt8cfXuJAX8cK16rtzkEe12yq6/pXyW7c6qEMj7d+PNsoAcO+3AbJSfPg==", "license": "MIT", "dependencies": { "@react-email/body": "0.2.0", @@ -7132,11 +7649,11 @@ "@react-email/render": "2.0.0", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", - "@react-email/tailwind": "2.0.1", + "@react-email/tailwind": "2.0.2", "@react-email/text": "0.1.5" }, "engines": { - "node": ">=22.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" @@ -7304,15 +7821,15 @@ } }, "node_modules/@react-email/tailwind": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.1.tgz", - "integrity": "sha512-/xq0IDYVY7863xPY7cdI45Xoz7M6CnIQBJcQvbqN7MNVpopfH9f+mhjayV1JGfKaxlGWuxfLKhgi9T2shsnEFg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.2.tgz", + "integrity": "sha512-ooi1H77+w+MN3a3Yps66GYTMoo9PvLtzJ1bTEI+Ta58MUUEQOcdxxXPwbnox+xj2kSwv0g/B63qquNTabKI8Bw==", "license": "MIT", "dependencies": { - "tailwindcss": "^4.1.12" + "tailwindcss": "^4.1.18" }, "engines": { - "node": ">=22.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "@react-email/body": "0.2.0", @@ -7462,12 +7979,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", - "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz", + "integrity": "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7500,16 +8017,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", - "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.5.tgz", + "integrity": "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" }, "engines": { @@ -7517,18 +8034,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.18.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.7.tgz", - "integrity": "sha512-axG9MvKhMWOhFbvf5y2DuyTxQueO0dkedY9QC3mAfndLosRI/9LJv8WaL0mw7ubNhsO4IuXX9/9dYGPFvHrqlw==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.0.tgz", + "integrity": "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.6", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -7538,15 +8055,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", - "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.7.tgz", + "integrity": "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", "tslib": "^2.6.2" }, "engines": { @@ -7554,13 +8071,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.5.tgz", - "integrity": "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.7.tgz", + "integrity": "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" }, @@ -7569,13 +8086,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.5.tgz", - "integrity": "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.7.tgz", + "integrity": "sha512-ujzPk8seYoDBmABDE5YqlhQZAXLOrtxtJLrbhHMKjBoG5b4dK4i6/mEU+6/7yXIAkqOO8sJ6YxZl+h0QQ1IJ7g==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/eventstream-serde-universal": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7583,12 +8100,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.5.tgz", - "integrity": "sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.7.tgz", + "integrity": "sha512-x7BtAiIPSaNaWuzm24Q/mtSkv+BrISO/fmheiJ39PKRNH3RmH2Hph/bUKSOBOBC9unqfIYDhKTHwpyZycLGPVQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7596,13 +8113,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.5.tgz", - "integrity": "sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.7.tgz", + "integrity": "sha512-roySCtHC5+pQq5lK4be1fZ/WR6s/AxnPaLfCODIPArtN2du8s5Ot4mKVK3pPtijL/L654ws592JHJ1PbZFF6+A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/eventstream-serde-universal": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7610,13 +8127,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.5.tgz", - "integrity": "sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.7.tgz", + "integrity": "sha512-QVD+g3+icFkThoy4r8wVFZMsIP08taHVKjE6Jpmz8h5CgX/kk6pTODq5cht0OMtcapUx+xrPzUTQdA+TmO0m1g==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/eventstream-codec": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7624,14 +8141,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", - "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.8.tgz", + "integrity": "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, @@ -7640,14 +8157,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.6.tgz", - "integrity": "sha512-8P//tA8DVPk+3XURk2rwcKgYwFvwGwmJH/wJqQiSKwXZtf/LiZK+hbUZmPj/9KzM+OVSwe4o85KTp5x9DUZTjw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.8.tgz", + "integrity": "sha512-07InZontqsM1ggTCPSRgI7d8DirqRrnpL7nIACT4PW0AWrgDiHhjGZzbAE5UtRSiU0NISGUYe7/rri9ZeWyDpw==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7655,12 +8172,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", - "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.7.tgz", + "integrity": "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -7670,12 +8187,12 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.5.tgz", - "integrity": "sha512-6+do24VnEyvWcGdHXomlpd0m8bfZePpUKBy7m311n+JuRwug8J4dCanJdTymx//8mi0nlkflZBvJe+dEO/O12Q==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.7.tgz", + "integrity": "sha512-ZQVoAwNYnFMIbd4DUc517HuwNelJUY6YOzwqrbcAgCnVn+79/OK7UjwA93SPpdTOpKDVkLIzavWm/Ck7SmnDPQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -7684,12 +8201,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", - "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.7.tgz", + "integrity": "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7709,12 +8226,12 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.5.tgz", - "integrity": "sha512-Bt6jpSTMWfjCtC0s79gZ/WZ1w90grfmopVOWqkI2ovhjpD5Q2XRXuecIPB9689L2+cCySMbaXDhBPU56FKNDNg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.7.tgz", + "integrity": "sha512-Wv6JcUxtOLTnxvNjDnAiATUsk8gvA6EeS8zzHig07dotpByYsLot+m0AaQEniUBjx97AC41MQR4hW0baraD1Xw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -7723,13 +8240,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", - "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.7.tgz", + "integrity": "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7737,18 +8254,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.14.tgz", - "integrity": "sha512-v0q4uTKgBM8dsqGjqsabZQyH85nFaTnFcgpWU1uydKFsdyyMzfvOkNum9G7VK+dOP01vUnoZxIeRiJ6uD0kjIg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.1.tgz", + "integrity": "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.18.7", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-middleware": "^4.2.5", + "@smithy/core": "^3.20.0", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" }, "engines": { @@ -7756,18 +8273,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.14.tgz", - "integrity": "sha512-Z2DG8Ej7FyWG1UA+7HceINtSLzswUgs2np3sZX0YBBxCt+CXG4QUxv88ZDS3+2/1ldW7LqtSY1UO/6VQ1pND8Q==", + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.17.tgz", + "integrity": "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/service-error-classification": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/service-error-classification": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, @@ -7776,13 +8293,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", - "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.8.tgz", + "integrity": "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7790,12 +8307,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", - "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.7.tgz", + "integrity": "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7803,14 +8320,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", - "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz", + "integrity": "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7818,15 +8335,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", - "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.7.tgz", + "integrity": "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/abort-controller": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7834,12 +8351,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", - "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.7.tgz", + "integrity": "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7847,12 +8364,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", - "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.7.tgz", + "integrity": "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7860,12 +8377,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", - "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.7.tgz", + "integrity": "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -7874,12 +8391,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", - "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.7.tgz", + "integrity": "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7887,24 +8404,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", - "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.7.tgz", + "integrity": "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0" + "@smithy/types": "^4.11.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", - "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz", + "integrity": "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7912,16 +8429,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", - "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.7.tgz", + "integrity": "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-middleware": "^4.2.7", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -7931,17 +8448,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.9.10", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.10.tgz", - "integrity": "sha512-Jaoz4Jw1QYHc1EFww/E6gVtNjhoDU+gwRKqXP6C3LKYqqH2UQhP8tMP3+t/ePrhaze7fhLE8vS2q6vVxBANFTQ==", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.2.tgz", + "integrity": "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.18.7", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", + "@smithy/core": "^3.20.0", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" }, "engines": { @@ -7949,9 +8466,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", - "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.11.0.tgz", + "integrity": "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7961,13 +8478,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", - "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.7.tgz", + "integrity": "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/querystring-parser": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8038,14 +8555,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.13", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.13.tgz", - "integrity": "sha512-hlVLdAGrVfyNei+pKIgqDTxfu/ZI2NSyqj4IDxKd5bIsIqwR/dSlkxlPaYxFiIaDVrBy0he8orsFy+Cz119XvA==", + "version": "4.3.16", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.16.tgz", + "integrity": "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8053,17 +8570,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.16.tgz", - "integrity": "sha512-F1t22IUiJLHrxW9W1CQ6B9PN+skZ9cqSuzB18Eh06HrJPbjsyZ7ZHecAKw80DQtyGTRcVfeukKaCRYebFwclbg==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.19.tgz", + "integrity": "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.3", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8071,13 +8588,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", - "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz", + "integrity": "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8097,12 +8614,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", - "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.7.tgz", + "integrity": "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8110,13 +8627,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", - "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.7.tgz", + "integrity": "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/service-error-classification": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8124,14 +8641,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", - "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.8.tgz", + "integrity": "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/types": "^4.9.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", @@ -8168,13 +8685,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.5.tgz", - "integrity": "sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.7.tgz", + "integrity": "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/abort-controller": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8445,9 +8962,9 @@ } }, "node_modules/@tailwindcss/forms": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", - "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz", + "integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==", "license": "MIT", "dependencies": { "mini-svg-data-uri": "^1.2.3" @@ -8457,9 +8974,9 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", - "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8469,37 +8986,37 @@ "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.17" + "tailwindcss": "4.1.18" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", - "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", "dev": true, "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.17", - "@tailwindcss/oxide-darwin-arm64": "4.1.17", - "@tailwindcss/oxide-darwin-x64": "4.1.17", - "@tailwindcss/oxide-freebsd-x64": "4.1.17", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", - "@tailwindcss/oxide-linux-x64-musl": "4.1.17", - "@tailwindcss/oxide-wasm32-wasi": "4.1.17", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", - "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", "cpu": [ "arm64" ], @@ -8514,9 +9031,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", - "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", "cpu": [ "arm64" ], @@ -8531,9 +9048,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", - "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", "cpu": [ "x64" ], @@ -8548,9 +9065,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", - "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", "cpu": [ "x64" ], @@ -8565,9 +9082,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", - "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", "cpu": [ "arm" ], @@ -8582,9 +9099,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", - "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", "cpu": [ "arm64" ], @@ -8599,9 +9116,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", - "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", "cpu": [ "arm64" ], @@ -8616,9 +9133,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", - "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", "cpu": [ "x64" ], @@ -8633,9 +9150,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", - "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", "cpu": [ "x64" ], @@ -8650,9 +9167,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", - "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -8668,10 +9185,10 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.6.0", - "@emnapi/runtime": "^1.6.0", + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.0.7", + "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, @@ -8680,7 +9197,7 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.6.0", + "version": "1.7.1", "dev": true, "inBundle": true, "license": "MIT", @@ -8691,7 +9208,7 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.6.0", + "version": "1.7.1", "dev": true, "inBundle": true, "license": "MIT", @@ -8711,14 +9228,14 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", + "version": "1.1.0", "dev": true, "inBundle": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, @@ -8740,9 +9257,9 @@ "optional": true }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", - "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", "cpu": [ "arm64" ], @@ -8757,9 +9274,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", - "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", "cpu": [ "x64" ], @@ -8774,17 +9291,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", - "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.17", - "@tailwindcss/oxide": "4.1.17", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", - "tailwindcss": "4.1.17" + "tailwindcss": "4.1.18" } }, "node_modules/@tanstack/query-core": { @@ -12597,9 +13114,9 @@ } }, "node_modules/drizzle-orm": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.0.tgz", - "integrity": "sha512-lyd9VRk3SXKRjV/gQckQzmJgkoYMvVG3A2JAV0vh3L+Lwk+v9+rK5Gj0H22y+ZBmxsrRBgJ5/RbQCN7DWd1dtQ==", + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz", + "integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -12940,9 +13457,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13154,9 +13671,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -13168,32 +13685,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.1", - "@esbuild/android-arm": "0.27.1", - "@esbuild/android-arm64": "0.27.1", - "@esbuild/android-x64": "0.27.1", - "@esbuild/darwin-arm64": "0.27.1", - "@esbuild/darwin-x64": "0.27.1", - "@esbuild/freebsd-arm64": "0.27.1", - "@esbuild/freebsd-x64": "0.27.1", - "@esbuild/linux-arm": "0.27.1", - "@esbuild/linux-arm64": "0.27.1", - "@esbuild/linux-ia32": "0.27.1", - "@esbuild/linux-loong64": "0.27.1", - "@esbuild/linux-mips64el": "0.27.1", - "@esbuild/linux-ppc64": "0.27.1", - "@esbuild/linux-riscv64": "0.27.1", - "@esbuild/linux-s390x": "0.27.1", - "@esbuild/linux-x64": "0.27.1", - "@esbuild/netbsd-arm64": "0.27.1", - "@esbuild/netbsd-x64": "0.27.1", - "@esbuild/openbsd-arm64": "0.27.1", - "@esbuild/openbsd-x64": "0.27.1", - "@esbuild/openharmony-arm64": "0.27.1", - "@esbuild/sunos-x64": "0.27.1", - "@esbuild/win32-arm64": "0.27.1", - "@esbuild/win32-ia32": "0.27.1", - "@esbuild/win32-x64": "0.27.1" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/esbuild-node-externals": { @@ -13253,9 +13770,9 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "license": "MIT", "peer": true, "dependencies": { @@ -13265,7 +13782,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -13313,12 +13830,12 @@ } }, "node_modules/eslint-config-next": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.8.tgz", - "integrity": "sha512-8J5cOAboXIV3f8OD6BOyj7Fik6n/as7J4MboiUSExWruf/lCu1OPR3ZVSdnta6WhzebrmAATEmNSBZsLWA6kbg==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.0.tgz", + "integrity": "sha512-RlPb8E2uO/Ix/w3kizxz6+6ogw99WqtNzTG0ArRZ5NEkIYcsfRb8U0j7aTG7NjRvcrsak5QtUSuxGNN2UcA58g==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.0.8", + "@next/eslint-plugin-next": "16.1.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -15932,9 +16449,9 @@ } }, "node_modules/lucide-react": { - "version": "0.559.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.559.0.tgz", - "integrity": "sha512-3ymrkBPXWk3U2bwUDg6TdA6hP5iGDMgPEAMLhchEgTQmA+g0Zk24tOtKtXMx35w1PizTmsBC3RhP88QYm+7mHQ==", + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -16344,9 +16861,9 @@ } }, "node_modules/next-intl": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.5.8.tgz", - "integrity": "sha512-BdN6494nvt09WtmW5gbWdwRhDDHC/Sg7tBMhN7xfYds3vcRCngSDXat81gmJkblw9jYOv8zXzzFJyu5VYXnJzg==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.6.1.tgz", + "integrity": "sha512-KlWgWtKLBPUsTPgxqwyjws1wCMD2QKxLlVjeeGj53DC1JWfKmBShKOrhIP0NznZrRQ0GleeoDUeHSETmyyIFeA==", "funding": [ { "type": "individual", @@ -16356,11 +16873,12 @@ "license": "MIT", "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", + "@parcel/watcher": "^2.4.1", "@swc/core": "^1.15.2", "negotiator": "^1.0.0", - "next-intl-swc-plugin-extractor": "^4.5.8", - "po-parser": "^1.0.2", - "use-intl": "^4.5.8" + "next-intl-swc-plugin-extractor": "^4.6.1", + "po-parser": "^2.0.0", + "use-intl": "^4.6.1" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", @@ -16374,9 +16892,9 @@ } }, "node_modules/next-intl-swc-plugin-extractor": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.5.8.tgz", - "integrity": "sha512-hscCKUv+5GQ0CCNbvqZ8gaxnAGToCgDTbL++jgCq8SCk/ljtZDEeQZcMk46Nm6Ynn49Q/JKF4Npo/Sq1mpbusA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.6.1.tgz", + "integrity": "sha512-+HHNeVERfSvuPDF7LYVn3pxst5Rf7EYdUTw7C7WIrYhcLaKiZ1b9oSRkTQddAN3mifDMCfHqO4kAQ/pcKiBl3A==", "license": "MIT" }, "node_modules/next-themes": { @@ -16456,6 +16974,12 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-cache": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", @@ -19358,9 +19882,9 @@ } }, "node_modules/po-parser": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-1.0.2.tgz", - "integrity": "sha512-yTIQL8PZy7V8c0psPoJUx7fayez+Mo/53MZgX9MPuPHx+Dt+sRPNuRbI+6Oqxnddhkd68x4Nlgon/zizL1Xg+w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.0.0.tgz", + "integrity": "sha512-SZvoKi3PoI/hHa2V9je9CW7Xgxl4dvO74cvaa6tWShIHT51FkPxje6pt0gTJznJrU67ix91nDaQp2hUxkOYhKA==", "license": "MIT" }, "node_modules/possible-typed-array-names": { @@ -19441,12 +19965,12 @@ } }, "node_modules/posthog-node": { - "version": "5.17.2", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.17.2.tgz", - "integrity": "sha512-lz3YJOr0Nmiz0yHASaINEDHqoV+0bC3eD8aZAG+Ky292dAnVYul+ga/dMX8KCBXg8hHfKdxw0SztYD5j6dgUqQ==", + "version": "5.17.4", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.17.4.tgz", + "integrity": "sha512-hrd+Do/DMt40By12ESIDUfD81V9OASjq9XHjycZrGiD8cX/ZwCIVSJLUb7nQmvSCWcKII+u+nnPVuc4LjTDl9g==", "license": "MIT", "dependencies": { - "@posthog/core": "1.7.1" + "@posthog/core": "1.8.1" }, "engines": { "node": ">=20" @@ -19747,9 +20271,9 @@ } }, "node_modules/react-day-picker": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.12.0.tgz", - "integrity": "sha512-t8OvG/Zrciso5CQJu5b1A7yzEmebvST+S3pOVQJWxwjjVngyG/CA2htN/D15dLI4uTEuLLkbZyS4YYt480FAtA==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.0.tgz", + "integrity": "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==", "license": "MIT", "dependencies": { "@date-fns/tz": "^1.4.1", @@ -19768,16 +20292,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", - "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.1" + "react": "^19.2.3" } }, "node_modules/react-easy-sort": { @@ -21936,9 +22460,9 @@ } }, "node_modules/stripe": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.0.0.tgz", - "integrity": "sha512-EaZeWpbJOCcDytdjKSwdrL5BxzbDGNueiCfHjHXlPdBQvLqoxl6AAivC35SPzTmVXJb5duXQlXFGS45H0+e6Gg==", + "version": "20.1.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.1.0.tgz", + "integrity": "sha512-o1VNRuMkY76ZCq92U3EH3/XHm/WHp7AerpzDs4Zyo8uE5mFL4QUcv/2SudWsSnhBSp4moO2+ZoGCZ7mT8crPmQ==", "license": "MIT", "dependencies": { "qs": "^6.11.0" @@ -22127,9 +22651,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", - "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "license": "MIT", "peer": true }, @@ -22780,9 +23304,9 @@ } }, "node_modules/use-intl": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.5.8.tgz", - "integrity": "sha512-rWPV2Sirw55BQbA/7ndUBtsikh8WXwBrUkZJ1mD35+emj/ogPPqgCZdv1DdrEFK42AjF1g5w8d3x8govhqPH6Q==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.6.1.tgz", + "integrity": "sha512-mUIj6QvJZ7Rk33mLDxRziz1YiBBAnIji8YW4TXXMdYHtaPEbVucrXD3iKQGAqJhbVn0VnjrEtIKYO1B18mfSJw==", "license": "MIT", "dependencies": { "@formatjs/fast-memoize": "^2.2.0", @@ -23319,9 +23843,9 @@ } }, "node_modules/zod": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", - "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", "peer": true, "funding": { diff --git a/package.json b/package.json index 5609b688..af6a8dde 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts", "db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts", "db:clear-migrations": "rm -rf server/migrations", - "set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json", - "set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json", - "set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json", + "set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json", + "set:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json", + "set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json", "set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts", "set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts", "next:build": "next build", @@ -34,7 +34,7 @@ }, "dependencies": { "@asteasolutions/zod-to-openapi": "8.2.0", - "@aws-sdk/client-s3": "3.948.0", + "@aws-sdk/client-s3": "3.955.0", "@faker-js/faker": "10.1.0", "@headlessui/react": "2.2.9", "@hookform/resolvers": "5.2.2", @@ -60,12 +60,12 @@ "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-tooltip": "1.2.8", - "@react-email/components": "1.0.1", + "@react-email/components": "1.0.2", "@react-email/render": "2.0.0", - "@react-email/tailwind": "2.0.1", + "@react-email/tailwind": "2.0.2", "@simplewebauthn/browser": "13.2.2", "@simplewebauthn/server": "13.2.2", - "@tailwindcss/forms": "0.5.10", + "@tailwindcss/forms": "0.5.11", "@tanstack/react-query": "5.90.12", "@tanstack/react-table": "8.21.3", "arctic": "3.7.0", @@ -82,9 +82,9 @@ "crypto-js": "4.2.0", "d3": "7.9.0", "date-fns": "4.1.0", - "drizzle-orm": "0.45.0", - "eslint": "9.39.1", - "eslint-config-next": "16.0.8", + "drizzle-orm": "0.45.1", + "eslint": "9.39.2", + "eslint-config-next": "16.1.0", "express": "5.2.1", "express-rate-limit": "8.2.1", "glob": "13.0.0", @@ -96,11 +96,11 @@ "jmespath": "0.16.0", "js-yaml": "4.1.1", "jsonwebtoken": "9.0.3", - "lucide-react": "0.559.0", + "lucide-react": "0.562.0", "maxmind": "5.0.1", "moment": "2.30.1", "next": "15.5.9", - "next-intl": "4.5.8", + "next-intl": "4.6.1", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", @@ -110,11 +110,11 @@ "nprogress": "0.2.0", "oslo": "1.2.1", "pg": "8.16.3", - "posthog-node": "5.17.2", + "posthog-node": "5.17.4", "qrcode.react": "4.2.0", "react": "19.2.3", - "react-day-picker": "9.12.0", - "react-dom": "19.2.1", + "react-day-picker": "9.13.0", + "react-dom": "19.2.3", "react-easy-sort": "1.8.0", "react-hook-form": "7.68.0", "react-icons": "5.5.0", @@ -123,7 +123,7 @@ "reodotdev": "1.0.0", "resend": "6.6.0", "semver": "7.7.3", - "stripe": "20.0.0", + "stripe": "20.1.0", "swagger-ui-express": "5.0.1", "tailwind-merge": "3.4.0", "topojson-client": "3.1.0", @@ -136,13 +136,13 @@ "ws": "8.18.3", "yaml": "2.8.2", "yargs": "18.0.0", - "zod": "4.1.13", + "zod": "4.2.1", "zod-validation-error": "5.0.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.51.1", + "@dotenvx/dotenvx": "1.51.2", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@tailwindcss/postcss": "4.1.17", + "@tailwindcss/postcss": "4.1.18", "@tanstack/react-query-devtools": "5.91.1", "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", @@ -167,12 +167,12 @@ "@types/js-yaml": "4.0.9", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.8", - "esbuild": "0.27.1", + "esbuild": "0.27.2", "esbuild-node-externals": "1.20.1", "postcss": "8.5.6", "prettier": "3.7.4", "react-email": "5.0.7", - "tailwindcss": "4.1.17", + "tailwindcss": "4.1.18", "tsc-alias": "1.8.16", "tsx": "4.21.0", "typescript": "5.9.3", diff --git a/server/db/asns.ts b/server/db/asns.ts new file mode 100644 index 00000000..f78577f5 --- /dev/null +++ b/server/db/asns.ts @@ -0,0 +1,321 @@ +// Curated list of major ASNs (Cloud Providers, CDNs, ISPs, etc.) +// This is not exhaustive - there are 100,000+ ASNs globally +// Users can still enter any ASN manually in the input field +export const MAJOR_ASNS = [ + { + name: "ALL ASNs", + code: "ALL", + asn: 0 // Special value that will match all + }, + // Major Cloud Providers + { + name: "Google LLC", + code: "AS15169", + asn: 15169 + }, + { + name: "Amazon AWS", + code: "AS16509", + asn: 16509 + }, + { + name: "Amazon AWS (EC2)", + code: "AS14618", + asn: 14618 + }, + { + name: "Microsoft Azure", + code: "AS8075", + asn: 8075 + }, + { + name: "Microsoft Corporation", + code: "AS8068", + asn: 8068 + }, + { + name: "DigitalOcean", + code: "AS14061", + asn: 14061 + }, + { + name: "Linode", + code: "AS63949", + asn: 63949 + }, + { + name: "Hetzner Online", + code: "AS24940", + asn: 24940 + }, + { + name: "OVH SAS", + code: "AS16276", + asn: 16276 + }, + { + name: "Oracle Cloud", + code: "AS31898", + asn: 31898 + }, + { + name: "Alibaba Cloud", + code: "AS45102", + asn: 45102 + }, + { + name: "IBM Cloud", + code: "AS36351", + asn: 36351 + }, + + // CDNs + { + name: "Cloudflare", + code: "AS13335", + asn: 13335 + }, + { + name: "Fastly", + code: "AS54113", + asn: 54113 + }, + { + name: "Akamai Technologies", + code: "AS20940", + asn: 20940 + }, + { + name: "Akamai (Primary)", + code: "AS16625", + asn: 16625 + }, + + // Mobile Carriers - US + { + name: "T-Mobile USA", + code: "AS21928", + asn: 21928 + }, + { + name: "Verizon Wireless", + code: "AS6167", + asn: 6167 + }, + { + name: "AT&T Mobility", + code: "AS20057", + asn: 20057 + }, + { + name: "Sprint (T-Mobile)", + code: "AS1239", + asn: 1239 + }, + { + name: "US Cellular", + code: "AS6430", + asn: 6430 + }, + + // Mobile Carriers - Europe + { + name: "Vodafone UK", + code: "AS25135", + asn: 25135 + }, + { + name: "EE (UK)", + code: "AS12576", + asn: 12576 + }, + { + name: "Three UK", + code: "AS29194", + asn: 29194 + }, + { + name: "O2 UK", + code: "AS13285", + asn: 13285 + }, + { + name: "Telefonica Spain Mobile", + code: "AS12430", + asn: 12430 + }, + + // Mobile Carriers - Asia + { + name: "NTT DoCoMo (Japan)", + code: "AS9605", + asn: 9605 + }, + { + name: "SoftBank Mobile (Japan)", + code: "AS17676", + asn: 17676 + }, + { + name: "SK Telecom (Korea)", + code: "AS9318", + asn: 9318 + }, + { + name: "KT Corporation Mobile (Korea)", + code: "AS4766", + asn: 4766 + }, + { + name: "Airtel India", + code: "AS24560", + asn: 24560 + }, + { + name: "China Mobile", + code: "AS9808", + asn: 9808 + }, + + // Major US ISPs + { + name: "AT&T Services", + code: "AS7018", + asn: 7018 + }, + { + name: "Comcast Cable", + code: "AS7922", + asn: 7922 + }, + { + name: "Verizon", + code: "AS701", + asn: 701 + }, + { + name: "Cox Communications", + code: "AS22773", + asn: 22773 + }, + { + name: "Charter Communications", + code: "AS20115", + asn: 20115 + }, + { + name: "CenturyLink", + code: "AS209", + asn: 209 + }, + + // Major European ISPs + { + name: "Deutsche Telekom", + code: "AS3320", + asn: 3320 + }, + { + name: "Vodafone", + code: "AS1273", + asn: 1273 + }, + { + name: "British Telecom", + code: "AS2856", + asn: 2856 + }, + { + name: "Orange", + code: "AS3215", + asn: 3215 + }, + { + name: "Telefonica", + code: "AS12956", + asn: 12956 + }, + + // Major Asian ISPs + { + name: "China Telecom", + code: "AS4134", + asn: 4134 + }, + { + name: "China Unicom", + code: "AS4837", + asn: 4837 + }, + { + name: "NTT Communications", + code: "AS2914", + asn: 2914 + }, + { + name: "KDDI Corporation", + code: "AS2516", + asn: 2516 + }, + { + name: "Reliance Jio (India)", + code: "AS55836", + asn: 55836 + }, + + // VPN/Proxy Providers + { + name: "Private Internet Access", + code: "AS46562", + asn: 46562 + }, + { + name: "NordVPN", + code: "AS202425", + asn: 202425 + }, + { + name: "Mullvad VPN", + code: "AS213281", + asn: 213281 + }, + + // Social Media / Major Tech + { + name: "Facebook/Meta", + code: "AS32934", + asn: 32934 + }, + { + name: "Twitter/X", + code: "AS13414", + asn: 13414 + }, + { + name: "Apple", + code: "AS714", + asn: 714 + }, + { + name: "Netflix", + code: "AS2906", + asn: 2906 + }, + + // Academic/Research + { + name: "MIT", + code: "AS3", + asn: 3 + }, + { + name: "Stanford University", + code: "AS32", + asn: 32 + }, + { + name: "CERN", + code: "AS513", + asn: 513 + } +]; diff --git a/server/db/maxmindAsn.ts b/server/db/maxmindAsn.ts new file mode 100644 index 00000000..13951262 --- /dev/null +++ b/server/db/maxmindAsn.ts @@ -0,0 +1,13 @@ +import maxmind, { AsnResponse, Reader } from "maxmind"; +import config from "@server/lib/config"; + +let maxmindAsnLookup: Reader | null; +if (config.getRawConfig().server.maxmind_asn_path) { + maxmindAsnLookup = await maxmind.open( + config.getRawConfig().server.maxmind_asn_path! + ); +} else { + maxmindAsnLookup = null; +} + +export { maxmindAsnLookup }; diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 9456effb..5b357d06 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -6,28 +6,28 @@ import { withReplicas } from "drizzle-orm/pg-core"; function createDb() { const config = readConfigFile(); - if (!config.postgres) { - // check the environment variables for postgres config - if (process.env.POSTGRES_CONNECTION_STRING) { - config.postgres = { - connection_string: process.env.POSTGRES_CONNECTION_STRING - }; - if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) { - const replicas = - process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split( - "," - ).map((conn) => ({ + // check the environment variables for postgres config first before the config file + if (process.env.POSTGRES_CONNECTION_STRING) { + config.postgres = { + connection_string: process.env.POSTGRES_CONNECTION_STRING + }; + if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) { + const replicas = + process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map( + (conn) => ({ connection_string: conn.trim() - })); - config.postgres.replicas = replicas; - } - } else { - throw new Error( - "Postgres configuration is missing in the configuration file." - ); + }) + ); + config.postgres.replicas = replicas; } } + if (!config.postgres) { + throw new Error( + "Postgres configuration is missing in the configuration file." + ); + } + const connectionString = config.postgres?.connection_string; const replicaConnections = config.postgres?.replicas || []; @@ -81,6 +81,7 @@ function createDb() { export const db = createDb(); export default db; +export const primaryDb = db.$primary; export type Transaction = Parameters< Parameters<(typeof db)["transaction"]>[0] >[0]; diff --git a/server/db/pg/migrate.ts b/server/db/pg/migrate.ts index 8bbcceb7..2d2abca3 100644 --- a/server/db/pg/migrate.ts +++ b/server/db/pg/migrate.ts @@ -10,7 +10,7 @@ const runMigrations = async () => { await migrate(db as any, { migrationsFolder: migrationsFolder }); - console.log("Migrations completed successfully."); + console.log("Migrations completed successfully. ✅"); process.exit(0); } catch (error) { console.error("Error running migrations:", error); diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index cb809b71..1f30dbf5 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -204,6 +204,29 @@ export const loginPageOrg = pgTable("loginPageOrg", { .references(() => orgs.orgId, { onDelete: "cascade" }) }); +export const loginPageBranding = pgTable("loginPageBranding", { + loginPageBrandingId: serial("loginPageBrandingId").primaryKey(), + logoUrl: text("logoUrl").notNull(), + logoWidth: integer("logoWidth").notNull(), + logoHeight: integer("logoHeight").notNull(), + primaryColor: text("primaryColor"), + resourceTitle: text("resourceTitle").notNull(), + resourceSubtitle: text("resourceSubtitle"), + orgTitle: text("orgTitle"), + orgSubtitle: text("orgSubtitle") +}); + +export const loginPageBrandingOrg = pgTable("loginPageBrandingOrg", { + loginPageBrandingId: integer("loginPageBrandingId") + .notNull() + .references(() => loginPageBranding.loginPageBrandingId, { + onDelete: "cascade" + }), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + export const sessionTransferToken = pgTable("sessionTransferToken", { token: varchar("token").primaryKey(), sessionId: varchar("sessionId") @@ -283,5 +306,6 @@ export type RemoteExitNodeSession = InferSelectModel< >; export type ExitNodeOrg = InferSelectModel; export type LoginPage = InferSelectModel; +export type LoginPageBranding = InferSelectModel; export type ActionAuditLog = InferSelectModel; export type AccessAuditLog = InferSelectModel; diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 71877f2f..c689a35a 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -7,7 +7,8 @@ import { bigint, real, text, - index + index, + uniqueIndex } from "drizzle-orm/pg-core"; import { InferSelectModel } from "drizzle-orm"; import { randomUUID } from "crypto"; @@ -213,7 +214,10 @@ export const siteResources = pgTable("siteResources", { destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode enabled: boolean("enabled").notNull().default(true), alias: varchar("alias"), - aliasAddress: varchar("aliasAddress") + aliasAddress: varchar("aliasAddress"), + tcpPortRangeString: varchar("tcpPortRangeString"), + udpPortRangeString: varchar("udpPortRangeString"), + disableIcmp: boolean("disableIcmp").notNull().default(false) }); export const clientSiteResources = pgTable("clientSiteResources", { diff --git a/server/db/sqlite/driver.ts b/server/db/sqlite/driver.ts index 0f696df6..9cbc8d7b 100644 --- a/server/db/sqlite/driver.ts +++ b/server/db/sqlite/driver.ts @@ -20,6 +20,7 @@ function createDb() { export const db = createDb(); export default db; +export const primaryDb = db; export type Transaction = Parameters< Parameters<(typeof db)["transaction"]>[0] >[0]; diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index 975a949b..af7d021d 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -1,13 +1,12 @@ -import { - sqliteTable, - integer, - text, - real, - index -} from "drizzle-orm/sqlite-core"; import { InferSelectModel } from "drizzle-orm"; -import { domains, orgs, targets, users, exitNodes, sessions } from "./schema"; -import { metadata } from "@app/app/[orgId]/settings/layout"; +import { + index, + integer, + real, + sqliteTable, + text +} from "drizzle-orm/sqlite-core"; +import { domains, exitNodes, orgs, sessions, users } from "./schema"; export const certificates = sqliteTable("certificates", { certId: integer("certId").primaryKey({ autoIncrement: true }), @@ -203,6 +202,31 @@ export const loginPageOrg = sqliteTable("loginPageOrg", { .references(() => orgs.orgId, { onDelete: "cascade" }) }); +export const loginPageBranding = sqliteTable("loginPageBranding", { + loginPageBrandingId: integer("loginPageBrandingId").primaryKey({ + autoIncrement: true + }), + logoUrl: text("logoUrl").notNull(), + logoWidth: integer("logoWidth").notNull(), + logoHeight: integer("logoHeight").notNull(), + primaryColor: text("primaryColor"), + resourceTitle: text("resourceTitle").notNull(), + resourceSubtitle: text("resourceSubtitle"), + orgTitle: text("orgTitle"), + orgSubtitle: text("orgSubtitle") +}); + +export const loginPageBrandingOrg = sqliteTable("loginPageBrandingOrg", { + loginPageBrandingId: integer("loginPageBrandingId") + .notNull() + .references(() => loginPageBranding.loginPageBrandingId, { + onDelete: "cascade" + }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + export const sessionTransferToken = sqliteTable("sessionTransferToken", { token: text("token").primaryKey(), sessionId: text("sessionId") @@ -282,5 +306,6 @@ export type RemoteExitNodeSession = InferSelectModel< >; export type ExitNodeOrg = InferSelectModel; export type LoginPage = InferSelectModel; +export type LoginPageBranding = InferSelectModel; export type ActionAuditLog = InferSelectModel; export type AccessAuditLog = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 6e17cac4..848289ee 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1,6 +1,12 @@ import { randomUUID } from "crypto"; import { InferSelectModel } from "drizzle-orm"; -import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; +import { + sqliteTable, + text, + integer, + index, + uniqueIndex +} from "drizzle-orm/sqlite-core"; import { no } from "zod/v4/locales"; export const domains = sqliteTable("domains", { @@ -234,7 +240,10 @@ export const siteResources = sqliteTable("siteResources", { destination: text("destination").notNull(), // ip, cidr, hostname enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), alias: text("alias"), - aliasAddress: text("aliasAddress") + aliasAddress: text("aliasAddress"), + tcpPortRangeString: text("tcpPortRangeString"), + udpPortRangeString: text("udpPortRangeString"), + disableIcmp: integer("disableIcmp", { mode: "boolean" }) }); export const clientSiteResources = sqliteTable("clientSiteResources", { diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index c8a0b077..32a5fb47 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -10,6 +10,7 @@ export async function sendEmail( from: string | undefined; to: string | undefined; subject: string; + replyTo?: string; } ) { if (!emailClient) { @@ -32,6 +33,7 @@ export async function sendEmail( address: opts.from }, to: opts.to, + replyTo: opts.replyTo, subject: opts.subject, html: emailHtml }); diff --git a/server/lib/asn.ts b/server/lib/asn.ts new file mode 100644 index 00000000..18a39c46 --- /dev/null +++ b/server/lib/asn.ts @@ -0,0 +1,29 @@ +import logger from "@server/logger"; +import { maxmindAsnLookup } from "@server/db/maxmindAsn"; + +export async function getAsnForIp(ip: string): Promise { + try { + if (!maxmindAsnLookup) { + logger.debug( + "MaxMind ASN DB path not configured, cannot perform ASN lookup" + ); + return; + } + + const result = maxmindAsnLookup.get(ip); + + if (!result || !result.autonomous_system_number) { + return; + } + + logger.debug( + `ASN lookup successful for IP ${ip}: AS${result.autonomous_system_number}` + ); + + return result.autonomous_system_number; + } catch (error) { + logger.error("Error performing ASN lookup:", error); + } + + return; +} diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts index 6168f85d..a3f977ab 100644 --- a/server/lib/blueprints/applyBlueprint.ts +++ b/server/lib/blueprints/applyBlueprint.ts @@ -1,4 +1,4 @@ -import { db, newts, blueprints, Blueprint } from "@server/db"; +import { db, newts, blueprints, Blueprint, Site, siteResources, roleSiteResources, userSiteResources, clientSiteResources } from "@server/db"; import { Config, ConfigSchema } from "./types"; import { ProxyResourcesResults, updateProxyResources } from "./proxyResources"; import { fromError } from "zod-validation-error"; @@ -15,6 +15,7 @@ import { BlueprintSource } from "@server/routers/blueprints/types"; import { stringify as stringifyYaml } from "yaml"; import { faker } from "@faker-js/faker"; import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource"; +import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations"; type ApplyBlueprintArgs = { orgId: string; @@ -108,38 +109,136 @@ export async function applyBlueprint({ // We need to update the targets on the newts from the successfully updated information for (const result of clientResourcesResults) { - const [site] = await trx - .select() - .from(sites) - .innerJoin(newts, eq(sites.siteId, newts.siteId)) - .where( - and( - eq(sites.siteId, result.newSiteResource.siteId), - eq(sites.orgId, orgId), - eq(sites.type, "newt"), - isNotNull(sites.pubKey) + if ( + result.oldSiteResource && + result.oldSiteResource.siteId != + result.newSiteResource.siteId + ) { + // query existing associations + const existingRoleIds = await trx + .select() + .from(roleSiteResources) + .where( + eq( + roleSiteResources.siteResourceId, + result.oldSiteResource.siteResourceId + ) ) - ) - .limit(1); + .then((rows) => rows.map((row) => row.roleId)); - if (!site) { - logger.debug( - `No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update` + const existingUserIds= await trx + .select() + .from(userSiteResources) + .where( + eq( + userSiteResources.siteResourceId, + result.oldSiteResource.siteResourceId + ) + ).then((rows) => rows.map((row) => row.userId)); + + const existingClientIds = await trx + .select() + .from(clientSiteResources) + .where( + eq( + clientSiteResources.siteResourceId, + result.oldSiteResource.siteResourceId + ) + ).then((rows) => rows.map((row) => row.clientId)); + + // delete the existing site resource + await trx + .delete(siteResources) + .where( + and(eq(siteResources.siteResourceId, result.oldSiteResource.siteResourceId)) + ); + + await rebuildClientAssociationsFromSiteResource( + result.oldSiteResource, + trx + ); + + const [insertedSiteResource] = await trx + .insert(siteResources) + .values({ + ...result.newSiteResource, + }) + .returning(); + + // wait some time to allow for messages to be handled + await new Promise((resolve) => setTimeout(resolve, 750)); + + //////////////////// update the associations //////////////////// + + if (existingRoleIds.length > 0) { + await trx.insert(roleSiteResources).values( + existingRoleIds.map((roleId) => ({ + roleId, + siteResourceId: insertedSiteResource!.siteResourceId + })) + ); + } + + if (existingUserIds.length > 0) { + await trx.insert(userSiteResources).values( + existingUserIds.map((userId) => ({ + userId, + siteResourceId: insertedSiteResource!.siteResourceId + })) + ); + } + + if (existingClientIds.length > 0) { + await trx.insert(clientSiteResources).values( + existingClientIds.map((clientId) => ({ + clientId, + siteResourceId: insertedSiteResource!.siteResourceId + })) + ); + } + + await rebuildClientAssociationsFromSiteResource( + insertedSiteResource, + trx + ); + + } else { + const [newSite] = await trx + .select() + .from(sites) + .innerJoin(newts, eq(sites.siteId, newts.siteId)) + .where( + and( + eq(sites.siteId, result.newSiteResource.siteId), + eq(sites.orgId, orgId), + eq(sites.type, "newt"), + isNotNull(sites.pubKey) + ) + ) + .limit(1); + + if (!newSite) { + logger.debug( + `No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update` + ); + continue; + } + + logger.debug( + `Updating client resource ${result.newSiteResource.siteResourceId} on site ${newSite.sites.siteId}` + ); + + await handleMessagingForUpdatedSiteResource( + result.oldSiteResource, + result.newSiteResource, + { + siteId: newSite.sites.siteId, + orgId: newSite.sites.orgId + }, + trx ); - continue; } - logger.debug( - `Updating client resource ${result.newSiteResource.siteResourceId} on site ${site.sites.siteId}` - ); - - await handleMessagingForUpdatedSiteResource( - result.oldSiteResource, - result.newSiteResource, - { siteId: site.sites.siteId, orgId: site.sites.orgId }, - trx - ); - // await addClientTargets( // site.newt.newtId, // result.resource.destination, diff --git a/server/lib/blueprints/clientResources.ts b/server/lib/blueprints/clientResources.ts index ab65336d..64de9867 100644 --- a/server/lib/blueprints/clientResources.ts +++ b/server/lib/blueprints/clientResources.ts @@ -14,6 +14,7 @@ import { sites } from "@server/db"; import { eq, and, ne, inArray } from "drizzle-orm"; import { Config } from "./types"; import logger from "@server/logger"; +import { getNextAvailableAliasAddress } from "../ip"; export type ClientResourcesResults = { newSiteResource: SiteResource; @@ -75,22 +76,20 @@ export async function updateClientResources( } if (existingResource) { - if (existingResource.siteId !== site.siteId) { - throw new Error( - `You can not change the site of an existing client resource (${resourceNiceId}). Please delete and recreate it instead.` - ); - } - // Update existing resource const [updatedResource] = await trx .update(siteResources) .set({ name: resourceData.name || resourceNiceId, + siteId: site.siteId, mode: resourceData.mode, destination: resourceData.destination, enabled: true, // hardcoded for now // enabled: resourceData.enabled ?? true, - alias: resourceData.alias || null + alias: resourceData.alias || null, + disableIcmp: resourceData["disable-icmp"], + tcpPortRangeString: resourceData["tcp-ports"], + udpPortRangeString: resourceData["udp-ports"] }) .where( eq( @@ -205,6 +204,12 @@ export async function updateClientResources( oldSiteResource: existingResource }); } else { + let aliasAddress: string | null = null; + if (resourceData.mode == "host") { + // we can only have an alias on a host + aliasAddress = await getNextAvailableAliasAddress(orgId); + } + // Create new resource const [newResource] = await trx .insert(siteResources) @@ -217,7 +222,11 @@ export async function updateClientResources( destination: resourceData.destination, enabled: true, // hardcoded for now // enabled: resourceData.enabled ?? true, - alias: resourceData.alias || null + alias: resourceData.alias || null, + aliasAddress: aliasAddress, + disableIcmp: resourceData["disable-icmp"], + tcpPortRangeString: resourceData["tcp-ports"], + udpPortRangeString: resourceData["udp-ports"] }) .returning(); diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 23e2176f..df6d7bb0 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { portRangeStringSchema } from "@server/lib/ip"; export const SiteSchema = z.object({ name: z.string().min(1).max(100), @@ -71,11 +72,71 @@ export const AuthSchema = z.object({ "auto-login-idp": z.int().positive().optional() }); -export const RuleSchema = z.object({ - action: z.enum(["allow", "deny", "pass"]), - match: z.enum(["cidr", "path", "ip", "country"]), - value: z.string() -}); +export const RuleSchema = z + .object({ + action: z.enum(["allow", "deny", "pass"]), + match: z.enum(["cidr", "path", "ip", "country", "asn"]), + value: z.string() + }) + .refine( + (rule) => { + if (rule.match === "ip") { + // Check if it's a valid IP address (v4 or v6) + return z.union([z.ipv4(), z.ipv6()]).safeParse(rule.value) + .success; + } + return true; + }, + { + path: ["value"], + message: "Value must be a valid IP address when match is 'ip'" + } + ) + .refine( + (rule) => { + if (rule.match === "cidr") { + // Check if it's a valid CIDR (v4 or v6) + return z.union([z.cidrv4(), z.cidrv6()]).safeParse(rule.value) + .success; + } + return true; + }, + { + path: ["value"], + message: "Value must be a valid CIDR notation when match is 'cidr'" + } + ) + .refine( + (rule) => { + if (rule.match === "country") { + // Check if it's a valid 2-letter country code + return /^[A-Z]{2}$/.test(rule.value); + } + return true; + }, + { + path: ["value"], + message: + "Value must be a 2-letter country code when match is 'country'" + } + ) + .refine( + (rule) => { + if (rule.match === "asn") { + // Check if it's either AS format or just a number + const asNumberPattern = /^AS\d+$/i; + const isASFormat = asNumberPattern.test(rule.value); + const isNumeric = /^\d+$/.test(rule.value); + return isASFormat || isNumeric; + } + return true; + }, + { + path: ["value"], + message: + "Value must be either 'AS' format or a number when match is 'asn'" + } + ); export const HeaderSchema = z.object({ name: z.string().min(1), @@ -222,6 +283,9 @@ export const ClientResourceSchema = z // destinationPort: z.int().positive().optional(), destination: z.string().min(1), // enabled: z.boolean().default(true), + "tcp-ports": portRangeStringSchema.optional().default("*"), + "udp-ports": portRangeStringSchema.optional().default("*"), + "disable-icmp": z.boolean().optional().default(false), alias: z .string() .regex( diff --git a/server/lib/config.ts b/server/lib/config.ts index 9874518e..405db2d1 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -99,6 +99,10 @@ export class Config { process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path; } + if (parsedConfig.server.maxmind_asn_path) { + process.env.MAXMIND_ASN_PATH = parsedConfig.server.maxmind_asn_path; + } + this.rawConfig = parsedConfig; } diff --git a/server/lib/consts.ts b/server/lib/consts.ts index d93cf224..d1f66a9e 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.13.0"; +export const APP_VERSION = "1.13.1"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 02683edc..87a0c3c6 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -1,10 +1,4 @@ -import { - clientSitesAssociationsCache, - db, - SiteResource, - siteResources, - Transaction -} from "@server/db"; +import { db, SiteResource, siteResources, Transaction } from "@server/db"; import { clients, orgs, sites } from "@server/db"; import { and, eq, isNotNull } from "drizzle-orm"; import config from "@server/lib/config"; @@ -307,6 +301,29 @@ export function isIpInCidr(ip: string, cidr: string): boolean { return ipBigInt >= range.start && ipBigInt <= range.end; } +/** + * Checks if two CIDR ranges overlap + * @param cidr1 First CIDR string + * @param cidr2 Second CIDR string + * @returns boolean indicating if the two CIDRs overlap + */ +export function doCidrsOverlap(cidr1: string, cidr2: string): boolean { + const version1 = detectIpVersion(cidr1.split("/")[0]); + const version2 = detectIpVersion(cidr2.split("/")[0]); + if (version1 !== version2) { + // Different IP versions cannot overlap + return false; + } + const range1 = cidrToRange(cidr1); + const range2 = cidrToRange(cidr2); + + // Overlap if the ranges intersect + return ( + range1.start <= range2.end && + range2.start <= range1.end + ); +} + export async function getNextAvailableClientSubnet( orgId: string, transaction: Transaction | typeof db = db @@ -432,7 +449,12 @@ export function generateRemoteSubnets( ): string[] { const remoteSubnets = allSiteResources .filter((sr) => { - if (sr.mode === "cidr") return true; + if (sr.mode === "cidr") { + // check if its a valid CIDR using zod + const cidrSchema = z.union([z.cidrv4(), z.cidrv6()]); + const parseResult = cidrSchema.safeParse(sr.destination); + return parseResult.success; + } if (sr.mode === "host") { // check if its a valid IP using zod const ipSchema = z.union([z.ipv4(), z.ipv6()]); @@ -456,22 +478,23 @@ export function generateRemoteSubnets( export type Alias = { alias: string | null; aliasAddress: string | null }; export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] { - let aliasConfigs = allSiteResources + return allSiteResources .filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host") .map((sr) => ({ alias: sr.alias, aliasAddress: sr.aliasAddress })); - return aliasConfigs; } export type SubnetProxyTarget = { sourcePrefix: string; // must be a cidr destPrefix: string; // must be a cidr + disableIcmp?: boolean; rewriteTo?: string; // must be a cidr portRange?: { min: number; max: number; + protocol: "tcp" | "udp"; }[]; }; @@ -501,6 +524,11 @@ export function generateSubnetProxyTargets( } const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`; + const portRange = [ + ...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"), + ...parsePortRangeString(siteResource.udpPortRangeString, "udp") + ]; + const disableIcmp = siteResource.disableIcmp ?? false; if (siteResource.mode == "host") { let destination = siteResource.destination; @@ -511,7 +539,9 @@ export function generateSubnetProxyTargets( targets.push({ sourcePrefix: clientPrefix, - destPrefix: destination + destPrefix: destination, + portRange, + disableIcmp }); } @@ -520,13 +550,17 @@ export function generateSubnetProxyTargets( targets.push({ sourcePrefix: clientPrefix, destPrefix: `${siteResource.aliasAddress}/32`, - rewriteTo: destination + rewriteTo: destination, + portRange, + disableIcmp }); } } else if (siteResource.mode == "cidr") { targets.push({ sourcePrefix: clientPrefix, - destPrefix: siteResource.destination + destPrefix: siteResource.destination, + portRange, + disableIcmp }); } } @@ -538,3 +572,117 @@ export function generateSubnetProxyTargets( return targets; } + +// Custom schema for validating port range strings +// Format: "80,443,8000-9000" or "*" for all ports, or empty string +export const portRangeStringSchema = z + .string() + .optional() + .refine( + (val) => { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + // Split by comma and validate each part + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; // empty parts not allowed + } + + // Check if it's a range (contains dash) + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + + // Both parts must be present + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + // Must be valid numbers + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + // Must be valid port range (1-65535) + if ( + startPort < 1 || + startPort > 65535 || + endPort < 1 || + endPort > 65535 + ) { + return false; + } + + // Start must be <= end + if (startPort > endPort) { + return false; + } + } else { + // Single port + const port = parseInt(part, 10); + + // Must be a valid number + if (isNaN(port)) { + return false; + } + + // Must be valid port range (1-65535) + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; + }, + { + message: + 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535, and ranges must have start <= end.' + } + ); + +/** + * Parses a port range string into an array of port range objects + * @param portRangeStr - Port range string (e.g., "80,443,8000-9000", "*", or "") + * @param protocol - Protocol to use for all ranges (default: "tcp") + * @returns Array of port range objects with min, max, and protocol fields + */ +export function parsePortRangeString( + portRangeStr: string | undefined | null, + protocol: "tcp" | "udp" = "tcp" +): { min: number; max: number; protocol: "tcp" | "udp" }[] { + // Handle undefined or empty string - insert dummy value with port 0 + if (!portRangeStr || portRangeStr.trim() === "") { + return [{ min: 0, max: 0, protocol }]; + } + + // Handle wildcard - return empty array (all ports allowed) + if (portRangeStr.trim() === "*") { + return []; + } + + const result: { min: number; max: number; protocol: "tcp" | "udp" }[] = []; + const parts = portRangeStr.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part.includes("-")) { + // Range + const [start, end] = part.split("-").map((p) => p.trim()); + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + result.push({ min: startPort, max: endPort, protocol }); + } else { + // Single port + const port = parseInt(part, 10); + result.push({ min: port, max: port, protocol }); + } + } + + return result; +} diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index fe610663..da567820 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -133,7 +133,8 @@ export const configSchema = z .optional(), trust_proxy: z.int().gte(0).optional().default(1), secret: z.string().pipe(z.string().min(8)).optional(), - maxmind_db_path: z.string().optional() + maxmind_db_path: z.string().optional(), + maxmind_asn_path: z.string().optional() }) .optional() .default({ @@ -255,11 +256,11 @@ export const configSchema = z orgs: z .object({ block_size: z.number().positive().gt(0).optional().default(24), - subnet_group: z.string().optional().default("100.90.128.0/24"), + subnet_group: z.string().optional().default("100.90.128.0/20"), utility_subnet_group: z .string() .optional() - .default("100.96.128.0/24") //just hardcode this for now as well + .default("100.96.128.0/20") //just hardcode this for now as well }) .optional() .default({ diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index e0867dc5..625e5793 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -955,28 +955,8 @@ export async function rebuildClientAssociationsFromClient( /////////// Send messages /////////// - // Get the olm for this client - const [olm] = await trx - .select({ olmId: olms.olmId }) - .from(olms) - .where(eq(olms.clientId, client.clientId)) - .limit(1); - - if (!olm) { - logger.warn( - `Olm not found for client ${client.clientId}, skipping peer updates` - ); - return; - } - // Handle messages for sites being added - await handleMessagesForClientSites( - client, - olm.olmId, - sitesToAdd, - sitesToRemove, - trx - ); + await handleMessagesForClientSites(client, sitesToAdd, sitesToRemove, trx); // Handle subnet proxy target updates for resources await handleMessagesForClientResources( @@ -996,11 +976,26 @@ async function handleMessagesForClientSites( userId: string | null; orgId: string; }, - olmId: string, sitesToAdd: number[], sitesToRemove: number[], trx: Transaction | typeof db = db ): Promise { + // Get the olm for this client + const [olm] = await trx + .select({ olmId: olms.olmId }) + .from(olms) + .where(eq(olms.clientId, client.clientId)) + .limit(1); + + if (!olm) { + logger.warn( + `Olm not found for client ${client.clientId}, skipping peer updates` + ); + return; + } + + const olmId = olm.olmId; + if (!client.subnet || !client.pubKey) { logger.warn( `Client ${client.clientId} missing subnet or pubKey, skipping peer updates` @@ -1021,9 +1016,9 @@ async function handleMessagesForClientSites( .leftJoin(newts, eq(sites.siteId, newts.siteId)) .where(inArray(sites.siteId, allSiteIds)); - let newtJobs: Promise[] = []; - let olmJobs: Promise[] = []; - let exitNodeJobs: Promise[] = []; + const newtJobs: Promise[] = []; + const olmJobs: Promise[] = []; + const exitNodeJobs: Promise[] = []; for (const siteData of sitesData) { const site = siteData.sites; @@ -1130,18 +1125,8 @@ async function handleMessagesForClientResources( resourcesToRemove: number[], trx: Transaction | typeof db = db ): Promise { - // Group resources by site - const resourcesBySite = new Map(); - - for (const resource of allNewResources) { - if (!resourcesBySite.has(resource.siteId)) { - resourcesBySite.set(resource.siteId, []); - } - resourcesBySite.get(resource.siteId)!.push(resource); - } - - let proxyJobs: Promise[] = []; - let olmJobs: Promise[] = []; + const proxyJobs: Promise[] = []; + const olmJobs: Promise[] = []; // Handle additions if (resourcesToAdd.length > 0) { diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 8060ccad..82568216 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -823,7 +823,7 @@ export async function getTraefikConfig( (cert) => cert.queriedDomain === lp.fullDomain ); if (!matchingCert) { - logger.warn( + logger.debug( `No matching certificate found for login page domain: ${lp.fullDomain}` ); continue; diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts index eb0cae5d..96d241fb 100644 --- a/server/private/routers/auditLogs/queryAccessAuditLog.ts +++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts @@ -48,7 +48,7 @@ export const queryAccessAuditLogsQuery = z.object({ }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .prefault(new Date().toISOString()) + .prefault(() => new Date().toISOString()) .openapi({ type: "string", format: "date-time", diff --git a/server/private/routers/auditLogs/queryActionAuditLog.ts b/server/private/routers/auditLogs/queryActionAuditLog.ts index 518eb982..7eed741b 100644 --- a/server/private/routers/auditLogs/queryActionAuditLog.ts +++ b/server/private/routers/auditLogs/queryActionAuditLog.ts @@ -48,7 +48,7 @@ export const queryActionAuditLogsQuery = z.object({ }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .prefault(new Date().toISOString()) + .prefault(() => new Date().toISOString()) .openapi({ type: "string", format: "date-time", diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 568f2b35..d9608e21 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -311,6 +311,33 @@ authenticated.get( loginPage.getLoginPage ); +authenticated.get( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getLoginPage), + logActionAudit(ActionsEnum.getLoginPage), + loginPage.getLoginPageBranding +); + +authenticated.put( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateLoginPage), + logActionAudit(ActionsEnum.updateLoginPage), + loginPage.upsertLoginPageBranding +); + +authenticated.delete( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.deleteLoginPage), + logActionAudit(ActionsEnum.deleteLoginPage), + loginPage.deleteLoginPageBranding +); + authRouter.post( "/remoteExitNode/get-token", verifyValidLicense, diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index 3accc500..751a1a0c 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -76,6 +76,7 @@ import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes"; import { maxmindLookup } from "@server/db/maxmind"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import semver from "semver"; +import { maxmindAsnLookup } from "@server/db/maxmindAsn"; // Zod schemas for request validation const getResourceByDomainParamsSchema = z.strictObject({ @@ -1238,6 +1239,70 @@ hybridRouter.get( } ); +const asnIpLookupParamsSchema = z.object({ + ip: z.union([z.ipv4(), z.ipv6()]) +}); +hybridRouter.get( + "/asnip/:ip", + async (req: Request, res: Response, next: NextFunction) => { + try { + const parsedParams = asnIpLookupParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { ip } = parsedParams.data; + + if (!maxmindAsnLookup) { + return next( + createHttpError( + HttpCode.SERVICE_UNAVAILABLE, + "ASNIP service is not available" + ) + ); + } + + const result = maxmindAsnLookup.get(ip); + + if (!result || !result.autonomous_system_number) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "ASNIP information not found" + ) + ); + } + + const { autonomous_system_number } = result; + + logger.debug( + `ASNIP lookup successful for IP ${ip}: ${autonomous_system_number}` + ); + + return response(res, { + data: { asn: autonomous_system_number }, + success: true, + error: false, + message: "GeoIP lookup successful", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to validate resource session token" + ) + ); + } + } +); + // GERBIL ROUTERS const getConfigSchema = z.object({ publicKey: z.string(), diff --git a/server/private/routers/internal.ts b/server/private/routers/internal.ts index b393b884..49596a1f 100644 --- a/server/private/routers/internal.ts +++ b/server/private/routers/internal.ts @@ -28,6 +28,7 @@ internalRouter.get("/org/:orgId/idp", orgIdp.listOrgIdps); internalRouter.get("/org/:orgId/billing/tier", billing.getOrgTier); internalRouter.get("/login-page", loginPage.loadLoginPage); +internalRouter.get("/login-page-branding", loginPage.loadLoginPageBranding); internalRouter.post( "/get-session-transfer-token", diff --git a/server/private/routers/loginPage/deleteLoginPageBranding.ts b/server/private/routers/loginPage/deleteLoginPageBranding.ts new file mode 100644 index 00000000..1fb243b0 --- /dev/null +++ b/server/private/routers/loginPage/deleteLoginPageBranding.ts @@ -0,0 +1,113 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { eq } from "drizzle-orm"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +export async function deleteLoginPageBranding( + 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 { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + if (!existingLoginPageBranding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Login page branding not found" + ) + ); + } + + await db + .delete(loginPageBranding) + .where( + eq( + loginPageBranding.loginPageBrandingId, + existingLoginPageBranding.loginPageBranding + .loginPageBrandingId + ) + ); + + return response(res, { + data: existingLoginPageBranding.loginPageBranding, + success: true, + error: false, + message: "Login page branding deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/loginPage/getLoginPageBranding.ts b/server/private/routers/loginPage/getLoginPageBranding.ts new file mode 100644 index 00000000..262e9ce8 --- /dev/null +++ b/server/private/routers/loginPage/getLoginPageBranding.ts @@ -0,0 +1,103 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { eq } from "drizzle-orm"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +export async function getLoginPageBranding( + 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 { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + if (!existingLoginPageBranding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Login page branding not found" + ) + ); + } + + return response(res, { + data: existingLoginPageBranding.loginPageBranding, + success: true, + error: false, + message: "Login page branding retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/loginPage/index.ts b/server/private/routers/loginPage/index.ts index 2372ddfa..1bfe6e16 100644 --- a/server/private/routers/loginPage/index.ts +++ b/server/private/routers/loginPage/index.ts @@ -17,3 +17,7 @@ export * from "./getLoginPage"; export * from "./loadLoginPage"; export * from "./updateLoginPage"; export * from "./deleteLoginPage"; +export * from "./upsertLoginPageBranding"; +export * from "./deleteLoginPageBranding"; +export * from "./getLoginPageBranding"; +export * from "./loadLoginPageBranding"; diff --git a/server/private/routers/loginPage/loadLoginPageBranding.ts b/server/private/routers/loginPage/loadLoginPageBranding.ts new file mode 100644 index 00000000..823f75a6 --- /dev/null +++ b/server/private/routers/loginPage/loadLoginPageBranding.ts @@ -0,0 +1,100 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, loginPageBranding, loginPageBrandingOrg, orgs } from "@server/db"; +import { eq, and } 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 type { LoadLoginPageBrandingResponse } from "@server/routers/loginPage/types"; + +const querySchema = z.object({ + orgId: z.string().min(1) +}); + +async function query(orgId: string) { + const [orgLink] = await db + .select() + .from(loginPageBrandingOrg) + .where(eq(loginPageBrandingOrg.orgId, orgId)) + .innerJoin(orgs, eq(loginPageBrandingOrg.orgId, orgs.orgId)); + if (!orgLink) { + return null; + } + + const [res] = await db + .select() + .from(loginPageBranding) + .where( + and( + eq( + loginPageBranding.loginPageBrandingId, + orgLink.loginPageBrandingOrg.loginPageBrandingId + ) + ) + ) + .limit(1); + return { + ...res, + orgId: orgLink.orgs.orgId, + orgName: orgLink.orgs.name + }; +} + +export async function loadLoginPageBranding( + 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).toString() + ) + ); + } + + const { orgId } = parsedQuery.data; + + const branding = await query(orgId); + + if (!branding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Branding for Login page not found" + ) + ); + } + + return response(res, { + data: branding, + success: true, + error: false, + message: "Login page branding retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts new file mode 100644 index 00000000..f9f9d08c --- /dev/null +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -0,0 +1,162 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { eq, InferInsertModel } from "drizzle-orm"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z.strictObject({ + orgId: z.string() +}); + +const bodySchema = z.strictObject({ + logoUrl: z.url(), + logoWidth: z.coerce.number().min(1), + logoHeight: z.coerce.number().min(1), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional(), + orgTitle: z.string().optional(), + orgSubtitle: z.string().optional(), + primaryColor: z + .string() + .regex(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i) + .optional() +}); + +export type UpdateLoginPageBrandingBody = z.infer; + +export async function upsertLoginPageBranding( + 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; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + let updateData = parsedBody.data satisfies InferInsertModel< + typeof loginPageBranding + >; + + if (build !== "saas") { + // org branding settings are only considered in the saas build + const { orgTitle, orgSubtitle, ...rest } = updateData; + updateData = rest; + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + let updatedLoginPageBranding: LoginPageBranding; + + if (existingLoginPageBranding) { + updatedLoginPageBranding = await db.transaction(async (tx) => { + const [branding] = await tx + .update(loginPageBranding) + .set({ ...updateData }) + .where( + eq( + loginPageBranding.loginPageBrandingId, + existingLoginPageBranding.loginPageBranding + .loginPageBrandingId + ) + ) + .returning(); + return branding; + }); + } else { + updatedLoginPageBranding = await db.transaction(async (tx) => { + const [branding] = await tx + .insert(loginPageBranding) + .values({ ...updateData }) + .returning(); + + await tx.insert(loginPageBrandingOrg).values({ + loginPageBrandingId: branding.loginPageBrandingId, + orgId: orgId + }); + return branding; + }); + } + + return response(res, { + data: updatedLoginPageBranding, + success: true, + error: false, + message: existingLoginPageBranding + ? "Login page branding updated successfully" + : "Login page branding created successfully", + status: existingLoginPageBranding ? HttpCode.OK : HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/misc/sendSupportEmail.ts b/server/private/routers/misc/sendSupportEmail.ts index 404a2501..cd37560d 100644 --- a/server/private/routers/misc/sendSupportEmail.ts +++ b/server/private/routers/misc/sendSupportEmail.ts @@ -66,6 +66,7 @@ export async function sendSupportEmail( { name: req.user?.email || "Support User", to: "support@pangolin.net", + replyTo: req.user?.email || undefined, from: config.getNoReplyEmail(), subject: `Support Request: ${subject}` } diff --git a/server/routers/auditLogs/queryRequestAnalytics.ts b/server/routers/auditLogs/queryRequestAnalytics.ts index a765f176..cd1218ce 100644 --- a/server/routers/auditLogs/queryRequestAnalytics.ts +++ b/server/routers/auditLogs/queryRequestAnalytics.ts @@ -1,4 +1,4 @@ -import { db, requestAuditLog, driver } from "@server/db"; +import { db, requestAuditLog, driver, primaryDb } from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; @@ -35,7 +35,7 @@ const queryAccessAuditLogsQuery = z.object({ }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .prefault(new Date().toISOString()) + .prefault(() => new Date().toISOString()) .openapi({ type: "string", format: "date-time", @@ -74,12 +74,12 @@ async function query(query: Q) { ); } - const [all] = await db + const [all] = await primaryDb .select({ total: count() }) .from(requestAuditLog) .where(baseConditions); - const [blocked] = await db + const [blocked] = await primaryDb .select({ total: count() }) .from(requestAuditLog) .where(and(baseConditions, eq(requestAuditLog.action, false))); @@ -88,7 +88,9 @@ async function query(query: Q) { .mapWith(Number) .as("total"); - const requestsPerCountry = await db + const DISTINCT_LIMIT = 500; + + const requestsPerCountry = await primaryDb .selectDistinct({ code: requestAuditLog.location, count: totalQ @@ -96,7 +98,16 @@ async function query(query: Q) { .from(requestAuditLog) .where(and(baseConditions, not(isNull(requestAuditLog.location)))) .groupBy(requestAuditLog.location) - .orderBy(desc(totalQ)); + .orderBy(desc(totalQ)) + .limit(DISTINCT_LIMIT+1); + + if (requestsPerCountry.length > DISTINCT_LIMIT) { + // throw an error + throw createHttpError( + HttpCode.BAD_REQUEST, + `Too many distinct countries. Please narrow your query.` + ); + } const groupByDayFunction = driver === "pg" @@ -106,7 +117,7 @@ async function query(query: Q) { const booleanTrue = driver === "pg" ? sql`true` : sql`1`; const booleanFalse = driver === "pg" ? sql`false` : sql`0`; - const requestsPerDay = await db + const requestsPerDay = await primaryDb .select({ day: groupByDayFunction.as("day"), allowedCount: diff --git a/server/routers/auditLogs/queryRequestAuditLog.ts b/server/routers/auditLogs/queryRequestAuditLog.ts index 9cedec63..602b4475 100644 --- a/server/routers/auditLogs/queryRequestAuditLog.ts +++ b/server/routers/auditLogs/queryRequestAuditLog.ts @@ -1,4 +1,4 @@ -import { db, requestAuditLog, resources } from "@server/db"; +import { db, primaryDb, requestAuditLog, resources } from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; @@ -35,7 +35,7 @@ export const queryAccessAuditLogsQuery = z.object({ }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() - .prefault(new Date().toISOString()) + .prefault(() => new Date().toISOString()) .openapi({ type: "string", format: "date-time", @@ -107,7 +107,7 @@ function getWhere(data: Q) { } export function queryRequest(data: Q) { - return db + return primaryDb .select({ id: requestAuditLog.id, timestamp: requestAuditLog.timestamp, @@ -143,7 +143,7 @@ export function queryRequest(data: Q) { } export function countRequestQuery(data: Q) { - const countQuery = db + const countQuery = primaryDb .select({ count: count() }) .from(requestAuditLog) .where(getWhere(data)); @@ -173,50 +173,61 @@ async function queryUniqueFilterAttributes( eq(requestAuditLog.orgId, orgId) ); - // Get unique actors - const uniqueActors = await db - .selectDistinct({ - actor: requestAuditLog.actor - }) - .from(requestAuditLog) - .where(baseConditions); + const DISTINCT_LIMIT = 500; - // Get unique locations - const uniqueLocations = await db - .selectDistinct({ - locations: requestAuditLog.location - }) - .from(requestAuditLog) - .where(baseConditions); + // TODO: SOMEONE PLEASE OPTIMIZE THIS!!!!! - // Get unique actors - const uniqueHosts = await db - .selectDistinct({ - hosts: requestAuditLog.host - }) - .from(requestAuditLog) - .where(baseConditions); + // Run all queries in parallel + const [ + uniqueActors, + uniqueLocations, + uniqueHosts, + uniquePaths, + uniqueResources + ] = await Promise.all([ + primaryDb + .selectDistinct({ actor: requestAuditLog.actor }) + .from(requestAuditLog) + .where(baseConditions) + .limit(DISTINCT_LIMIT+1), + primaryDb + .selectDistinct({ locations: requestAuditLog.location }) + .from(requestAuditLog) + .where(baseConditions) + .limit(DISTINCT_LIMIT+1), + primaryDb + .selectDistinct({ hosts: requestAuditLog.host }) + .from(requestAuditLog) + .where(baseConditions) + .limit(DISTINCT_LIMIT+1), + primaryDb + .selectDistinct({ paths: requestAuditLog.path }) + .from(requestAuditLog) + .where(baseConditions) + .limit(DISTINCT_LIMIT+1), + primaryDb + .selectDistinct({ + id: requestAuditLog.resourceId, + name: resources.name + }) + .from(requestAuditLog) + .leftJoin( + resources, + eq(requestAuditLog.resourceId, resources.resourceId) + ) + .where(baseConditions) + .limit(DISTINCT_LIMIT+1) + ]); - // Get unique actors - const uniquePaths = await db - .selectDistinct({ - paths: requestAuditLog.path - }) - .from(requestAuditLog) - .where(baseConditions); - - // Get unique resources with names - const uniqueResources = await db - .selectDistinct({ - id: requestAuditLog.resourceId, - name: resources.name - }) - .from(requestAuditLog) - .leftJoin( - resources, - eq(requestAuditLog.resourceId, resources.resourceId) - ) - .where(baseConditions); + if ( + uniqueActors.length > DISTINCT_LIMIT || + uniqueLocations.length > DISTINCT_LIMIT || + uniqueHosts.length > DISTINCT_LIMIT || + uniquePaths.length > DISTINCT_LIMIT || + uniqueResources.length > DISTINCT_LIMIT + ) { + throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range."); + } return { actors: uniqueActors @@ -295,6 +306,12 @@ export async function queryRequestAuditLogs( }); } catch (error) { logger.error(error); + // if the message is "Too many distinct filter attributes to retrieve. Please refine your time range.", return a 400 and the message + if (error instanceof Error && error.message === "Too many distinct filter attributes to retrieve. Please refine your time range.") { + return next( + createHttpError(HttpCode.BAD_REQUEST, error.message) + ); + } return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index d7fe9190..0e3a3489 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -29,6 +29,7 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import { getCountryCodeForIp } from "@server/lib/geoip"; +import { getAsnForIp } from "@server/lib/asn"; import { getOrgTierData } from "#dynamic/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { verifyPassword } from "@server/auth/password"; @@ -128,6 +129,10 @@ export async function verifyResourceSession( ? await getCountryCodeFromIp(clientIp) : undefined; + const ipAsn = clientIp + ? await getAsnFromIp(clientIp) + : undefined; + let cleanHost = host; // if the host ends with :port, strip it if (cleanHost.match(/:[0-9]{1,5}$/)) { @@ -216,7 +221,8 @@ export async function verifyResourceSession( resource.resourceId, clientIp, path, - ipCC + ipCC, + ipAsn ); if (action == "ACCEPT") { @@ -910,7 +916,8 @@ async function checkRules( resourceId: number, clientIp: string | undefined, path: string | undefined, - ipCC?: string + ipCC?: string, + ipAsn?: number ): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> { const ruleCacheKey = `rules:${resourceId}`; @@ -954,6 +961,12 @@ async function checkRules( (await isIpInGeoIP(ipCC, rule.value)) ) { return rule.action as any; + } else if ( + clientIp && + rule.match == "ASN" && + (await isIpInAsn(ipAsn, rule.value)) + ) { + return rule.action as any; } } @@ -1090,6 +1103,52 @@ async function isIpInGeoIP( return ipCountryCode?.toUpperCase() === checkCountryCode.toUpperCase(); } +async function isIpInAsn( + ipAsn: number | undefined, + checkAsn: string +): Promise { + // Handle "ALL" special case + if (checkAsn === "ALL" || checkAsn === "AS0") { + return true; + } + + if (!ipAsn) { + return false; + } + + // Normalize the check ASN - remove "AS" prefix if present and convert to number + const normalizedCheckAsn = checkAsn.toUpperCase().replace(/^AS/, ""); + const checkAsnNumber = parseInt(normalizedCheckAsn, 10); + + if (isNaN(checkAsnNumber)) { + logger.warn(`Invalid ASN format in rule: ${checkAsn}`); + return false; + } + + const match = ipAsn === checkAsnNumber; + logger.debug( + `ASN check: IP ASN ${ipAsn} ${match ? "matches" : "does not match"} rule ASN ${checkAsnNumber}` + ); + + return match; +} + +async function getAsnFromIp(ip: string): Promise { + const asnCacheKey = `asn:${ip}`; + + let cachedAsn: number | undefined = cache.get(asnCacheKey); + + if (!cachedAsn) { + cachedAsn = await getAsnForIp(ip); // do it locally + // Cache for longer since IP ASN doesn't change frequently + if (cachedAsn) { + cache.set(asnCacheKey, cachedAsn, 300); // 5 minutes + } + } + + return cachedAsn; +} + async function getCountryCodeFromIp(ip: string): Promise { const geoIpCacheKey = `geoip:${ip}`; diff --git a/server/routers/certificates/types.ts b/server/routers/certificates/types.ts index 3ec90857..bca9412c 100644 --- a/server/routers/certificates/types.ts +++ b/server/routers/certificates/types.ts @@ -6,8 +6,8 @@ export type GetCertificateResponse = { status: string; // pending, requested, valid, expired, failed expiresAt: string | null; lastRenewalAttempt: Date | null; - createdAt: string; - updatedAt: string; + createdAt: number; + updatedAt: number; errorMessage?: string | null; renewalCount: number; }; diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index b7b91925..653a2578 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -4,21 +4,48 @@ import { Alias, SubnetProxyTarget } from "@server/lib/ip"; import logger from "@server/logger"; import { eq } from "drizzle-orm"; +const BATCH_SIZE = 50; +const BATCH_DELAY_MS = 50; + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function chunkArray(array: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)); + } + return chunks; +} + export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) { - await sendToClient(newtId, { - type: `newt/wg/targets/add`, - data: targets - }); + const batches = chunkArray(targets, BATCH_SIZE); + for (let i = 0; i < batches.length; i++) { + if (i > 0) { + await sleep(BATCH_DELAY_MS); + } + await sendToClient(newtId, { + type: `newt/wg/targets/add`, + data: batches[i] + }); + } } export async function removeTargets( newtId: string, targets: SubnetProxyTarget[] ) { - await sendToClient(newtId, { - type: `newt/wg/targets/remove`, - data: targets - }); + const batches = chunkArray(targets, BATCH_SIZE); + for (let i = 0; i < batches.length; i++) { + if (i > 0) { + await sleep(BATCH_DELAY_MS); + } + await sendToClient(newtId, { + type: `newt/wg/targets/remove`, + data: batches[i] + }); + } } export async function updateTargets( @@ -28,12 +55,24 @@ export async function updateTargets( newTargets: SubnetProxyTarget[]; } ) { - await sendToClient(newtId, { - type: `newt/wg/targets/update`, - data: targets - }).catch((error) => { - logger.warn(`Error sending message:`, error); - }); + const oldBatches = chunkArray(targets.oldTargets, BATCH_SIZE); + const newBatches = chunkArray(targets.newTargets, BATCH_SIZE); + const maxBatches = Math.max(oldBatches.length, newBatches.length); + + for (let i = 0; i < maxBatches; i++) { + if (i > 0) { + await sleep(BATCH_DELAY_MS); + } + await sendToClient(newtId, { + type: `newt/wg/targets/update`, + data: { + oldTargets: oldBatches[i] || [], + newTargets: newBatches[i] || [] + } + }).catch((error) => { + logger.warn(`Error sending message:`, error); + }); + } } export async function addPeerData( diff --git a/server/routers/external.ts b/server/routers/external.ts index 54b48c6e..f85cc4be 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -239,9 +239,8 @@ authenticated.get( // Site Resource endpoints authenticated.put( - "/org/:orgId/site/:siteId/resource", + "/org/:orgId/site-resource", verifyOrgAccess, - verifySiteAccess, verifyUserHasAction(ActionsEnum.createSiteResource), logActionAudit(ActionsEnum.createSiteResource), siteResource.createSiteResource @@ -263,18 +262,14 @@ authenticated.get( ); authenticated.get( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyOrgAccess, - verifySiteAccess, + "/site-resource/:siteResourceId", verifySiteResourceAccess, verifyUserHasAction(ActionsEnum.getSiteResource), siteResource.getSiteResource ); authenticated.post( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyOrgAccess, - verifySiteAccess, + "/site-resource/:siteResourceId", verifySiteResourceAccess, verifyUserHasAction(ActionsEnum.updateSiteResource), logActionAudit(ActionsEnum.updateSiteResource), @@ -282,9 +277,7 @@ authenticated.post( ); authenticated.delete( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyOrgAccess, - verifySiteAccess, + "/site-resource/:siteResourceId", verifySiteResourceAccess, verifyUserHasAction(ActionsEnum.deleteSiteResource), logActionAudit(ActionsEnum.deleteSiteResource), diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index ba3ab7ad..488ef75b 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -51,7 +51,10 @@ export async function getConfig( ); } - const exitNode = await createExitNode(publicKey, reachableAt); + // clean up the public key - keep only valid base64 characters (A-Z, a-z, 0-9, +, /, =) + const cleanedPublicKey = publicKey.replace(/[^A-Za-z0-9+/=]/g, ''); + + const exitNode = await createExitNode(cleanedPublicKey, reachableAt); if (!exitNode) { return next( diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index f6b21ff6..e87fe3ce 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -192,11 +192,71 @@ export async function validateOidcCallback( state }); - const tokens = await client.validateAuthorizationCode( - ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl), - code, - codeVerifier - ); + let tokens: arctic.OAuth2Tokens; + try { + tokens = await client.validateAuthorizationCode( + ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl), + code, + codeVerifier + ); + } catch (err: unknown) { + if (err instanceof arctic.OAuth2RequestError) { + logger.warn("OIDC provider rejected the authorization code", { + error: err.code, + description: err.description, + uri: err.uri, + state: err.state + }); + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + err.description || + `OIDC provider rejected the request (${err.code})` + ) + ); + } + + if (err instanceof arctic.UnexpectedResponseError) { + logger.error( + "OIDC provider returned an unexpected response during token exchange", + { status: err.status } + ); + return next( + createHttpError( + HttpCode.BAD_GATEWAY, + "Received an unexpected response from the identity provider while exchanging the authorization code." + ) + ); + } + + if (err instanceof arctic.UnexpectedErrorResponseBodyError) { + logger.error( + "OIDC provider returned an unexpected error payload during token exchange", + { status: err.status, data: err.data } + ); + return next( + createHttpError( + HttpCode.BAD_GATEWAY, + "Identity provider returned an unexpected error payload while exchanging the authorization code." + ) + ); + } + + if (err instanceof arctic.ArcticFetchError) { + logger.error( + "Failed to reach OIDC provider while exchanging authorization code", + { error: err.message } + ); + return next( + createHttpError( + HttpCode.BAD_GATEWAY, + "Unable to reach the identity provider while exchanging the authorization code. Please try again." + ) + ); + } + + throw err; + } const idToken = tokens.idToken(); logger.debug("ID token", { idToken }); @@ -545,9 +605,18 @@ export async function validateOidcCallback( res.appendHeader("Set-Cookie", cookie); + let finalRedirectUrl = postAuthRedirectUrl; + if (loginPageId) { + finalRedirectUrl = `/auth/org/?redirect=${encodeURIComponent( + postAuthRedirectUrl + )}`; + } + + logger.debug("Final redirect URL", { finalRedirectUrl }); + return response(res, { data: { - redirectUrl: postAuthRedirectUrl + redirectUrl: finalRedirectUrl }, success: true, error: false, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 878d61fa..f66494a0 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -146,9 +146,8 @@ authenticated.get( ); // Site Resource endpoints authenticated.put( - "/org/:orgId/site/:siteId/resource", + "/org/:orgId/private-resource", verifyApiKeyOrgAccess, - verifyApiKeySiteAccess, verifyApiKeyHasAction(ActionsEnum.createSiteResource), logActionAudit(ActionsEnum.createSiteResource), siteResource.createSiteResource @@ -170,18 +169,14 @@ authenticated.get( ); authenticated.get( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyApiKeyOrgAccess, - verifyApiKeySiteAccess, + "/site-resource/:siteResourceId", verifyApiKeySiteResourceAccess, verifyApiKeyHasAction(ActionsEnum.getSiteResource), siteResource.getSiteResource ); authenticated.post( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyApiKeyOrgAccess, - verifyApiKeySiteAccess, + "/site-resource/:siteResourceId", verifyApiKeySiteResourceAccess, verifyApiKeyHasAction(ActionsEnum.updateSiteResource), logActionAudit(ActionsEnum.updateSiteResource), @@ -189,9 +184,7 @@ authenticated.post( ); authenticated.delete( - "/org/:orgId/site/:siteId/resource/:siteResourceId", - verifyApiKeyOrgAccess, - verifyApiKeySiteAccess, + "/site-resource/:siteResourceId", verifyApiKeySiteResourceAccess, verifyApiKeyHasAction(ActionsEnum.deleteSiteResource), logActionAudit(ActionsEnum.deleteSiteResource), @@ -352,6 +345,14 @@ authenticated.post( user.inviteUser ); +authenticated.delete( + "/org/:orgId/invitations/:inviteId", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.removeInvitation), + logActionAudit(ActionsEnum.removeInvitation), + user.removeInvitation +); + authenticated.get( "/resource/:resourceId/roles", verifyApiKeyResourceAccess, @@ -857,6 +858,22 @@ authenticated.put( blueprints.applyJSONBlueprint ); + +authenticated.get( + "/org/:orgId/blueprint/:blueprintId", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.getBlueprint), + blueprints.getBlueprint +); + + +authenticated.get( + "/org/:orgId/blueprints", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listBlueprints), + blueprints.listBlueprints +); + authenticated.get( "/org/:orgId/logs/request", verifyApiKeyOrgAccess, diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts index a68dd7d4..8a253d07 100644 --- a/server/routers/loginPage/types.ts +++ b/server/routers/loginPage/types.ts @@ -1,4 +1,4 @@ -import { LoginPage } from "@server/db"; +import type { LoginPage, LoginPageBranding } from "@server/db"; export type CreateLoginPageResponse = LoginPage; @@ -9,3 +9,10 @@ export type GetLoginPageResponse = LoginPage; export type UpdateLoginPageResponse = LoginPage; export type LoadLoginPageResponse = LoginPage & { orgId: string }; + +export type LoadLoginPageBrandingResponse = LoginPageBranding & { + orgId: string; + orgName: string; +}; + +export type GetLoginPageBrandingResponse = LoginPageBranding; diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 77e49a20..c7f2131e 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -346,6 +346,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { type: "newt/wg/connect", data: { endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`, + relayPort: config.getRawConfig().gerbil.clients_start_port, publicKey: exitNode.publicKey, serverIP: exitNode.address.split("/")[0], tunnelIP: siteSubnet.split("/")[0], diff --git a/server/routers/olm/getOlmToken.ts b/server/routers/olm/getOlmToken.ts index 3852b00e..c8ede518 100644 --- a/server/routers/olm/getOlmToken.ts +++ b/server/routers/olm/getOlmToken.ts @@ -194,10 +194,23 @@ export async function getOlmToken( .where(inArray(exitNodes.exitNodeId, exitNodeIds)); } + // Map exitNodeId to siteIds + const exitNodeIdToSiteIds: Record = {}; + for (const { sites: site } of clientSites) { + if (site.exitNodeId !== null) { + if (!exitNodeIdToSiteIds[site.exitNodeId]) { + exitNodeIdToSiteIds[site.exitNodeId] = []; + } + exitNodeIdToSiteIds[site.exitNodeId].push(site.siteId); + } + } + const exitNodesHpData = allExitNodes.map((exitNode: ExitNode) => { return { publicKey: exitNode.publicKey, - endpoint: exitNode.endpoint + relayPort: config.getRawConfig().gerbil.clients_start_port, + endpoint: exitNode.endpoint, + siteIds: exitNodeIdToSiteIds[exitNode.exitNodeId] ?? [] }; }); diff --git a/server/routers/olm/handleOlmRelayMessage.ts b/server/routers/olm/handleOlmRelayMessage.ts index 595b35ba..88886cd1 100644 --- a/server/routers/olm/handleOlmRelayMessage.ts +++ b/server/routers/olm/handleOlmRelayMessage.ts @@ -4,6 +4,7 @@ import { clients, clientSitesAssociationsCache, Olm } from "@server/db"; import { and, eq } from "drizzle-orm"; import { updatePeer as newtUpdatePeer } from "../newt/peers"; import logger from "@server/logger"; +import config from "@server/lib/config"; export const handleOlmRelayMessage: MessageHandler = async (context) => { const { message, client: c, sendToClient } = context; @@ -88,7 +89,8 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => { type: "olm/wg/peer/relay", data: { siteId: siteId, - relayEndpoint: exitNode.endpoint + relayEndpoint: exitNode.endpoint, + relayPort: config.getRawConfig().gerbil.clients_start_port } }, broadcast: false, diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts index 4aa8edd7..e164b257 100644 --- a/server/routers/olm/peers.ts +++ b/server/routers/olm/peers.ts @@ -1,5 +1,6 @@ import { sendToClient } from "#dynamic/routers/ws"; import { db, olms } from "@server/db"; +import config from "@server/lib/config"; import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { Alias } from "yaml"; @@ -156,6 +157,7 @@ export async function initPeerAddHandshake( siteId: peer.siteId, exitNode: { publicKey: peer.exitNode.publicKey, + relayPort: config.getRawConfig().gerbil.clients_start_port, endpoint: peer.exitNode.endpoint } } diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index f1d06566..e93af889 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -27,6 +27,7 @@ import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; +import { doCidrsOverlap } from "@server/lib/ip"; const createOrgSchema = z.strictObject({ orgId: z.string(), @@ -36,6 +37,11 @@ const createOrgSchema = z.strictObject({ .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere .refine((val) => isValidCIDR(val), { message: "Invalid subnet CIDR" + }), + utilitySubnet: z + .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere + .refine((val) => isValidCIDR(val), { + message: "Invalid utility subnet CIDR" }) }); @@ -84,7 +90,7 @@ export async function createOrg( ); } - const { orgId, name, subnet } = parsedBody.data; + const { orgId, name, subnet, utilitySubnet } = parsedBody.data; // TODO: for now we are making all of the orgs the same subnet // make sure the subnet is unique @@ -119,6 +125,15 @@ export async function createOrg( ); } + if (doCidrsOverlap(subnet, utilitySubnet)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Subnet ${subnet} overlaps with utility subnet ${utilitySubnet}` + ) + ); + } + let error = ""; let org: Org | null = null; @@ -128,9 +143,6 @@ export async function createOrg( .from(domains) .where(eq(domains.configManaged, true)); - const utilitySubnet = - config.getRawConfig().orgs.utility_subnet_group; - const newOrg = await trx .insert(orgs) .values({ diff --git a/server/routers/org/pickOrgDefaults.ts b/server/routers/org/pickOrgDefaults.ts index 771b0d99..cce46a01 100644 --- a/server/routers/org/pickOrgDefaults.ts +++ b/server/routers/org/pickOrgDefaults.ts @@ -8,6 +8,7 @@ import config from "@server/lib/config"; export type PickOrgDefaultsResponse = { subnet: string; + utilitySubnet: string; }; export async function pickOrgDefaults( @@ -20,10 +21,13 @@ export async function pickOrgDefaults( // const subnet = await getNextAvailableOrgSubnet(); // Just hard code the subnet for now for everyone const subnet = config.getRawConfig().orgs.subnet_group; + const utilitySubnet = + config.getRawConfig().orgs.utility_subnet_group; return response(res, { data: { - subnet: subnet + subnet: subnet, + utilitySubnet: utilitySubnet }, success: true, error: false, diff --git a/server/routers/resource/createResourceRule.ts b/server/routers/resource/createResourceRule.ts index 3f86665b..a516d14a 100644 --- a/server/routers/resource/createResourceRule.ts +++ b/server/routers/resource/createResourceRule.ts @@ -17,7 +17,7 @@ import { OpenAPITags, registry } from "@server/openApi"; const createResourceRuleSchema = z.strictObject({ action: z.enum(["ACCEPT", "DROP", "PASS"]), - match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]), + match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN"]), value: z.string().min(1), priority: z.int(), enabled: z.boolean().optional() diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index fe0a38c8..61479a4d 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -89,7 +89,6 @@ export async function getResourceAuthInfo( resourcePassword, eq(resourcePassword.resourceId, resources.resourceId) ) - .leftJoin( resourceHeaderAuth, eq( diff --git a/server/routers/resource/updateResourceRule.ts b/server/routers/resource/updateResourceRule.ts index cae3f16e..b443bd1c 100644 --- a/server/routers/resource/updateResourceRule.ts +++ b/server/routers/resource/updateResourceRule.ts @@ -25,7 +25,7 @@ const updateResourceRuleParamsSchema = z.strictObject({ const updateResourceRuleSchema = z .strictObject({ action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(), - match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]).optional(), + match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN"]).optional(), value: z.string().min(1).optional(), priority: z.int(), enabled: z.boolean().optional() diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index 09750c31..55423c01 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -89,7 +89,7 @@ export async function deleteSite( // Send termination message outside of transaction to prevent blocking if (deletedNewtId) { const payload = { - type: `newt/terminate`, + type: `newt/wg/terminate`, data: {} }; // Don't await this to prevent blocking the response diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 37ca8fe4..e7a3bb37 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -1,5 +1,6 @@ import { db, exitNodes, newts } from "@server/db"; import { orgs, roleSites, sites, userSites } from "@server/db"; +import { remoteExitNodes } from "@server/db"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; @@ -104,12 +105,17 @@ function querySites(orgId: string, accessibleSiteIds: number[]) { newtVersion: newts.version, exitNodeId: sites.exitNodeId, exitNodeName: exitNodes.name, - exitNodeEndpoint: exitNodes.endpoint + exitNodeEndpoint: exitNodes.endpoint, + remoteExitNodeId: remoteExitNodes.remoteExitNodeId }) .from(sites) .leftJoin(orgs, eq(sites.orgId, orgs.orgId)) .leftJoin(newts, eq(newts.siteId, sites.siteId)) .leftJoin(exitNodes, eq(exitNodes.exitNodeId, sites.exitNodeId)) + .leftJoin( + remoteExitNodes, + eq(remoteExitNodes.exitNodeId, sites.exitNodeId) + ) .where( and( inArray(sites.siteId, accessibleSiteIds), diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index e9ce8e04..d2196e87 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -2,6 +2,7 @@ import { clientSiteResources, db, newts, + orgs, roles, roleSiteResources, SiteResource, @@ -10,7 +11,7 @@ import { userSiteResources } from "@server/db"; import { getUniqueSiteResourceName } from "@server/db/names"; -import { getNextAvailableAliasAddress } from "@server/lib/ip"; +import { getNextAvailableAliasAddress, isIpInCidr, portRangeStringSchema } from "@server/lib/ip"; import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations"; import response from "@server/lib/response"; import logger from "@server/logger"; @@ -23,7 +24,6 @@ import { z } from "zod"; import { fromError } from "zod-validation-error"; const createSiteResourceParamsSchema = z.strictObject({ - siteId: z.string().transform(Number).pipe(z.int().positive()), orgId: z.string() }); @@ -31,6 +31,7 @@ const createSiteResourceSchema = z .strictObject({ name: z.string().min(1).max(255), mode: z.enum(["host", "cidr", "port"]), + siteId: z.int(), // protocol: z.enum(["tcp", "udp"]).optional(), // proxyPort: z.int().positive().optional(), // destinationPort: z.int().positive().optional(), @@ -39,13 +40,16 @@ const createSiteResourceSchema = z alias: z .string() .regex( - /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, - "Alias must be a fully qualified domain name (e.g., example.com)" + /^(?:[a-zA-Z0-9*?](?:[a-zA-Z0-9*?-]{0,61}[a-zA-Z0-9*?])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, + "Alias must be a fully qualified domain name with optional wildcards (e.g., example.com, *.example.com, host-0?.example.internal)" ) .optional(), userIds: z.array(z.string()), roleIds: z.array(z.int()), - clientIds: z.array(z.int()) + clientIds: z.array(z.int()), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema, + disableIcmp: z.boolean().optional() }) .strict() .refine( @@ -65,7 +69,7 @@ const createSiteResourceSchema = z const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; const isValidDomain = domainRegex.test(data.destination); - const isValidAlias = data.alias && domainRegex.test(data.alias); + const isValidAlias = data.alias !== undefined && data.alias !== null && data.alias.trim() !== ""; return isValidDomain && isValidAlias; // require the alias to be set in the case of domain } @@ -81,8 +85,7 @@ const createSiteResourceSchema = z if (data.mode === "cidr") { // Check if it's a valid CIDR (v4 or v6) const isValidCIDR = z - // .union([z.cidrv4(), z.cidrv6()]) - .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere + .union([z.cidrv4(), z.cidrv6()]) .safeParse(data.destination).success; return isValidCIDR; } @@ -98,7 +101,7 @@ export type CreateSiteResourceResponse = SiteResource; registry.registerPath({ method: "put", - path: "/org/{orgId}/site/{siteId}/resource", + path: "/org/{orgId}/site-resource", description: "Create a new site resource.", tags: [OpenAPITags.Client, OpenAPITags.Org], request: { @@ -142,9 +145,10 @@ export async function createSiteResource( ); } - const { siteId, orgId } = parsedParams.data; + const { orgId } = parsedParams.data; const { name, + siteId, mode, // protocol, // proxyPort, @@ -154,7 +158,10 @@ export async function createSiteResource( alias, userIds, roleIds, - clientIds + clientIds, + tcpPortRangeString, + udpPortRangeString, + disableIcmp } = parsedBody.data; // Verify the site exists and belongs to the org @@ -168,6 +175,39 @@ export async function createSiteResource( return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); } + const [org] = await db + .select() + .from(orgs) + .where(eq(orgs.orgId, orgId)) + .limit(1); + + if (!org) { + return next(createHttpError(HttpCode.NOT_FOUND, "Organization not found")); + } + + if (!org.subnet || !org.utilitySubnet) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Organization with ID ${orgId} has no subnet or utilitySubnet defined defined` + ) + ); + } + + // Only check if destination is an IP address + const isIp = z.union([z.ipv4(), z.ipv6()]).safeParse(destination).success; + if ( + isIp && + (isIpInCidr(destination, org.subnet) || isIpInCidr(destination, org.utilitySubnet)) + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "IP can not be in the CIDR range of the organization's subnet or utility subnet" + ) + ); + } + // // check if resource with same protocol and proxy port already exists (only for port mode) // if (mode === "port" && protocol && proxyPort) { // const [existingResource] = await db @@ -239,7 +279,10 @@ export async function createSiteResource( destination, enabled, alias, - aliasAddress + aliasAddress, + tcpPortRangeString, + udpPortRangeString, + disableIcmp }) .returning(); diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts index 3d1e70cc..0abc7d73 100644 --- a/server/routers/siteResource/deleteSiteResource.ts +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -12,9 +12,7 @@ import { OpenAPITags, registry } from "@server/openApi"; import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations"; const deleteSiteResourceParamsSchema = z.strictObject({ - siteResourceId: z.string().transform(Number).pipe(z.int().positive()), - siteId: z.string().transform(Number).pipe(z.int().positive()), - orgId: z.string() + siteResourceId: z.string().transform(Number).pipe(z.int().positive()) }); export type DeleteSiteResourceResponse = { @@ -23,7 +21,7 @@ export type DeleteSiteResourceResponse = { registry.registerPath({ method: "delete", - path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", + path: "/site-resource/{siteResourceId}", description: "Delete a site resource.", tags: [OpenAPITags.Client, OpenAPITags.Org], request: { @@ -50,29 +48,13 @@ export async function deleteSiteResource( ); } - const { siteResourceId, siteId, orgId } = parsedParams.data; - - const [site] = await db - .select() - .from(sites) - .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) - .limit(1); - - if (!site) { - return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); - } + const { siteResourceId } = parsedParams.data; // Check if site resource exists const [existingSiteResource] = await db .select() .from(siteResources) - .where( - and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - ) - ) + .where(and(eq(siteResources.siteResourceId, siteResourceId))) .limit(1); if (!existingSiteResource) { @@ -85,19 +67,13 @@ export async function deleteSiteResource( // Delete the site resource const [removedSiteResource] = await trx .delete(siteResources) - .where( - and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - ) - ) + .where(and(eq(siteResources.siteResourceId, siteResourceId))) .returning(); const [newt] = await trx .select() .from(newts) - .where(eq(newts.siteId, site.siteId)) + .where(eq(newts.siteId, removedSiteResource.siteId)) .limit(1); if (!newt) { @@ -113,7 +89,7 @@ export async function deleteSiteResource( }); logger.info( - `Deleted site resource ${siteResourceId} for site ${siteId}` + `Deleted site resource ${siteResourceId}` ); return response(res, { diff --git a/server/routers/siteResource/getSiteResource.ts b/server/routers/siteResource/getSiteResource.ts index 7cb9e620..f97a5e22 100644 --- a/server/routers/siteResource/getSiteResource.ts +++ b/server/routers/siteResource/getSiteResource.ts @@ -63,7 +63,7 @@ export type GetSiteResourceResponse = NonNullable< registry.registerPath({ method: "get", - path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", + path: "/site-resource/{siteResourceId}", description: "Get a specific site resource by siteResourceId.", tags: [OpenAPITags.Client, OpenAPITags.Org], request: { diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index f6975cd2..7b2e0233 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -97,6 +97,9 @@ export async function listAllSiteResourcesByOrg( destination: siteResources.destination, enabled: siteResources.enabled, alias: siteResources.alias, + tcpPortRangeString: siteResources.tcpPortRangeString, + udpPortRangeString: siteResources.udpPortRangeString, + disableIcmp: siteResources.disableIcmp, siteName: sites.name, siteNiceId: sites.niceId, siteAddress: sites.address diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 92704adb..c0383616 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -5,6 +5,7 @@ import { clientSiteResourcesAssociationsCache, db, newts, + orgs, roles, roleSiteResources, sites, @@ -23,7 +24,9 @@ import { updatePeerData, updateTargets } from "@server/routers/client/targets"; import { generateAliasConfig, generateRemoteSubnets, - generateSubnetProxyTargets + generateSubnetProxyTargets, + isIpInCidr, + portRangeStringSchema } from "@server/lib/ip"; import { getClientSiteResourceAccess, @@ -31,14 +34,13 @@ import { } from "@server/lib/rebuildClientAssociations"; const updateSiteResourceParamsSchema = z.strictObject({ - siteResourceId: z.string().transform(Number).pipe(z.int().positive()), - siteId: z.string().transform(Number).pipe(z.int().positive()), - orgId: z.string() + siteResourceId: z.string().transform(Number).pipe(z.int().positive()) }); const updateSiteResourceSchema = z .strictObject({ name: z.string().min(1).max(255).optional(), + siteId: z.int(), // mode: z.enum(["host", "cidr", "port"]).optional(), mode: z.enum(["host", "cidr"]).optional(), // protocol: z.enum(["tcp", "udp"]).nullish(), @@ -49,13 +51,16 @@ const updateSiteResourceSchema = z alias: z .string() .regex( - /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, - "Alias must be a fully qualified domain name (e.g., example.internal)" + /^(?:[a-zA-Z0-9*?](?:[a-zA-Z0-9*?-]{0,61}[a-zA-Z0-9*?])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, + "Alias must be a fully qualified domain name with optional wildcards (e.g., example.internal, *.example.internal, host-0?.example.internal)" ) .nullish(), userIds: z.array(z.string()), roleIds: z.array(z.int()), - clientIds: z.array(z.int()) + clientIds: z.array(z.int()), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema, + disableIcmp: z.boolean().optional() }) .strict() .refine( @@ -74,7 +79,10 @@ const updateSiteResourceSchema = z const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; const isValidDomain = domainRegex.test(data.destination); - const isValidAlias = data.alias && domainRegex.test(data.alias); + const isValidAlias = + data.alias !== undefined && + data.alias !== null && + data.alias.trim() !== ""; return isValidDomain && isValidAlias; // require the alias to be set in the case of domain } @@ -90,8 +98,7 @@ const updateSiteResourceSchema = z if (data.mode === "cidr" && data.destination) { // Check if it's a valid CIDR (v4 or v6) const isValidCIDR = z - // .union([z.cidrv4(), z.cidrv6()]) - .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere + .union([z.cidrv4(), z.cidrv6()]) .safeParse(data.destination).success; return isValidCIDR; } @@ -107,7 +114,7 @@ export type UpdateSiteResourceResponse = SiteResource; registry.registerPath({ method: "post", - path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", + path: "/site-resource/{siteResourceId}", description: "Update a site resource.", tags: [OpenAPITags.Client, OpenAPITags.Org], request: { @@ -151,22 +158,26 @@ export async function updateSiteResource( ); } - const { siteResourceId, siteId, orgId } = parsedParams.data; + const { siteResourceId } = parsedParams.data; const { name, + siteId, // because it can change mode, destination, alias, enabled, userIds, roleIds, - clientIds + clientIds, + tcpPortRangeString, + udpPortRangeString, + disableIcmp } = parsedBody.data; const [site] = await db .select() .from(sites) - .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) + .where(eq(sites.siteId, siteId)) .limit(1); if (!site) { @@ -177,13 +188,7 @@ export async function updateSiteResource( const [existingSiteResource] = await db .select() .from(siteResources) - .where( - and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - ) - ) + .where(and(eq(siteResources.siteResourceId, siteResourceId))) .limit(1); if (!existingSiteResource) { @@ -192,6 +197,60 @@ export async function updateSiteResource( ); } + const [org] = await db + .select() + .from(orgs) + .where(eq(orgs.orgId, existingSiteResource.orgId)) + .limit(1); + + if (!org) { + return next(createHttpError(HttpCode.NOT_FOUND, "Organization not found")); + } + + if (!org.subnet || !org.utilitySubnet) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Organization with ID ${existingSiteResource.orgId} has no subnet or utilitySubnet defined defined` + ) + ); + } + + // Only check if destination is an IP address + const isIp = z.union([z.ipv4(), z.ipv6()]).safeParse(destination).success; + if ( + isIp && + (isIpInCidr(destination!, org.subnet) || isIpInCidr(destination!, org.utilitySubnet)) + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "IP can not be in the CIDR range of the organization's subnet or utility subnet" + ) + ); + } + + let existingSite = site; + let siteChanged = false; + if (existingSiteResource.siteId !== siteId) { + siteChanged = true; + // get the existing site + [existingSite] = await db + .select() + .from(sites) + .where(eq(sites.siteId, existingSiteResource.siteId)) + .limit(1); + + if (!existingSite) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Existing site not found" + ) + ); + } + } + // make sure the alias is unique within the org if provided if (alias) { const [conflict] = await db @@ -199,7 +258,7 @@ export async function updateSiteResource( .from(siteResources) .where( and( - eq(siteResources.orgId, orgId), + eq(siteResources.orgId, existingSiteResource.orgId), eq(siteResources.alias, alias.trim()), ne(siteResources.siteResourceId, siteResourceId) // exclude self ) @@ -218,97 +277,220 @@ export async function updateSiteResource( let updatedSiteResource: SiteResource | undefined; await db.transaction(async (trx) => { - // Update the site resource - [updatedSiteResource] = await trx - .update(siteResources) - .set({ - name: name, - mode: mode, - destination: destination, - enabled: enabled, - alias: alias && alias.trim() ? alias : null - }) - .where( - and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - ) - ) - .returning(); - - //////////////////// update the associations //////////////////// - - await trx - .delete(clientSiteResources) - .where(eq(clientSiteResources.siteResourceId, siteResourceId)); - - if (clientIds.length > 0) { - await trx.insert(clientSiteResources).values( - clientIds.map((clientId) => ({ - clientId, - siteResourceId - })) - ); - } - - await trx - .delete(userSiteResources) - .where(eq(userSiteResources.siteResourceId, siteResourceId)); - - if (userIds.length > 0) { + // if the site is changed we need to delete and recreate the resource to avoid complications with the rebuild function otherwise we can just update in place + if (siteChanged) { + // delete the existing site resource await trx - .insert(userSiteResources) - .values( - userIds.map((userId) => ({ userId, siteResourceId })) + .delete(siteResources) + .where( + and(eq(siteResources.siteResourceId, siteResourceId)) ); - } - // Get all admin role IDs for this org to exclude from deletion - const adminRoles = await trx - .select() - .from(roles) - .where( - and( - eq(roles.isAdmin, true), - eq(roles.orgId, updatedSiteResource.orgId) - ) + await rebuildClientAssociationsFromSiteResource( + existingSiteResource, + trx ); - const adminRoleIds = adminRoles.map((role) => role.roleId); - if (adminRoleIds.length > 0) { - await trx.delete(roleSiteResources).where( - and( - eq(roleSiteResources.siteResourceId, siteResourceId), - ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role + // create the new site resource from the removed one - the ID should stay the same + const [insertedSiteResource] = await trx + .insert(siteResources) + .values({ + ...existingSiteResource, + }) + .returning(); + + // wait some time to allow for messages to be handled + await new Promise((resolve) => setTimeout(resolve, 750)); + + [updatedSiteResource] = await trx + .update(siteResources) + .set({ + name: name, + siteId: siteId, + mode: mode, + destination: destination, + enabled: enabled, + alias: alias && alias.trim() ? alias : null, + tcpPortRangeString: tcpPortRangeString, + udpPortRangeString: udpPortRangeString, + disableIcmp: disableIcmp + }) + .where( + and( + eq( + siteResources.siteResourceId, + insertedSiteResource.siteResourceId + ) + ) ) + .returning(); + + if (!updatedSiteResource) { + throw new Error( + "Failed to create updated site resource after site change" + ); + } + + //////////////////// update the associations //////////////////// + + const [adminRole] = await trx + .select() + .from(roles) + .where( + and( + eq(roles.isAdmin, true), + eq(roles.orgId, updatedSiteResource.orgId) + ) + ) + .limit(1); + + if (!adminRole) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Admin role not found` + ) + ); + } + + await trx.insert(roleSiteResources).values({ + roleId: adminRole.roleId, + siteResourceId: updatedSiteResource.siteResourceId + }); + + if (roleIds.length > 0) { + await trx.insert(roleSiteResources).values( + roleIds.map((roleId) => ({ + roleId, + siteResourceId: updatedSiteResource!.siteResourceId + })) + ); + } + + if (userIds.length > 0) { + await trx.insert(userSiteResources).values( + userIds.map((userId) => ({ + userId, + siteResourceId: updatedSiteResource!.siteResourceId + })) + ); + } + + if (clientIds.length > 0) { + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId: updatedSiteResource!.siteResourceId + })) + ); + } + + await rebuildClientAssociationsFromSiteResource( + updatedSiteResource, + trx ); } else { - await trx - .delete(roleSiteResources) + // Update the site resource + [updatedSiteResource] = await trx + .update(siteResources) + .set({ + name: name, + siteId: siteId, + mode: mode, + destination: destination, + enabled: enabled, + alias: alias && alias.trim() ? alias : null, + tcpPortRangeString: tcpPortRangeString, + udpPortRangeString: udpPortRangeString, + disableIcmp: disableIcmp + }) .where( - eq(roleSiteResources.siteResourceId, siteResourceId) - ); - } + and(eq(siteResources.siteResourceId, siteResourceId)) + ) + .returning(); + + //////////////////// update the associations //////////////////// - if (roleIds.length > 0) { await trx - .insert(roleSiteResources) - .values( - roleIds.map((roleId) => ({ roleId, siteResourceId })) + .delete(clientSiteResources) + .where( + eq(clientSiteResources.siteResourceId, siteResourceId) ); + + if (clientIds.length > 0) { + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId + })) + ); + } + + await trx + .delete(userSiteResources) + .where( + eq(userSiteResources.siteResourceId, siteResourceId) + ); + + if (userIds.length > 0) { + await trx.insert(userSiteResources).values( + userIds.map((userId) => ({ + userId, + siteResourceId + })) + ); + } + + // Get all admin role IDs for this org to exclude from deletion + const adminRoles = await trx + .select() + .from(roles) + .where( + and( + eq(roles.isAdmin, true), + eq(roles.orgId, updatedSiteResource.orgId) + ) + ); + const adminRoleIds = adminRoles.map((role) => role.roleId); + + if (adminRoleIds.length > 0) { + await trx.delete(roleSiteResources).where( + and( + eq( + roleSiteResources.siteResourceId, + siteResourceId + ), + ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role + ) + ); + } else { + await trx + .delete(roleSiteResources) + .where( + eq(roleSiteResources.siteResourceId, siteResourceId) + ); + } + + if (roleIds.length > 0) { + await trx.insert(roleSiteResources).values( + roleIds.map((roleId) => ({ + roleId, + siteResourceId + })) + ); + } + + logger.info( + `Updated site resource ${siteResourceId} for site ${siteId}` + ); + + await handleMessagingForUpdatedSiteResource( + existingSiteResource, + updatedSiteResource, + { siteId: site.siteId, orgId: site.orgId }, + trx + ); } - - logger.info( - `Updated site resource ${siteResourceId} for site ${siteId}` - ); - - await handleMessagingForUpdatedSiteResource( - existingSiteResource, - updatedSiteResource!, - { siteId: site.siteId, orgId: site.orgId }, - trx - ); }); return response(res, { @@ -335,6 +517,10 @@ export async function handleMessagingForUpdatedSiteResource( site: { siteId: number; orgId: string }, trx: Transaction ) { + + logger.debug("handleMessagingForUpdatedSiteResource: existingSiteResource is: ", existingSiteResource); + logger.debug("handleMessagingForUpdatedSiteResource: updatedSiteResource is: ", updatedSiteResource); + const { mergedAllClients } = await rebuildClientAssociationsFromSiteResource( existingSiteResource || updatedSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below @@ -348,10 +534,18 @@ export async function handleMessagingForUpdatedSiteResource( const aliasChanged = existingSiteResource && existingSiteResource.alias !== updatedSiteResource.alias; + const portRangesChanged = + existingSiteResource && + (existingSiteResource.tcpPortRangeString !== + updatedSiteResource.tcpPortRangeString || + existingSiteResource.udpPortRangeString !== + updatedSiteResource.udpPortRangeString || + existingSiteResource.disableIcmp !== + updatedSiteResource.disableIcmp); // if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all - if (destinationChanged || aliasChanged) { + if (destinationChanged || aliasChanged || portRangesChanged) { const [newt] = await trx .select() .from(newts) @@ -365,7 +559,7 @@ export async function handleMessagingForUpdatedSiteResource( } // Only update targets on newt if destination changed - if (destinationChanged) { + if (destinationChanged || portRangesChanged) { const oldTargets = generateSubnetProxyTargets( existingSiteResource, mergedAllClients diff --git a/server/routers/user/removeInvitation.ts b/server/routers/user/removeInvitation.ts index 6a000afc..ab6a96d2 100644 --- a/server/routers/user/removeInvitation.ts +++ b/server/routers/user/removeInvitation.ts @@ -8,12 +8,24 @@ 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 removeInvitationParamsSchema = z.strictObject({ orgId: z.string(), inviteId: z.string() }); +registry.registerPath({ + method: "delete", + path: "/org/{orgId}/invitations/{inviteId}", + description: "Remove an open invitation from an organization", + tags: [OpenAPITags.Org], + request: { + params: removeInvitationParamsSchema + }, + responses: {} +}); + export async function removeInvitation( req: Request, res: Response, diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts index 64298029..87b86321 100644 --- a/server/setup/ensureSetupToken.ts +++ b/server/setup/ensureSetupToken.ts @@ -16,11 +16,23 @@ function generateToken(): string { return generateRandomString(random, alphabet, 32); } +function validateToken(token: string): boolean { + const tokenRegex = /^[a-z0-9]{32}$/; + return tokenRegex.test(token); +} + function generateId(length: number): string { const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; return generateRandomString(random, alphabet, length); } +function showSetupToken(token: string, source: string): void { + console.log(`=== SETUP TOKEN ${source} ===`); + console.log("Token:", token); + console.log("Use this token on the initial setup page"); + console.log("================================"); +} + export async function ensureSetupToken() { try { // Check if a server admin already exists @@ -38,17 +50,48 @@ export async function ensureSetupToken() { } // Check if a setup token already exists - const existingTokens = await db + const [existingToken] = await db .select() .from(setupTokens) .where(eq(setupTokens.used, false)); + const envSetupToken = process.env.PANGOLIN_SETUP_TOKEN; + console.debug("PANGOLIN_SETUP_TOKEN:", envSetupToken); + if (envSetupToken) { + if (!validateToken(envSetupToken)) { + throw new Error( + "invalid token format for PANGOLIN_SETUP_TOKEN" + ); + } + + if (existingToken?.token !== envSetupToken) { + console.warn( + "Overwriting existing token in DB since PANGOLIN_SETUP_TOKEN is set" + ); + + await db + .update(setupTokens) + .set({ token: envSetupToken }) + .where(eq(setupTokens.tokenId, existingToken.tokenId)); + } else { + const tokenId = generateId(15); + + await db.insert(setupTokens).values({ + tokenId: tokenId, + token: envSetupToken, + used: false, + dateCreated: moment().toISOString(), + dateUsed: null + }); + } + + showSetupToken(envSetupToken, "FROM ENVIRONMENT"); + return; + } + // If unused token exists, display it instead of creating a new one - if (existingTokens.length > 0) { - console.log("=== SETUP TOKEN EXISTS ==="); - console.log("Token:", existingTokens[0].token); - console.log("Use this token on the initial setup page"); - console.log("================================"); + if (existingToken) { + showSetupToken(existingToken.token, "EXISTS"); return; } @@ -64,10 +107,7 @@ export async function ensureSetupToken() { dateUsed: null }); - console.log("=== SETUP TOKEN GENERATED ==="); - console.log("Token:", token); - console.log("Use this token on the initial setup page"); - console.log("================================"); + showSetupToken(token, "GENERATED"); } catch (error) { console.error("Failed to ensure setup token:", error); throw error; diff --git a/src/app/[orgId]/settings/(private)/billing/layout.tsx b/src/app/[orgId]/settings/(private)/billing/layout.tsx index e52f19ed..c4048bcc 100644 --- a/src/app/[orgId]/settings/(private)/billing/layout.tsx +++ b/src/app/[orgId]/settings/(private)/billing/layout.tsx @@ -1,16 +1,11 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; -import { GetOrgResponse } from "@server/routers/org"; -import { GetOrgUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; -import { cache } from "react"; import { getTranslations } from "next-intl/server"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; type BillingSettingsProps = { children: React.ReactNode; @@ -23,8 +18,7 @@ export default async function BillingSettingsPage({ }: BillingSettingsProps) { const { orgId } = await params; - const getUser = cache(verifySession); - const user = await getUser(); + const user = await verifySession(); if (!user) { redirect(`/`); @@ -32,13 +26,7 @@ export default async function BillingSettingsPage({ let orgUser = null; try { - const getOrgUser = cache(async () => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - await authCookieHeader() - ) - ); - const res = await getOrgUser(); + const res = await getCachedOrgUser(orgId, user.userId); orgUser = res.data.data; } catch { redirect(`/${orgId}`); @@ -46,13 +34,7 @@ export default async function BillingSettingsPage({ let org = null; try { - const getOrg = cache(async () => - internal.get>( - `/org/${orgId}`, - await authCookieHeader() - ) - ); - const res = await getOrg(); + const res = await getCachedOrg(orgId); org = res.data.data; } catch { redirect(`/${orgId}`); diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx index 7cdea07a..6cdbf23c 100644 --- a/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx +++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx @@ -3,7 +3,7 @@ import { GetIdpResponse as GetOrgIdpResponse } from "@server/routers/idp"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; @@ -28,7 +28,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { redirect(`/${params.orgId}/settings/idp`); } - const navItems: HorizontalTabs = [ + const navItems: TabItem[] = [ { title: t("general"), href: `/${params.orgId}/settings/idp/${params.idpId}/general` diff --git a/src/app/[orgId]/settings/(private)/idp/create/page.tsx b/src/app/[orgId]/settings/(private)/idp/create/page.tsx index a899a2aa..786c8635 100644 --- a/src/app/[orgId]/settings/(private)/idp/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/idp/create/page.tsx @@ -303,6 +303,24 @@ export default function Page() { +
+
+ + {t("idpType")} + +
+ { + handleProviderChange( + value as "oidc" | "google" | "azure" + ); + }} + cols={3} + /> +
+
- - - - {t("idpType")} - - - {t("idpTypeDescription")} - - - - { - handleProviderChange( - value as "oidc" | "google" | "azure" - ); - }} - cols={3} - /> - - - {/* Auto Provision Settings */} @@ -705,29 +700,6 @@ export default function Page() { id="create-idp-form" onSubmit={form.handleSubmit(onSubmit)} > - - - - {t("idpJmespathAbout")} - - - {t( - "idpJmespathAboutDescription" - )}{" "} - - {t( - "idpJmespathAboutDescriptionLink" - )}{" "} - - - - - - {t("generatedcredentials")} + {t("credentials")} - {t("regenerateCredentials")} + {t("remoteNodeCredentialsDescription")} - + - {t("endpoint") || "Endpoint"} + {t("endpoint")} - {t("remoteExitNodeId") || - "Remote Exit Node ID"} + {t("remoteExitNodeId")} {displayRemoteExitNodeId ? ( @@ -168,7 +167,7 @@ export default function CredentialsPage() { - {t("secretKey") || "Secret Key"} + {t("remoteExitNodeSecretKey")} {displaySecret ? ( diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx index 98af49a6..ec1dea83 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/layout.tsx @@ -43,7 +43,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( <> diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx index 062b7e9a..0e68c379 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/create/page.tsx @@ -319,19 +319,6 @@ export default function CreateRemoteExitNodePage() { id: "${defaults?.remoteExitNodeId}" secret: "${defaults?.secret}"`} /> - - - - {t( - "remoteExitNodeCreate.generate.saveCredentialsTitle" - )} - - - {t( - "remoteExitNodeCreate.generate.saveCredentialsDescription" - )} - - )} diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx index 632dc0ad..2da0e0da 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx @@ -2,7 +2,9 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; import { AxiosResponse } from "axios"; -import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable"; +import ExitNodesTable, { + RemoteExitNodeRow +} from "@app/components/ExitNodesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; diff --git a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx index e75aa3eb..d212ab4a 100644 --- a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx @@ -22,7 +22,6 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { build } from "@server/build"; -import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; import { InfoSection, InfoSectionContent, @@ -32,6 +31,7 @@ import { import CopyToClipboard from "@app/components/CopyToClipboard"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -127,7 +127,7 @@ export default function CredentialsPage() { - + diff --git a/src/app/[orgId]/settings/clients/machine/create/page.tsx b/src/app/[orgId]/settings/clients/machine/create/page.tsx index 05ace912..42dfe0d5 100644 --- a/src/app/[orgId]/settings/clients/machine/create/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/create/page.tsx @@ -523,18 +523,6 @@ export default function Page() { - - - - - {t("clientCredentialsSave")} - - - {t( - "clientCredentialsSaveDescription" - )} - - diff --git a/src/app/[orgId]/settings/clients/machine/page.tsx b/src/app/[orgId]/settings/clients/machine/page.tsx index e1a904ad..f2618bc2 100644 --- a/src/app/[orgId]/settings/clients/machine/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/page.tsx @@ -1,6 +1,7 @@ import type { ClientRow } from "@app/components/MachineClientsTable"; import MachineClientsTable from "@app/components/MachineClientsTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import MachineClientsBanner from "@app/components/MachineClientsBanner"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListClientsResponse } from "@server/routers/client"; @@ -71,6 +72,8 @@ export default async function ClientsPage(props: ClientsPageProps) { description={t("manageMachineClientsDescription")} /> + + ; +} + +export default async function AuthPage(props: AuthPageProps) { + const orgId = (await props.params).orgId; + let subscriptionStatus: GetOrgTierResponse | null = null; + try { + const subRes = await getCachedSubscription(orgId); + subscriptionStatus = subRes.data.data; + } catch {} + + let loginPage: GetLoginPageResponse | null = null; + try { + if (build === "saas") { + const res = await internal.get>( + `/org/${orgId}/login-page`, + await authCookieHeader() + ); + if (res.status === 200) { + loginPage = res.data.data; + } + } + } catch (error) {} + + let loginPageBranding: GetLoginPageBrandingResponse | null = null; + try { + const res = await internal.get< + AxiosResponse + >(`/org/${orgId}/login-page-branding`, await authCookieHeader()); + if (res.status === 200) { + loginPageBranding = res.data.data; + } + } catch (error) {} + + return ( + + {build === "saas" && } + + + ); +} diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 8c8efa59..31c67966 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -1,16 +1,15 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { HorizontalTabs, type TabItem } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; -import { GetOrgResponse } from "@server/routers/org"; -import { GetOrgUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; +import OrgInfoCard from "@app/components/OrgInfoCard"; + import { redirect } from "next/navigation"; -import { cache } from "react"; import { getTranslations } from "next-intl/server"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; +import { build } from "@server/build"; type GeneralSettingsProps = { children: React.ReactNode; @@ -23,8 +22,7 @@ export default async function GeneralSettingsPage({ }: GeneralSettingsProps) { const { orgId } = await params; - const getUser = cache(verifySession); - const user = await getUser(); + const user = await verifySession(); if (!user) { redirect(`/`); @@ -32,13 +30,7 @@ export default async function GeneralSettingsPage({ let orgUser = null; try { - const getOrgUser = cache(async () => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - await authCookieHeader() - ) - ); - const res = await getOrgUser(); + const res = await getCachedOrgUser(orgId, user.userId); orgUser = res.data.data; } catch { redirect(`/${orgId}`); @@ -46,13 +38,7 @@ export default async function GeneralSettingsPage({ let org = null; try { - const getOrg = cache(async () => - internal.get>( - `/org/${orgId}`, - await authCookieHeader() - ) - ); - const res = await getOrg(); + const res = await getCachedOrg(orgId); org = res.data.data; } catch { redirect(`/${orgId}`); @@ -60,12 +46,19 @@ export default async function GeneralSettingsPage({ const t = await getTranslations(); - const navItems = [ + const navItems: TabItem[] = [ { title: t("general"), - href: `/{orgId}/settings/general` + href: `/{orgId}/settings/general`, + exact: true } ]; + if (build !== "oss") { + navItems.push({ + title: t("authPage"), + href: `/{orgId}/settings/general/auth-page` + }); + } return ( <> @@ -76,7 +69,10 @@ export default async function GeneralSettingsPage({ description={t("orgSettingsDescription")} /> - {children} +
+ + {children} +
diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index e391922f..3e78adc3 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -8,7 +8,13 @@ import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { userOrgUserContext } from "@app/hooks/useOrgUserContext"; import { toast } from "@app/hooks/useToast"; -import { useState, useRef } from "react"; +import { + useState, + useRef, + useTransition, + useActionState, + type ComponentRef +} from "react"; import { Form, FormControl, @@ -50,9 +56,12 @@ import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { SwitchInput } from "@app/components/SwitchInput"; -import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import type { t } from "@faker-js/faker/dist/airline-DF6RqYmq"; +import type { OrgContextType } from "@app/contexts/orgContext"; // Session length options in hours const SESSION_LENGTH_OPTIONS = [ @@ -111,93 +120,31 @@ const LOG_RETENTION_OPTIONS = [ ]; export default function GeneralPage() { - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const { orgUser } = userOrgUserContext(); - const router = useRouter(); const { org } = useOrgContext(); - const api = createApiClient(useEnvContext()); - const { user } = useUserContext(); + return ( + + + + + + {build !== "oss" && } + {build !== "saas" && } + + ); +} + +type SectionFormProps = { + org: OrgContextType["org"]["org"]; +}; + +function DeleteForm({ org }: SectionFormProps) { const t = useTranslations(); - const { env } = useEnvContext(); - const { licenseStatus, isUnlocked } = useLicenseStatusContext(); - const subscription = useSubscriptionStatusContext(); + const api = createApiClient(useEnvContext()); - // Check if security features are disabled due to licensing/subscription - const isSecurityFeatureDisabled = () => { - const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); - const isSaasNotSubscribed = - build === "saas" && !subscription?.isSubscribed(); - return isEnterpriseNotLicensed || isSaasNotSubscribed; - }; - - const [loadingDelete, setLoadingDelete] = useState(false); - const [loadingSave, setLoadingSave] = useState(false); - const [isSecurityPolicyConfirmOpen, setIsSecurityPolicyConfirmOpen] = - useState(false); - const authPageSettingsRef = useRef(null); - - const form = useForm({ - resolver: zodResolver(GeneralFormSchema), - defaultValues: { - name: org?.org.name, - subnet: org?.org.subnet || "", // Add default value for subnet - requireTwoFactor: org?.org.requireTwoFactor || false, - maxSessionLengthHours: org?.org.maxSessionLengthHours || null, - passwordExpiryDays: org?.org.passwordExpiryDays || null, - settingsLogRetentionDaysRequest: - org.org.settingsLogRetentionDaysRequest ?? 15, - settingsLogRetentionDaysAccess: - org.org.settingsLogRetentionDaysAccess ?? 15, - settingsLogRetentionDaysAction: - org.org.settingsLogRetentionDaysAction ?? 15 - }, - mode: "onChange" - }); - - // Track initial security policy values - const initialSecurityValues = { - requireTwoFactor: org?.org.requireTwoFactor || false, - maxSessionLengthHours: org?.org.maxSessionLengthHours || null, - passwordExpiryDays: org?.org.passwordExpiryDays || null - }; - - // Check if security policies have changed - const hasSecurityPolicyChanged = () => { - const currentValues = form.getValues(); - return ( - currentValues.requireTwoFactor !== - initialSecurityValues.requireTwoFactor || - currentValues.maxSessionLengthHours !== - initialSecurityValues.maxSessionLengthHours || - currentValues.passwordExpiryDays !== - initialSecurityValues.passwordExpiryDays - ); - }; - - async function deleteOrg() { - setLoadingDelete(true); - try { - const res = await api.delete>( - `/org/${org?.org.orgId}` - ); - toast({ - title: t("orgDeleted"), - description: t("orgDeletedMessage") - }); - if (res.status === 200) { - pickNewOrgAndNavigate(); - } - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("orgErrorDelete"), - description: formatAxiosError(err, t("orgErrorDeleteMessage")) - }); - } finally { - setLoadingDelete(false); - } - } + const router = useRouter(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [loadingDelete, startTransition] = useTransition(); + const { user } = useUserContext(); async function pickNewOrgAndNavigate() { try { @@ -224,46 +171,109 @@ export default function GeneralPage() { }); } } - - async function onSubmit(data: GeneralFormValues) { - // Check if security policies have changed - if (hasSecurityPolicyChanged()) { - setIsSecurityPolicyConfirmOpen(true); - return; + async function deleteOrg() { + try { + const res = await api.delete>( + `/org/${org.orgId}` + ); + toast({ + title: t("orgDeleted"), + description: t("orgDeletedMessage") + }); + if (res.status === 200) { + pickNewOrgAndNavigate(); + } + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("orgErrorDelete"), + description: formatAxiosError(err, t("orgErrorDeleteMessage")) + }); } - - await performSave(data); } + return ( + <> + { + setIsDeleteModalOpen(val); + }} + dialog={ +
+

{t("orgQuestionRemove")}

+

{t("orgMessageRemove")}

+
+ } + buttonText={t("orgDeleteConfirm")} + onConfirm={async () => startTransition(deleteOrg)} + string={org.name || ""} + title={t("orgDelete")} + /> + + + + {t("dangerSection")} + + + {t("dangerSectionDescription")} + + + + + + + + ); +} - async function performSave(data: GeneralFormValues) { - setLoadingSave(true); +function GeneralSectionForm({ org }: SectionFormProps) { + const { updateOrg } = useOrgContext(); + const form = useForm({ + resolver: zodResolver( + GeneralFormSchema.pick({ + name: true, + subnet: true + }) + ), + defaultValues: { + name: org.name, + subnet: org.subnet || "" // Add default value for subnet + }, + mode: "onChange" + }); + const t = useTranslations(); + const router = useRouter(); + + const [, formAction, loadingSave] = useActionState(performSave, null); + const api = createApiClient(useEnvContext()); + + async function performSave() { + const isValid = await form.trigger(); + if (!isValid) return; + + const data = form.getValues(); try { const reqData = { - name: data.name, - settingsLogRetentionDaysRequest: - data.settingsLogRetentionDaysRequest, - settingsLogRetentionDaysAccess: - data.settingsLogRetentionDaysAccess, - settingsLogRetentionDaysAction: - data.settingsLogRetentionDaysAction + name: data.name } as any; - if (build !== "oss") { - reqData.requireTwoFactor = data.requireTwoFactor || false; - reqData.maxSessionLengthHours = data.maxSessionLengthHours; - reqData.passwordExpiryDays = data.passwordExpiryDays; - } // Update organization - await api.post(`/org/${org?.org.orgId}`, reqData); + await api.post(`/org/${org.orgId}`, reqData); - // Also save auth page settings if they have unsaved changes - if ( - build === "saas" && - authPageSettingsRef.current?.hasUnsavedChanges() - ) { - await authPageSettingsRef.current.saveAuthSettings(); - } + // Update the org context to reflect the change in the info card + updateOrg({ + name: data.name + }); toast({ title: t("orgUpdated"), @@ -276,586 +286,691 @@ export default function GeneralPage() { title: t("orgErrorUpdate"), description: formatAxiosError(e, t("orgErrorUpdateMessage")) }); - } finally { - setLoadingSave(false); } } return ( - - { - setIsDeleteModalOpen(val); - }} - dialog={ -
-

{t("orgQuestionRemove")}

-

{t("orgMessageRemove")}

-
- } - buttonText={t("orgDeleteConfirm")} - onConfirm={deleteOrg} - string={org?.org.name || ""} - title={t("orgDelete")} - /> - -

{t("securityPolicyChangeDescription")}

- - } - buttonText={t("saveSettings")} - onConfirm={() => performSave(form.getValues())} - string={t("securityPolicyChangeConfirmMessage")} - title={t("securityPolicyChangeWarning")} - warningText={t("securityPolicyChangeWarningText")} - /> - - - - - - - {t("general")} - - - {t("orgGeneralSettingsDescription")} - - - - - ( - - {t("name")} - - - - - - {t("orgDisplayName")} - - - )} - /> - ( - - {t("subnet")} - - - - - - {t("subnetDescription")} - - - )} - /> - - - - - - - - {t("logRetention")} - - - {t("logRetentionDescription")} - - - - - ( - - - {t("logRetentionRequestLabel")} - - - - - - - )} - /> - - {build != "oss" && ( - <> - - - { - const isDisabled = - (build == "saas" && - !subscription?.subscribed) || - (build == "enterprise" && - !isUnlocked()); - - return ( - - - {t( - "logRetentionAccessLabel" - )} - - - - - - - ); - }} - /> - { - const isDisabled = - (build == "saas" && - !subscription?.subscribed) || - (build == "enterprise" && - !isUnlocked()); - - return ( - - - {t( - "logRetentionActionLabel" - )} - - - - - - - ); - }} - /> - + + + {t("general")} + + {t("orgGeneralSettingsDescription")} + + + + + + + ( + + {t("name")} + + + + + + {t("orgDisplayName")} + + )} - - - + /> + + + + - {build !== "oss" && ( - - - - {t("securitySettings")} - - - {t("securitySettingsDescription")} - - - - - - { - const isDisabled = - isSecurityFeatureDisabled(); - - return ( - -
- - { - if ( - !isDisabled - ) { - form.setValue( - "requireTwoFactor", - val - ); - } - }} - /> - -
- - - {t( - "requireTwoFactorDescription" - )} - -
- ); - }} - /> - { - const isDisabled = - isSecurityFeatureDisabled(); - - return ( - - - {t("maxSessionLength")} - - - - - - - {t( - "maxSessionLengthDescription" - )} - - - ); - }} - /> - { - const isDisabled = - isSecurityFeatureDisabled(); - - return ( - - - {t( - "passwordExpiryDays" - )} - - - - - - - {t( - "editPasswordExpiryDescription" - )} - - - ); - }} - /> -
-
-
- )} - - - - {build === "saas" && } - -
- {build !== "saas" && ( - - )} +
- + + ); +} + +function LogRetentionSectionForm({ org }: SectionFormProps) { + const form = useForm({ + resolver: zodResolver( + GeneralFormSchema.pick({ + settingsLogRetentionDaysRequest: true, + settingsLogRetentionDaysAccess: true, + settingsLogRetentionDaysAction: true + }) + ), + defaultValues: { + settingsLogRetentionDaysRequest: + org.settingsLogRetentionDaysRequest ?? 15, + settingsLogRetentionDaysAccess: + org.settingsLogRetentionDaysAccess ?? 15, + settingsLogRetentionDaysAction: + org.settingsLogRetentionDaysAction ?? 15 + }, + mode: "onChange" + }); + + const router = useRouter(); + const t = useTranslations(); + const { isPaidUser, hasSaasSubscription } = usePaidStatus(); + + const [, formAction, loadingSave] = useActionState(performSave, null); + const api = createApiClient(useEnvContext()); + + async function performSave() { + const isValid = await form.trigger(); + if (!isValid) return; + + const data = form.getValues(); + + try { + const reqData = { + settingsLogRetentionDaysRequest: + data.settingsLogRetentionDaysRequest, + settingsLogRetentionDaysAccess: + data.settingsLogRetentionDaysAccess, + settingsLogRetentionDaysAction: + data.settingsLogRetentionDaysAction + } as any; + + // Update organization + await api.post(`/org/${org.orgId}`, reqData); + + toast({ + title: t("orgUpdated"), + description: t("orgUpdatedDescription") + }); + router.refresh(); + } catch (e) { + toast({ + variant: "destructive", + title: t("orgErrorUpdate"), + description: formatAxiosError(e, t("orgErrorUpdateMessage")) + }); + } + } + + return ( + + + {t("logRetention")} + + {t("logRetentionDescription")} + + + + +
+ + ( + + + {t("logRetentionRequestLabel")} + + + + + + + )} + /> + + {build !== "oss" && ( + <> + + + { + const isDisabled = !isPaidUser; + + return ( + + + {t( + "logRetentionAccessLabel" + )} + + + + + + + ); + }} + /> + { + const isDisabled = !isPaidUser; + + return ( + + + {t( + "logRetentionActionLabel" + )} + + + + + + + ); + }} + /> + + )} + + +
+
+ +
+ +
+
+ ); +} + +function SecuritySettingsSectionForm({ org }: SectionFormProps) { + const router = useRouter(); + const form = useForm({ + resolver: zodResolver( + GeneralFormSchema.pick({ + requireTwoFactor: true, + maxSessionLengthHours: true, + passwordExpiryDays: true + }) + ), + defaultValues: { + requireTwoFactor: org.requireTwoFactor || false, + maxSessionLengthHours: org.maxSessionLengthHours || null, + passwordExpiryDays: org.passwordExpiryDays || null + }, + mode: "onChange" + }); + const t = useTranslations(); + const { isPaidUser } = usePaidStatus(); + + // Track initial security policy values + const initialSecurityValues = { + requireTwoFactor: org.requireTwoFactor || false, + maxSessionLengthHours: org.maxSessionLengthHours || null, + passwordExpiryDays: org.passwordExpiryDays || null + }; + + const [isSecurityPolicyConfirmOpen, setIsSecurityPolicyConfirmOpen] = + useState(false); + + // Check if security policies have changed + const hasSecurityPolicyChanged = () => { + const currentValues = form.getValues(); + return ( + currentValues.requireTwoFactor !== + initialSecurityValues.requireTwoFactor || + currentValues.maxSessionLengthHours !== + initialSecurityValues.maxSessionLengthHours || + currentValues.passwordExpiryDays !== + initialSecurityValues.passwordExpiryDays + ); + }; + + const [, formAction, loadingSave] = useActionState(onSubmit, null); + const api = createApiClient(useEnvContext()); + + const formRef = useRef>(null); + + async function onSubmit() { + // Check if security policies have changed + if (hasSecurityPolicyChanged()) { + setIsSecurityPolicyConfirmOpen(true); + return; + } + + await performSave(); + } + + async function performSave() { + const isValid = await form.trigger(); + if (!isValid) return; + + const data = form.getValues(); + + try { + const reqData = { + requireTwoFactor: data.requireTwoFactor || false, + maxSessionLengthHours: data.maxSessionLengthHours, + passwordExpiryDays: data.passwordExpiryDays + } as any; + + // Update organization + await api.post(`/org/${org.orgId}`, reqData); + + toast({ + title: t("orgUpdated"), + description: t("orgUpdatedDescription") + }); + router.refresh(); + } catch (e) { + toast({ + variant: "destructive", + title: t("orgErrorUpdate"), + description: formatAxiosError(e, t("orgErrorUpdateMessage")) + }); + } + } + + return ( + <> + +

{t("securityPolicyChangeDescription")}

+
+ } + buttonText={t("saveSettings")} + onConfirm={performSave} + string={t("securityPolicyChangeConfirmMessage")} + title={t("securityPolicyChangeWarning")} + warningText={t("securityPolicyChangeWarningText")} + /> + + + + {t("securitySettings")} + + + {t("securitySettingsDescription")} + + + + +
+ + + { + const isDisabled = !isPaidUser; + + return ( + +
+ + { + if ( + !isDisabled + ) { + form.setValue( + "requireTwoFactor", + val + ); + } + }} + /> + +
+ + + {t( + "requireTwoFactorDescription" + )} + +
+ ); + }} + /> + { + const isDisabled = !isPaidUser; + + return ( + + + {t("maxSessionLength")} + + + + + + + {t( + "maxSessionLengthDescription" + )} + + + ); + }} + /> + { + const isDisabled = !isPaidUser; + + return ( + + + {t("passwordExpiryDays")} + + + + + + + {t( + "editPasswordExpiryDescription" + )} + + + ); + }} + /> + + +
+
+ +
+ +
+
+ ); } diff --git a/src/app/[orgId]/settings/resources/client/page.tsx b/src/app/[orgId]/settings/resources/client/page.tsx index 49ccb97f..dfe61ef7 100644 --- a/src/app/[orgId]/settings/resources/client/page.tsx +++ b/src/app/[orgId]/settings/resources/client/page.tsx @@ -1,6 +1,7 @@ import type { InternalResourceRow } from "@app/components/ClientResourcesTable"; import ClientResourcesTable from "@app/components/ClientResourcesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import PrivateResourcesBanner from "@app/components/PrivateResourcesBanner"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { getCachedOrg } from "@app/lib/api/getCachedOrg"; @@ -67,7 +68,10 @@ export default async function ClientResourcesPage( // destinationPort: siteResource.destinationPort, alias: siteResource.alias || null, siteNiceId: siteResource.siteNiceId, - niceId: siteResource.niceId + niceId: siteResource.niceId, + tcpPortRangeString: siteResource.tcpPortRangeString || null, + udpPortRangeString: siteResource.udpPortRangeString || null, + disableIcmp: siteResource.disableIcmp || false, }; } ); @@ -78,6 +82,8 @@ export default async function ClientResourcesPage( description={t("clientResourceDescription")} /> + + ( - [] + const { data: whitelist = [], isLoading: isLoadingWhiteList } = useQuery( + resourceQueries.resourceWhitelist({ + resourceId: resource.resourceId + }) ); - const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>( - [] + + const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( + orgQueries.roles({ + orgId: org.org.orgId + }) ); + const { data: orgUsers = [], isLoading: isLoadingOrgUsers } = useQuery( + orgQueries.users({ + orgId: org.org.orgId + }) + ); + const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery( + orgQueries.identityProviders({ + orgId: org.org.orgId + }) + ); + + const pageLoading = + isLoadingOrgRoles || + isLoadingOrgUsers || + isLoadingResourceRoles || + isLoadingResourceUsers || + isLoadingWhiteList || + isLoadingOrgIdps; + + const allRoles = useMemo(() => { + return orgRoles + .map((role) => ({ + id: role.roleId.toString(), + text: role.name + })) + .filter((role) => role.text !== "Admin"); + }, [orgRoles]); + + const allUsers = useMemo(() => { + return orgUsers.map((user) => ({ + id: user.id.toString(), + text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + })); + }, [orgUsers]); + + const allIdps = useMemo(() => { + if (build === "saas") { + if (subscription?.subscribed) { + return orgIdps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })); + } + } else { + return orgIdps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })); + } + return []; + }, [orgIdps]); + const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< number | null >(null); @@ -115,15 +182,7 @@ export default function ResourceAuthenticationPage() { number | null >(null); - const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< - number | null - >(null); - const [ssoEnabled, setSsoEnabled] = useState(resource.sso); - // const [blockAccess, setBlockAccess] = useState(resource.blockAccess); - const [whitelistEnabled, setWhitelistEnabled] = useState( - resource.emailWhitelistEnabled - ); const [autoLoginEnabled, setAutoLoginEnabled] = useState( resource.skipToIdpId !== null && resource.skipToIdpId !== undefined @@ -131,10 +190,6 @@ export default function ResourceAuthenticationPage() { const [selectedIdpId, setSelectedIdpId] = useState( resource.skipToIdpId || null ); - const [allIdps, setAllIdps] = useState<{ id: number; text: string }[]>([]); - - const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false); - const [loadingSaveWhitelist, setLoadingSaveWhitelist] = useState(false); const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] = useState(false); @@ -159,167 +214,61 @@ export default function ResourceAuthenticationPage() { defaultValues: { emails: [] } }); + const hasInitializedRef = useRef(false); + useEffect(() => { - const fetchData = async () => { - try { - const [ - rolesResponse, - resourceRolesResponse, - usersResponse, - resourceUsersResponse, - whitelist, - idpsResponse - ] = await Promise.all([ - api.get>( - `/org/${org?.org.orgId}/roles` - ), - api.get>( - `/resource/${resource.resourceId}/roles` - ), - api.get>( - `/org/${org?.org.orgId}/users` - ), - api.get>( - `/resource/${resource.resourceId}/users` - ), - api.get>( - `/resource/${resource.resourceId}/whitelist` - ), - api.get< - AxiosResponse<{ - idps: { idpId: number; name: string }[]; - }> - >(build === "saas" ? `/org/${org?.org.orgId}/idp` : "/idp") - ]); + if (pageLoading || hasInitializedRef.current) return; - setAllRoles( - rolesResponse.data.data.roles - .map((role) => ({ - id: role.roleId.toString(), - text: role.name - })) - .filter((role) => role.text !== "Admin") - ); + usersRolesForm.setValue( + "roles", + resourceRoles + .map((i) => ({ + id: i.roleId.toString(), + text: i.name + })) + .filter((role) => role.text !== "Admin") + ); + usersRolesForm.setValue( + "users", + resourceUsers.map((i) => ({ + id: i.userId.toString(), + text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` + })) + ); - usersRolesForm.setValue( - "roles", - resourceRolesResponse.data.data.roles - .map((i) => ({ - id: i.roleId.toString(), - text: i.name - })) - .filter((role) => role.text !== "Admin") - ); - - setAllUsers( - usersResponse.data.data.users.map((user) => ({ - id: user.id.toString(), - text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` - })) - ); - - usersRolesForm.setValue( - "users", - resourceUsersResponse.data.data.users.map((i) => ({ - id: i.userId.toString(), - text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` - })) - ); - - whitelistForm.setValue( - "emails", - whitelist.data.data.whitelist.map((w) => ({ - id: w.email, - text: w.email - })) - ); - - if (build === "saas") { - if (subscription?.subscribed) { - setAllIdps( - idpsResponse.data.data.idps.map((idp) => ({ - id: idp.idpId, - text: idp.name - })) - ); - } - } else { - setAllIdps( - idpsResponse.data.data.idps.map((idp) => ({ - id: idp.idpId, - text: idp.name - })) - ); - } - - if ( - autoLoginEnabled && - !selectedIdpId && - idpsResponse.data.data.idps.length > 0 - ) { - setSelectedIdpId(idpsResponse.data.data.idps[0].idpId); - } - - setPageLoading(false); - } catch (e) { - console.error(e); - toast({ - variant: "destructive", - title: t("resourceErrorAuthFetch"), - description: formatAxiosError( - e, - t("resourceErrorAuthFetchDescription") - ) - }); - } - }; - - fetchData(); - }, []); - - async function saveWhitelist() { - setLoadingSaveWhitelist(true); - try { - await api.post(`/resource/${resource.resourceId}`, { - emailWhitelistEnabled: whitelistEnabled - }); - - if (whitelistEnabled) { - await api.post(`/resource/${resource.resourceId}/whitelist`, { - emails: whitelistForm.getValues().emails.map((i) => i.text) - }); - } - - updateResource({ - emailWhitelistEnabled: whitelistEnabled - }); - - toast({ - title: t("resourceWhitelistSave"), - description: t("resourceWhitelistSaveDescription") - }); - router.refresh(); - } catch (e) { - console.error(e); - toast({ - variant: "destructive", - title: t("resourceErrorWhitelistSave"), - description: formatAxiosError( - e, - t("resourceErrorWhitelistSaveDescription") - ) - }); - } finally { - setLoadingSaveWhitelist(false); + whitelistForm.setValue( + "emails", + whitelist.map((w) => ({ + id: w.email, + text: w.email + })) + ); + if (autoLoginEnabled && !selectedIdpId && orgIdps.length > 0) { + setSelectedIdpId(orgIdps[0].idpId); } - } + hasInitializedRef.current = true; + }, [ + pageLoading, + resourceRoles, + resourceUsers, + whitelist, + autoLoginEnabled, + selectedIdpId, + orgIdps + ]); + + const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState( + onSubmitUsersRoles, + null + ); + + async function onSubmitUsersRoles() { + const isValid = usersRolesForm.trigger(); + if (!isValid) return; + + const data = usersRolesForm.getValues(); - async function onSubmitUsersRoles( - data: z.infer - ) { try { - setLoadingSaveUsersRoles(true); - // Validate that an IDP is selected if auto login is enabled if (autoLoginEnabled && !selectedIdpId) { toast({ @@ -358,6 +307,17 @@ export default function ResourceAuthenticationPage() { title: t("resourceAuthSettingsSave"), description: t("resourceAuthSettingsSaveDescription") }); + await queryClient.invalidateQueries({ + predicate(query) { + const resourceKey = resourceQueries.resourceClients({ + resourceId: resource.resourceId + }).queryKey; + return ( + query.queryKey[0] === resourceKey[0] && + query.queryKey[1] === resourceKey[1] + ); + } + }); router.refresh(); } catch (e) { console.error(e); @@ -369,8 +329,6 @@ export default function ResourceAuthenticationPage() { t("resourceErrorUsersRolesSaveDescription") ) }); - } finally { - setLoadingSaveUsersRoles(false); } } @@ -534,9 +492,7 @@ export default function ResourceAuthenticationPage() {
@@ -661,7 +617,7 @@ export default function ResourceAuthenticationPage() { )} {ssoEnabled && allIdps.length > 0 && ( -
+ <>
+ + + + )} + /> + + ( + + + {t("identifier")} + + + + + + + )} + /> + + {!resource.http && ( + <> + ( + + + {t( + "resourcePortNumber" + )} + + + + field.onChange( + e.target + .value + ? parseInt( + e + .target + .value + ) + : undefined ) } /> -
- - - )} - /> - - ( - - - {t("name")} - - - - - - - )} - /> - - ( - - - {t("identifier")} - - - + + {t( + "resourcePortNumberDescription" )} - className="flex-1" - /> - - - - )} - /> + + + )} + /> + + )} - {!resource.http && ( - <> - ( - - - {t( - "resourcePortNumber" - )} - - - - field.onChange( - e - .target - .value - ? parseInt( - e - .target - .value - ) - : undefined - ) - } - /> - - - - {t( - "resourcePortNumberDescription" - )} - - - )} - /> - - {/* {build == "oss" && ( - ( - - - - -
- - {t( - "resourceEnableProxy" - )} - - - {t( - "resourceEnableProxyDescription" - )} - -
-
- )} - /> - )} */} - - )} - - {resource.http && ( -
- -
- - - {resourceFullDomain} - - -
+ {resource.http && ( +
+ +
+ + + {resourceFullDomain} + +
- )} - - - - +
+ )} + + + + - - - - - + + + + + - setEditDomainOpen(setOpen)} - > - - - Edit Domain - - Select a domain for your resource - - - - { - const selected = { - domainId: res.domainId, - subdomain: res.subdomain, - fullDomain: res.fullDomain, - baseDomain: res.baseDomain - }; - setSelectedDomain(selected); - }} - /> - - - - - - + + - - - - - ) + setResourceFullDomain( + `${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}` + ); + form.setValue( + "domainId", + selectedDomain.domainId + ); + form.setValue( + "subdomain", + sanitizedSubdomain + ); + + setEditDomainOpen(false); + } + }} + > + Select Domain + + + + + ); } diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx index c453b577..f410b4c8 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx @@ -13,9 +13,10 @@ import { GetOrgResponse } from "@server/routers/org"; import OrgProvider from "@app/providers/OrgProvider"; import { cache } from "react"; import ResourceInfoBox from "@app/components/ResourceInfoBox"; -import { GetSiteResponse } from "@server/routers/site"; import { getTranslations } from "next-intl/server"; +export const dynamic = "force-dynamic"; + interface ResourceLayoutProps { children: React.ReactNode; params: Promise<{ niceId: string; orgId: string }>; diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index 00f487ea..18e4e6b5 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, use } from "react"; +import HealthCheckDialog from "@/components/HealthCheckDialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -11,11 +11,34 @@ import { SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; -import { AxiosResponse } from "axios"; -import { ListTargetsResponse } from "@server/routers/target/listTargets"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; +import { ContainersSelector } from "@app/components/ContainersSelector"; +import { HeadersInput } from "@app/components/HeadersInput"; +import { + PathMatchDisplay, + PathMatchModal, + PathRewriteDisplay, + PathRewriteModal +} from "@app/components/PathMatchRenameModal"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { SwitchInput } from "@app/components/SwitchInput"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { Badge } from "@app/components/ui/badge"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; import { Form, FormControl, @@ -25,17 +48,11 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import { CreateTargetResponse } from "@server/routers/target"; import { - ColumnDef, - getFilteredRowModel, - getSortedRowModel, - getPaginationRowModel, - getCoreRowModel, - useReactTable, - flexRender, - Row -} from "@tanstack/react-table"; + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; import { Table, TableBody, @@ -44,153 +61,55 @@ import { TableHeader, TableRow } from "@app/components/ui/table"; -import { toast } from "@app/hooks/useToast"; -import { useResourceContext } from "@app/hooks/useResourceContext"; -import { ArrayElement } from "@server/types/ArrayElement"; -import { formatAxiosError } from "@app/lib/api/formatAxiosError"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { createApiClient } from "@app/lib/api"; -import { GetSiteResponse, ListSitesResponse } from "@server/routers/site"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle, - SettingsSectionDescription, - SettingsSectionBody, - 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"; -import { - CheckIcon, - ChevronsUpDown, - Settings, - Heart, - Check, - CircleCheck, - CircleX, - ArrowRight, - Plus, - MoveRight, - ArrowUp, - Info, - ArrowDown, - AlertTriangle -} from "lucide-react"; -import { ContainersSelector } from "@app/components/ContainersSelector"; -import { useTranslations } from "next-intl"; -import { build } from "@server/build"; -import HealthCheckDialog from "@/components/HealthCheckDialog"; -import { DockerManager, DockerState } from "@app/lib/docker"; -import { Container } from "@server/routers/site"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { cn } from "@app/lib/cn"; -import { CaretSortIcon } from "@radix-ui/react-icons"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { parseHostTarget } from "@app/lib/parseHostTarget"; -import { HeadersInput } from "@app/components/HeadersInput"; -import { - PathMatchDisplay, - PathMatchModal, - PathRewriteDisplay, - PathRewriteModal -} from "@app/components/PathMatchRenameModal"; -import { Badge } from "@app/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; -import { Alert, AlertDescription } from "@app/components/ui/alert"; - -const addTargetSchema = z - .object({ - ip: z.string().refine(isTargetValid), - method: z.string().nullable(), - port: z.coerce.number().int().positive(), - siteId: z.int().positive({ - error: "You must select a site for a target." - }), - path: z.string().optional().nullable(), - pathMatchType: z - .enum(["exact", "prefix", "regex"]) - .optional() - .nullable(), - rewritePath: z.string().optional().nullable(), - rewritePathType: z - .enum(["exact", "prefix", "regex", "stripPrefix"]) - .optional() - .nullable(), - priority: z.int().min(1).max(1000).optional() - }) - .refine( - (data) => { - // If path is provided, pathMatchType must be provided - if (data.path && !data.pathMatchType) { - return false; - } - // If pathMatchType is provided, path must be provided - if (data.pathMatchType && !data.path) { - return false; - } - // Validate path based on pathMatchType - if (data.path && data.pathMatchType) { - switch (data.pathMatchType) { - case "exact": - case "prefix": - // Path should start with / - return data.path.startsWith("/"); - case "regex": - // Validate regex - try { - new RegExp(data.path); - return true; - } catch { - return false; - } - } - } - return true; - }, - { - error: "Invalid path configuration" - } - ) - .refine( - (data) => { - // If rewritePath is provided, rewritePathType must be provided - if (data.rewritePath && !data.rewritePathType) { - return false; - } - // If rewritePathType is provided, rewritePath must be provided - // Exception: stripPrefix can have an empty rewritePath (to just strip the prefix) - if (data.rewritePathType && !data.rewritePath) { - // Allow empty rewritePath for stripPrefix type - if (data.rewritePathType !== "stripPrefix") { - return false; - } - } - return true; - }, - { - error: "Invalid rewrite path configuration" - } - ); +import type { ResourceContextType } from "@app/contexts/resourceContext"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient } from "@app/lib/api"; +import { formatAxiosError } from "@app/lib/api/formatAxiosError"; +import { cn } from "@app/lib/cn"; +import { DockerManager, DockerState } from "@app/lib/docker"; +import { parseHostTarget } from "@app/lib/parseHostTarget"; +import { orgQueries, resourceQueries } from "@app/lib/queries"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { CaretSortIcon } from "@radix-ui/react-icons"; +import { tlsNameSchema } from "@server/lib/schemas"; +import { type GetResourceResponse } from "@server/routers/resource"; +import type { ListSitesResponse } from "@server/routers/site"; +import { CreateTargetResponse } from "@server/routers/target"; +import { ListTargetsResponse } from "@server/routers/target/listTargets"; +import { ArrayElement } from "@server/types/ArrayElement"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable +} from "@tanstack/react-table"; +import { AxiosResponse } from "axios"; +import { + AlertTriangle, + CheckIcon, + CircleCheck, + CircleX, + Info, + Plus, + Settings +} from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { use, useActionState, useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; const targetsSettingsSchema = z.object({ stickySession: z.boolean() @@ -205,23 +124,72 @@ type LocalTarget = Omit< "protocol" >; -export default function ReverseProxyTargets(props: { +export default function ReverseProxyTargetsPage(props: { params: Promise<{ resourceId: number; orgId: string }>; }) { const params = use(props.params); - const t = useTranslations(); - const { env } = useEnvContext(); - const { resource, updateResource } = useResourceContext(); + const { data: remoteTargets = [], isLoading: isLoadingTargets } = useQuery( + resourceQueries.resourceTargets({ + resourceId: resource.resourceId + }) + ); + const { data: sites = [], isLoading: isLoadingSites } = useQuery( + orgQueries.sites({ + orgId: params.orgId + }) + ); + + if (isLoadingSites || isLoadingTargets) { + return null; + } + + return ( + + + + {resource.http && ( + + )} + + {!resource.http && resource.protocol == "tcp" && ( + + )} + + ); +} + +function ProxyResourceTargetsForm({ + sites, + initialTargets, + resource +}: { + initialTargets: LocalTarget[]; + sites: ListSitesResponse["sites"]; + resource: GetResourceResponse; +}) { + const t = useTranslations(); const api = createApiClient(useEnvContext()); - const [targets, setTargets] = useState([]); + const [targets, setTargets] = useState(initialTargets); const [targetsToRemove, setTargetsToRemove] = useState([]); - const [sites, setSites] = useState([]); const [dockerStates, setDockerStates] = useState>( new Map() ); + const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false); + const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = + useState(null); const initializeDockerForSite = async (siteId: number) => { if (dockerStates.has(siteId)) { @@ -258,12 +226,6 @@ export default function ReverseProxyTargets(props: { ); }; - const [httpsTlsLoading, setHttpsTlsLoading] = useState(false); - const [targetsLoading, setTargetsLoading] = useState(false); - const [proxySettingsLoading, setProxySettingsLoading] = useState(false); - - const [pageLoading, setPageLoading] = useState(true); - const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); const [isAdvancedMode, setIsAdvancedMode] = useState(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("proxy-advanced-mode"); @@ -271,572 +233,14 @@ export default function ReverseProxyTargets(props: { } return false; }); - const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false); - const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = - useState(null); - const router = useRouter(); - - const proxySettingsSchema = z.object({ - setHostHeader: z - .string() - .optional() - .refine( - (data) => { - if (data) { - return tlsNameSchema.safeParse(data).success; - } - return true; - }, - { - message: t("proxyErrorInvalidHeader") - } - ), - headers: z - .array(z.object({ name: z.string(), value: z.string() })) - .nullable(), - proxyProtocol: z.boolean().optional(), - proxyProtocolVersion: z.int().min(1).max(2).optional() - }); - - const tlsSettingsSchema = z.object({ - ssl: z.boolean(), - tlsServerName: z - .string() - .optional() - .refine( - (data) => { - if (data) { - return tlsNameSchema.safeParse(data).success; - } - return true; - }, - { - message: t("proxyErrorTls") - } - ) - }); - - type ProxySettingsValues = z.infer; - type TlsSettingsValues = z.infer; - type TargetsSettingsValues = 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 || "", - headers: resource.headers, - proxyProtocol: resource.proxyProtocol || false, - proxyProtocolVersion: resource.proxyProtocolVersion || 1 - } - }); - - const targetsSettingsForm = useForm({ - resolver: zodResolver(targetsSettingsSchema), - defaultValues: { - stickySession: resource.stickySession - } - }); - - useEffect(() => { - const fetchTargets = async () => { - try { - const res = await api.get>( - `/resource/${resource.resourceId}/targets` - ); - - if (res.status === 200) { - setTargets(res.data.data.targets); - } - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("targetErrorFetch"), - description: formatAxiosError( - err, - t("targetErrorFetchDescription") - ) - }); - } finally { - setPageLoading(false); - } - }; - fetchTargets(); - - const fetchSites = async () => { - const res = await api - .get< - AxiosResponse - >(`/org/${params.orgId}/sites`) - .catch((e) => { - toast({ - variant: "destructive", - title: t("sitesErrorFetch"), - description: formatAxiosError( - e, - t("sitesErrorFetchDescription") - ) - }); - }); - - if (res?.status === 200) { - setSites(res.data.data.sites); - - // Initialize Docker for newt sites - const newtSites = res.data.data.sites.filter( - (site) => site.type === "newt" - ); - for (const site of newtSites) { - initializeDockerForSite(site.siteId); - } - - // Sites loaded successfully - } - }; - fetchSites(); - - // const fetchSite = async () => { - // try { - // const res = await api.get>( - // `/site/${resource.siteId}` - // ); - // - // if (res.status === 200) { - // setSite(res.data.data); - // } - // } catch (err) { - // console.error(err); - // toast({ - // variant: "destructive", - // title: t("siteErrorFetch"), - // description: formatAxiosError( - // err, - // t("siteErrorFetchDescription") - // ) - // }); - // } - // }; - // fetchSite(); - }, []); - - // Save advanced mode preference to localStorage - useEffect(() => { - if (typeof window !== "undefined") { - localStorage.setItem( - "proxy-advanced-mode", - isAdvancedMode.toString() - ); - } - }, [isAdvancedMode]); - - function addNewTarget() { - const isHttp = resource.http; - - const newTarget: LocalTarget = { - targetId: -Date.now(), // Use negative timestamp as temporary ID - ip: "", - method: isHttp ? "http" : null, - port: 0, - siteId: sites.length > 0 ? sites[0].siteId : 0, - path: isHttp ? null : null, - pathMatchType: isHttp ? null : null, - rewritePath: isHttp ? null : null, - rewritePathType: isHttp ? null : null, - priority: isHttp ? 100 : 100, - enabled: true, - resourceId: resource.resourceId, - hcEnabled: false, - hcPath: null, - hcMethod: null, - hcInterval: null, - hcTimeout: null, - hcHeaders: null, - hcScheme: null, - hcHostname: null, - hcPort: null, - hcFollowRedirects: null, - hcHealth: "unknown", - hcStatus: null, - hcMode: null, - hcUnhealthyInterval: null, - hcTlsServerName: null, - siteType: sites.length > 0 ? sites[0].type : null, - new: true, - updated: false - }; - - setTargets((prev) => [...prev, newTarget]); - } - - async function saveNewTarget(target: LocalTarget) { - // Validate the target - if (!isTargetValid(target.ip)) { - toast({ - variant: "destructive", - title: t("targetErrorInvalidIp"), - description: t("targetErrorInvalidIpDescription") - }); - return; - } - - if (!target.port || target.port <= 0) { - toast({ - variant: "destructive", - title: t("targetErrorInvalidPort"), - description: t("targetErrorInvalidPortDescription") - }); - return; - } - - if (!target.siteId) { - toast({ - variant: "destructive", - title: t("targetErrorNoSite"), - description: t("targetErrorNoSiteDescription") - }); - return; - } - - try { - setTargetsLoading(true); - - const data: any = { - resourceId: resource.resourceId, - siteId: target.siteId, - ip: target.ip, - method: target.method, - port: target.port, - enabled: target.enabled, - hcEnabled: target.hcEnabled, - hcPath: target.hcPath || null, - hcScheme: target.hcScheme || null, - hcHostname: target.hcHostname || null, - hcPort: target.hcPort || null, - hcInterval: target.hcInterval || null, - hcTimeout: target.hcTimeout || null, - hcHeaders: target.hcHeaders || null, - hcFollowRedirects: target.hcFollowRedirects || null, - hcMethod: target.hcMethod || null, - hcStatus: target.hcStatus || null, - hcUnhealthyInterval: target.hcUnhealthyInterval || null, - hcMode: target.hcMode || null - }; - - // Only include path-related fields for HTTP resources - if (resource.http) { - data.path = target.path; - data.pathMatchType = target.pathMatchType; - data.rewritePath = target.rewritePath; - data.rewritePathType = target.rewritePathType; - data.priority = target.priority; - } - - const response = await api.post< - AxiosResponse - >(`/target`, data); - - if (response.status === 200) { - // Update the target with the new ID and remove the new flag - setTargets((prev) => - prev.map((t) => - t.targetId === target.targetId - ? { - ...t, - targetId: response.data.data.targetId, - new: false, - updated: false - } - : t - ) - ); - - toast({ - title: t("targetCreated"), - description: t("targetCreatedDescription") - }); - } - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("targetErrorCreate"), - description: formatAxiosError( - err, - t("targetErrorCreateDescription") - ) - }); - } finally { - setTargetsLoading(false); - } - } - - async function addTarget(data: z.infer) { - // if (site && site.type == "wireguard" && site.subnet) { - // // make sure that the target IP is within the site subnet - // const targetIp = data.ip; - // const subnet = site.subnet; - // try { - // if (!isIPInSubnet(targetIp, subnet)) { - // toast({ - // variant: "destructive", - // title: t("targetWireGuardErrorInvalidIp"), - // description: t( - // "targetWireGuardErrorInvalidIpDescription" - // ) - // }); - // return; - // } - // } catch (error) { - // console.error(error); - // toast({ - // variant: "destructive", - // title: t("targetWireGuardErrorInvalidIp"), - // description: t("targetWireGuardErrorInvalidIpDescription") - // }); - // return; - // } - // } - - const site = sites.find((site) => site.siteId === data.siteId); - const isHttp = resource.http; - - const newTarget: LocalTarget = { - ...data, - path: isHttp ? data.path || null : null, - pathMatchType: isHttp ? data.pathMatchType || null : null, - rewritePath: isHttp ? data.rewritePath || null : null, - rewritePathType: isHttp ? data.rewritePathType || null : null, - siteType: site?.type || null, - enabled: true, - targetId: new Date().getTime(), - new: true, - resourceId: resource.resourceId, - priority: isHttp ? data.priority || 100 : 100, - hcEnabled: false, - hcPath: null, - hcMethod: null, - hcInterval: null, - hcTimeout: null, - hcHeaders: null, - hcScheme: null, - hcHostname: null, - hcPort: null, - hcFollowRedirects: null, - hcHealth: "unknown", - hcStatus: null, - hcMode: null, - hcUnhealthyInterval: null, - hcTlsServerName: null - }; - - setTargets([...targets, newTarget]); - } - - const removeTarget = (targetId: number) => { - setTargets([ - ...targets.filter((target) => target.targetId !== targetId) - ]); - - if (!targets.find((target) => target.targetId === targetId)?.new) { - setTargetsToRemove([...targetsToRemove, targetId]); - } - }; - - async function updateTarget(targetId: number, data: Partial) { - const site = sites.find((site) => site.siteId === data.siteId); - setTargets( - targets.map((target) => - target.targetId === targetId - ? { - ...target, - ...data, - updated: true, - siteType: site ? site.type : target.siteType - } - : target - ) - ); - } - - function updateTargetHealthCheck(targetId: number, config: any) { - setTargets( - targets.map((target) => - target.targetId === targetId - ? { - ...target, - ...config, - updated: true - } - : target - ) - ); - } - - const openHealthCheckDialog = (target: LocalTarget) => { - console.log(target); - setSelectedTargetForHealthCheck(target); - setHealthCheckDialogOpen(true); - }; - - async function saveAllSettings() { - // Validate that no targets have blank IPs or invalid ports - const targetsWithInvalidFields = targets.filter( - (target) => - !target.ip || - target.ip.trim() === "" || - !target.port || - target.port <= 0 || - isNaN(target.port) - ); - console.log(targetsWithInvalidFields); - if (targetsWithInvalidFields.length > 0) { - toast({ - variant: "destructive", - title: t("targetErrorInvalidIp"), - description: t("targetErrorInvalidIpDescription") - }); - return; - } - - try { - setTargetsLoading(true); - setHttpsTlsLoading(true); - setProxySettingsLoading(true); - - for (const targetId of targetsToRemove) { - await api.delete(`/target/${targetId}`); - } - - // Save targets - for (const target of targets) { - const data: any = { - ip: target.ip, - port: target.port, - method: target.method, - enabled: target.enabled, - siteId: target.siteId, - hcEnabled: target.hcEnabled, - hcPath: target.hcPath || null, - hcScheme: target.hcScheme || null, - hcHostname: target.hcHostname || null, - hcPort: target.hcPort || null, - hcInterval: target.hcInterval || null, - hcTimeout: target.hcTimeout || null, - hcHeaders: target.hcHeaders || null, - hcFollowRedirects: target.hcFollowRedirects || null, - hcMethod: target.hcMethod || null, - hcStatus: target.hcStatus || null, - hcUnhealthyInterval: target.hcUnhealthyInterval || null, - hcMode: target.hcMode || null, - hcTlsServerName: target.hcTlsServerName - }; - - // Only include path-related fields for HTTP resources - if (resource.http) { - data.path = target.path; - data.pathMatchType = target.pathMatchType; - data.rewritePath = target.rewritePath; - data.rewritePathType = target.rewritePathType; - data.priority = target.priority; - } - - if (target.new) { - const res = await api.put< - AxiosResponse - >(`/resource/${resource.resourceId}/target`, data); - target.targetId = res.data.data.targetId; - target.new = false; - } else if (target.updated) { - await api.post(`/target/${target.targetId}`, data); - target.updated = false; - } - } - - if (resource.http) { - // Gather all settings - const stickySessionData = targetsSettingsForm.getValues(); - const tlsData = tlsSettingsForm.getValues(); - const proxyData = proxySettingsForm.getValues(); - - // Combine into one payload - const payload = { - stickySession: stickySessionData.stickySession, - ssl: tlsData.ssl, - tlsServerName: tlsData.tlsServerName || null, - setHostHeader: proxyData.setHostHeader || null, - headers: proxyData.headers || null - }; - - // Single API call to update all settings - await api.post(`/resource/${resource.resourceId}`, payload); - - // Update local resource context - updateResource({ - ...resource, - stickySession: stickySessionData.stickySession, - ssl: tlsData.ssl, - tlsServerName: tlsData.tlsServerName || null, - setHostHeader: proxyData.setHostHeader || null, - headers: proxyData.headers || null - }); - } else { - // For TCP/UDP resources, save proxy protocol settings - const proxyData = proxySettingsForm.getValues(); - - const payload = { - proxyProtocol: proxyData.proxyProtocol || false, - proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 - }; - - await api.post(`/resource/${resource.resourceId}`, payload); - - updateResource({ - ...resource, - proxyProtocol: proxyData.proxyProtocol || false, - proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 - }); - } - - toast({ - title: t("settingsUpdated"), - description: t("settingsUpdatedDescription") - }); - - setTargetsToRemove([]); - router.refresh(); - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("settingsErrorUpdate"), - description: formatAxiosError( - err, - t("settingsErrorUpdateDescription") - ) - }); - } finally { - setTargetsLoading(false); - setHttpsTlsLoading(false); - setProxySettingsLoading(false); - } - } const getColumns = (): ColumnDef[] => { - const baseColumns: ColumnDef[] = []; const isHttp = resource.http; const priorityColumn: ColumnDef = { id: "priority", header: () => ( -
+
{t("priority")} @@ -932,9 +336,9 @@ export default function ReverseProxyTargets(props: { } >
- + {getStatusText(status)}
@@ -1363,6 +767,91 @@ export default function ReverseProxyTargets(props: { } }; + function addNewTarget() { + const isHttp = resource.http; + + const newTarget: LocalTarget = { + targetId: -Date.now(), // Use negative timestamp as temporary ID + ip: "", + method: isHttp ? "http" : null, + port: 0, + siteId: sites.length > 0 ? sites[0].siteId : 0, + path: isHttp ? null : null, + pathMatchType: isHttp ? null : null, + rewritePath: isHttp ? null : null, + rewritePathType: isHttp ? null : null, + priority: isHttp ? 100 : 100, + enabled: true, + resourceId: resource.resourceId, + hcEnabled: false, + hcPath: null, + hcMethod: null, + hcInterval: null, + hcTimeout: null, + hcHeaders: null, + hcScheme: null, + hcHostname: null, + hcPort: null, + hcFollowRedirects: null, + hcHealth: "unknown", + hcStatus: null, + hcMode: null, + hcUnhealthyInterval: null, + hcTlsServerName: null, + siteType: sites.length > 0 ? sites[0].type : null, + new: true, + updated: false + }; + + setTargets((prev) => [...prev, newTarget]); + } + + const removeTarget = (targetId: number) => { + setTargets([ + ...targets.filter((target) => target.targetId !== targetId) + ]); + + if (!targets.find((target) => target.targetId === targetId)?.new) { + setTargetsToRemove([...targetsToRemove, targetId]); + } + }; + + async function updateTarget(targetId: number, data: Partial) { + const site = sites.find((site) => site.siteId === data.siteId); + setTargets( + targets.map((target) => + target.targetId === targetId + ? { + ...target, + ...data, + updated: true, + siteType: site ? site.type : target.siteType + } + : target + ) + ); + } + + function updateTargetHealthCheck(targetId: number, config: any) { + setTargets( + targets.map((target) => + target.targetId === targetId + ? { + ...target, + ...config, + updated: true + } + : target + ) + ); + } + + const openHealthCheckDialog = (target: LocalTarget) => { + console.log(target); + setSelectedTargetForHealthCheck(target); + setHealthCheckDialogOpen(true); + }; + const columns = getColumns(); const table = useReactTable({ @@ -1380,12 +869,128 @@ export default function ReverseProxyTargets(props: { } }); - if (pageLoading) { - return <>; + const router = useRouter(); + + const queryClient = useQueryClient(); + + useEffect(() => { + const newtSites = sites.filter((site) => site.type === "newt"); + for (const site of newtSites) { + initializeDockerForSite(site.siteId); + } + }, [sites]); + + // Save advanced mode preference to localStorage + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem( + "proxy-advanced-mode", + isAdvancedMode.toString() + ); + } + }, [isAdvancedMode]); + + const [, formAction, isSubmitting] = useActionState(saveTargets, null); + + async function saveTargets() { + // Validate that no targets have blank IPs or invalid ports + const targetsWithInvalidFields = targets.filter( + (target) => + !target.ip || + target.ip.trim() === "" || + !target.port || + target.port <= 0 || + isNaN(target.port) + ); + console.log(targetsWithInvalidFields); + if (targetsWithInvalidFields.length > 0) { + toast({ + variant: "destructive", + title: t("targetErrorInvalidIp"), + description: t("targetErrorInvalidIpDescription") + }); + return; + } + + try { + await Promise.all( + targetsToRemove.map((targetId) => + api.delete(`/target/${targetId}`) + ) + ); + + // Save targets + for (const target of targets) { + const data: any = { + ip: target.ip, + port: target.port, + method: target.method, + enabled: target.enabled, + siteId: target.siteId, + hcEnabled: target.hcEnabled, + hcPath: target.hcPath || null, + hcScheme: target.hcScheme || null, + hcHostname: target.hcHostname || null, + hcPort: target.hcPort || null, + hcInterval: target.hcInterval || null, + hcTimeout: target.hcTimeout || null, + hcHeaders: target.hcHeaders || null, + hcFollowRedirects: target.hcFollowRedirects || null, + hcMethod: target.hcMethod || null, + hcStatus: target.hcStatus || null, + hcUnhealthyInterval: target.hcUnhealthyInterval || null, + hcMode: target.hcMode || null, + hcTlsServerName: target.hcTlsServerName + }; + + // Only include path-related fields for HTTP resources + if (resource.http) { + data.path = target.path; + data.pathMatchType = target.pathMatchType; + data.rewritePath = target.rewritePath; + data.rewritePathType = target.rewritePathType; + data.priority = target.priority; + } + + if (target.new) { + const res = await api.put< + AxiosResponse + >(`/resource/${resource.resourceId}/target`, data); + target.targetId = res.data.data.targetId; + target.new = false; + } else if (target.updated) { + await api.post(`/target/${target.targetId}`, data); + target.updated = false; + } + } + + toast({ + title: t("settingsUpdated"), + description: t("settingsUpdatedDescription") + }); + + setTargetsToRemove([]); + router.refresh(); + await queryClient.invalidateQueries( + resourceQueries.resourceTargets({ + resourceId: resource.resourceId + }) + ); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("settingsErrorUpdate"), + description: formatAxiosError( + err, + t("settingsErrorUpdateDescription") + ) + }); + } } return ( - + <> {t("targets")} @@ -1514,7 +1119,7 @@ export default function ReverseProxyTargets(props: {
) : ( -
+

{t("targetNoOne")}

@@ -1525,330 +1130,18 @@ export default function ReverseProxyTargets(props: {
)} + +
+ +
- {resource.http && ( - - - - {t("proxyAdditional")} - - - {t("proxyAdditionalDescription")} - - - - -
- - {!env.flags.usePangolinDns && ( - ( - - - { - field.onChange( - val - ); - }} - /> - - - )} - /> - )} - ( - - - {t("targetTlsSni")} - - - - - - {t( - "targetTlsSniDescription" - )} - - - - )} - /> - - -
- - -
- - ( - - - { - field.onChange(val); - }} - /> - - - )} - /> - - -
- - -
- - ( - - - {t("proxyCustomHeader")} - - - - - - {t( - "proxyCustomHeaderDescription" - )} - - - - )} - /> - ( - - - {t("customHeaders")} - - - { - field.onChange( - value - ); - }} - rows={4} - /> - - - {t( - "customHeadersDescription" - )} - - - - )} - /> - - -
-
-
- )} - - {!resource.http && resource.protocol == "tcp" && ( - - - - {t("proxyProtocol")} - - - {t("proxyProtocolDescription")} - - - - -
- - ( - - - { - field.onChange(val); - }} - /> - - - )} - /> - - {proxySettingsForm.watch( - "proxyProtocol" - ) && ( - <> - ( - - - {t( - "proxyProtocolVersion" - )} - - - - - - {t( - "versionDescription" - )} - - - )} - /> - - - - - - {t("warning")}: - {" "} - {t("proxyProtocolWarning")} - - - - )} - - -
-
-
- )} - -
- -
- {selectedTargetForHealthCheck && ( )} - + ); } -function isIPInSubnet(subnet: string, ip: string): boolean { - const [subnetIP, maskBits] = subnet.split("/"); - const mask = parseInt(maskBits); +function ProxyResourceHttpForm({ + resource, + updateResource +}: Pick) { + const t = useTranslations(); - if (mask < 0 || mask > 32) { - throw new Error("subnetMaskErrorInvalid"); - } + const tlsSettingsSchema = z.object({ + ssl: z.boolean(), + tlsServerName: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: t("proxyErrorTls") + } + ) + }); - // Convert IP addresses to binary numbers - const subnetNum = ipToNumber(subnetIP); - const ipNum = ipToNumber(ip); - - // Calculate subnet mask - const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1); - - // Check if the IP is in the subnet - return (subnetNum & maskNum) === (ipNum & maskNum); -} - -function ipToNumber(ip: string): number { - // Validate IP address format - const parts = ip.split("."); - - if (parts.length !== 4) { - throw new Error("ipAddressErrorInvalidFormat"); - } - - // Convert IP octets to 32-bit number - return parts.reduce((num, octet) => { - const oct = parseInt(octet); - if (isNaN(oct) || oct < 0 || oct > 255) { - throw new Error("ipAddressErrorInvalidOctet"); + const tlsSettingsForm = useForm({ + resolver: zodResolver(tlsSettingsSchema), + defaultValues: { + ssl: resource.ssl, + tlsServerName: resource.tlsServerName || "" } - return (num << 8) + oct; - }, 0); + }); + + const proxySettingsSchema = z.object({ + setHostHeader: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: t("proxyErrorInvalidHeader") + } + ), + headers: z + .array(z.object({ name: z.string(), value: z.string() })) + .nullable(), + proxyProtocol: z.boolean().optional(), + proxyProtocolVersion: z.int().min(1).max(2).optional() + }); + + const proxySettingsForm = useForm({ + resolver: zodResolver(proxySettingsSchema), + defaultValues: { + setHostHeader: resource.setHostHeader || "", + headers: resource.headers, + proxyProtocol: resource.proxyProtocol || false, + proxyProtocolVersion: resource.proxyProtocolVersion || 1 + } + }); + + const { env } = useEnvContext(); + const api = createApiClient({ env }); + + const targetsSettingsForm = useForm({ + resolver: zodResolver(targetsSettingsSchema), + defaultValues: { + stickySession: resource.stickySession + } + }); + + const router = useRouter(); + const [, formAction, isSubmitting] = useActionState( + saveResourceHttpSettings, + null + ); + + async function saveResourceHttpSettings() { + const isValidTLS = await tlsSettingsForm.trigger(); + const isValidProxy = await proxySettingsForm.trigger(); + const targetSettingsForm = await targetsSettingsForm.trigger(); + if (!isValidTLS || !isValidProxy || !targetSettingsForm) return; + + try { + // Gather all settings + const stickySessionData = targetsSettingsForm.getValues(); + const tlsData = tlsSettingsForm.getValues(); + const proxyData = proxySettingsForm.getValues(); + + // Combine into one payload + const payload = { + stickySession: stickySessionData.stickySession, + ssl: tlsData.ssl, + tlsServerName: tlsData.tlsServerName || null, + setHostHeader: proxyData.setHostHeader || null, + headers: proxyData.headers || null + }; + + // Single API call to update all settings + await api.post(`/resource/${resource.resourceId}`, payload); + + // Update local resource context + updateResource({ + ...resource, + stickySession: stickySessionData.stickySession, + ssl: tlsData.ssl, + tlsServerName: tlsData.tlsServerName || null, + setHostHeader: proxyData.setHostHeader || null, + headers: proxyData.headers || null + }); + + toast({ + title: t("settingsUpdated"), + description: t("settingsUpdatedDescription") + }); + + router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("settingsErrorUpdate"), + description: formatAxiosError( + err, + t("settingsErrorUpdateDescription") + ) + }); + } + } + + return ( + + + + {t("proxyAdditional")} + + + {t("proxyAdditionalDescription")} + + + + +
+ + {!env.flags.usePangolinDns && ( + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + )} + ( + + + {t("targetTlsSni")} + + + + + + {t("targetTlsSniDescription")} + + + + )} + /> + + +
+ + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + + +
+ + +
+ + ( + + + {t("proxyCustomHeader")} + + + + + + {t("proxyCustomHeaderDescription")} + + + + )} + /> + ( + + + {t("customHeaders")} + + + { + field.onChange(value); + }} + rows={4} + /> + + + {t("customHeadersDescription")} + + + + )} + /> + + +
+
+ +
+
+
+ ); +} + +function ProxyResourceProtocolForm({ + resource, + updateResource +}: Pick) { + const t = useTranslations(); + + const api = createApiClient(useEnvContext()); + + const proxySettingsSchema = z.object({ + setHostHeader: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: t("proxyErrorInvalidHeader") + } + ), + headers: z + .array(z.object({ name: z.string(), value: z.string() })) + .nullable(), + proxyProtocol: z.boolean().optional(), + proxyProtocolVersion: z.int().min(1).max(2).optional() + }); + + const proxySettingsForm = useForm({ + resolver: zodResolver(proxySettingsSchema), + defaultValues: { + setHostHeader: resource.setHostHeader || "", + headers: resource.headers, + proxyProtocol: resource.proxyProtocol || false, + proxyProtocolVersion: resource.proxyProtocolVersion || 1 + } + }); + + const router = useRouter(); + + const [, formAction, isSubmitting] = useActionState( + saveProtocolSettings, + null + ); + + async function saveProtocolSettings() { + const isValid = proxySettingsForm.trigger(); + if (!isValid) return; + + try { + // For TCP/UDP resources, save proxy protocol settings + const proxyData = proxySettingsForm.getValues(); + + const payload = { + proxyProtocol: proxyData.proxyProtocol || false, + proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 + }; + + await api.post(`/resource/${resource.resourceId}`, payload); + + updateResource({ + ...resource, + proxyProtocol: proxyData.proxyProtocol || false, + proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 + }); + + toast({ + title: t("settingsUpdated"), + description: t("settingsUpdatedDescription") + }); + + router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("settingsErrorUpdate"), + description: formatAxiosError( + err, + t("settingsErrorUpdateDescription") + ) + }); + } + } + + return ( + + + + {t("proxyProtocol")} + + + {t("proxyProtocolDescription")} + + + + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + + {proxySettingsForm.watch("proxyProtocol") && ( + <> + ( + + + {t("proxyProtocolVersion")} + + + + + + {t("versionDescription")} + + + )} + /> + + + + + {t("warning")}:{" "} + {t("proxyProtocolWarning")} + + + + )} + + +
+
+ +
+
+
+ ); } diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx index 78a1b896..003b7f0e 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx @@ -53,7 +53,8 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionFooter + SettingsSectionFooter, + SettingsSectionForm } from "@app/components/Settings"; import { ListResourceRulesResponse } from "@server/routers/resource/listResourceRules"; import { SwitchInput } from "@app/components/SwitchInput"; @@ -74,6 +75,7 @@ import { Switch } from "@app/components/ui/switch"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { COUNTRIES } from "@server/db/countries"; +import { MAJOR_ASNS } from "@server/db/asns"; import { Command, CommandEmpty, @@ -116,11 +118,15 @@ export default function ResourceRules(props: { const [countrySelectValue, setCountrySelectValue] = useState(""); const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); + const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = + useState(false); const router = useRouter(); const t = useTranslations(); const { env } = useEnvContext(); const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0; + const isMaxmindAsnAvailable = + env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0; const RuleAction = { ACCEPT: t("alwaysAllow"), @@ -132,7 +138,8 @@ export default function ResourceRules(props: { PATH: t("path"), IP: "IP", CIDR: t("ipAddressRange"), - COUNTRY: t("country") + COUNTRY: t("country"), + ASN: "ASN" } as const; const addRuleForm = useForm({ @@ -171,6 +178,30 @@ export default function ResourceRules(props: { }, []); async function addRule(data: z.infer) { + // Normalize ASN value + if (data.match === "ASN") { + const originalValue = data.value.toUpperCase(); + + // Handle special "ALL" case + if (originalValue === "ALL" || originalValue === "AS0") { + data.value = "ALL"; + } else { + // Remove AS prefix if present + const normalized = originalValue.replace(/^AS/, ""); + if (!/^\d+$/.test(normalized)) { + toast({ + variant: "destructive", + title: "Invalid ASN", + description: + "ASN must be a number, optionally prefixed with 'AS' (e.g., AS15169 or 15169), or 'ALL'" + }); + return; + } + // Add "AS" prefix for consistent storage + data.value = "AS" + normalized; + } + } + const isDuplicate = rules.some( (rule) => rule.action === data.action && @@ -279,6 +310,8 @@ export default function ResourceRules(props: { return t("rulesMatchUrl"); case "COUNTRY": return t("rulesMatchCountry"); + case "ASN": + return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; } } @@ -504,12 +537,12 @@ export default function ResourceRules(props: { ) @@ -591,6 +629,93 @@ export default function ResourceRules(props: { + ) : row.original.match === "ASN" ? ( + + + + + + + + + + No ASN found. Enter a custom ASN below. + + + {MAJOR_ASNS.map((asn) => ( + { + updateRule( + row.original.ruleId, + { value: asn.code } + ); + }} + > + + {asn.name} ({asn.code}) + + ))} + + + +
+ + asn.code === row.original.value + ) + ? row.original.value + : "" + } + onKeyDown={(e) => { + if (e.key === "Enter") { + const value = e.currentTarget.value + .toUpperCase() + .replace(/^AS/, ""); + if (/^\d+$/.test(value)) { + updateRule( + row.original.ruleId, + { value: "AS" + value } + ); + } + } + }} + className="text-sm" + /> +
+
+
) : ( )} + {isMaxmindAsnAvailable && ( + + { + RuleMatch.ASN + } + + )} @@ -923,6 +1055,115 @@ export default function ResourceRules(props: { + ) : addRuleForm.watch( + "match" + ) === "ASN" ? ( + + + + + + + + + + No ASN found. Use the custom input below. + + + {MAJOR_ASNS.map( + ( + asn + ) => ( + { + field.onChange( + asn.code + ); + setOpenAddRuleAsnSelect( + false + ); + }} + > + + { + asn.name + }{" "} + ( + { + asn.code + } + ) + + ) + )} + + + +
+ { + if (e.key === "Enter") { + const value = e.currentTarget.value + .toUpperCase() + .replace(/^AS/, ""); + if (/^\d+$/.test(value)) { + field.onChange("AS" + value); + setOpenAddRuleAsnSelect(false); + } + } + }} + className="text-sm" + /> +
+
+
) : ( )} @@ -1018,17 +1259,16 @@ export default function ResourceRules(props: {
+ + + - -
- -
); } diff --git a/src/app/[orgId]/settings/resources/proxy/create/page.tsx b/src/app/[orgId]/settings/resources/proxy/create/page.tsx index 04f5ad04..1d6212bf 100644 --- a/src/app/[orgId]/settings/resources/proxy/create/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/create/page.tsx @@ -1312,6 +1312,35 @@ export default function Page() { + {resourceTypes.length > 1 && ( + <> +
+ + {t("type")} + +
+ + { + baseForm.setValue( + "http", + value === "http" + ); + // Update method default when switching resource type + addTargetForm.setValue( + "method", + value === "http" + ? "http" + : null + ); + }} + cols={2} + /> + + )} +
- - {resourceTypes.length > 1 && ( - <> -
- - {t("type")} - -
- - { - baseForm.setValue( - "http", - value === "http" - ); - // Update method default when switching resource type - addTargetForm.setValue( - "method", - value === "http" - ? "http" - : null - ); - }} - cols={2} - /> - - )}
@@ -1396,6 +1396,8 @@ export default function Page() { { + if (!res) return; + httpForm.setValue( "subdomain", res.subdomain @@ -1682,7 +1684,7 @@ export default function Page() {
) : ( -
+

{t("targetNoOne")}

@@ -1848,7 +1850,7 @@ export default function Page() { diff --git a/src/app/[orgId]/settings/resources/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/page.tsx index a59f790f..408a9352 100644 --- a/src/app/[orgId]/settings/resources/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/page.tsx @@ -1,6 +1,7 @@ import type { ResourceRow } from "@app/components/ProxyResourcesTable"; import ProxyResourcesTable from "@app/components/ProxyResourcesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import ProxyResourcesBanner from "@app/components/ProxyResourcesBanner"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import OrgProvider from "@app/providers/OrgProvider"; @@ -97,6 +98,8 @@ export default async function ProxyResourcesPage( description={t("proxyResourceDescription")} /> + + - + @@ -300,7 +300,7 @@ export default function CredentialsPage() { - + {!loadingDefaults && ( diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index 1379fc76..d536e78e 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -24,7 +24,8 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionForm + SettingsSectionForm, + SettingsSectionFooter } from "@app/components/Settings"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; @@ -210,18 +211,17 @@ export default function GeneralPage() { + + + - -
- -
); } diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 6395c265..744a32e7 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -674,6 +674,26 @@ WantedBy=default.target` + {tunnelTypes.length > 1 && ( + <> +
+ + {t("type")} + +
+ { + form.setValue("method", value); + }} + cols={3} + /> + + )} +
{ @@ -748,26 +768,6 @@ WantedBy=default.target` )}
- - {tunnelTypes.length > 1 && ( - <> -
- - {t("type")} - -
- { - form.setValue("method", value); - }} - cols={3} - /> - - )}
@@ -820,18 +820,6 @@ WantedBy=default.target`
- - - - {t("siteCredentialsSave")} - - - {t( - "siteCredentialsSaveDescription" - )} - - - {/*
*/} {/*
- - - - {t("siteCredentialsSave")} - - - {t( - "siteCredentialsSaveDescription" - )} - - )} diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 4c1798bc..132f0c05 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -4,6 +4,7 @@ import { ListSitesResponse } from "@server/routers/site"; import { AxiosResponse } from "axios"; import SitesTable, { SiteRow } from "../../../../components/SitesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import SitesBanner from "@app/components/SitesBanner"; import SitesSplashCard from "../../../../components/SitesSplashCard"; import { getTranslations } from "next-intl/server"; @@ -53,7 +54,8 @@ export default async function SitesPage(props: SitesPageProps) { newtVersion: site.newtVersion || undefined, newtUpdateAvailable: site.newtUpdateAvailable || false, exitNodeName: site.exitNodeName || undefined, - exitNodeEndpoint: site.exitNodeEndpoint || undefined + exitNodeEndpoint: site.exitNodeEndpoint || undefined, + remoteExitNodeId: (site as any).remoteExitNodeId || undefined }; }); @@ -66,6 +68,8 @@ export default async function SitesPage(props: SitesPageProps) { description={t("siteDescription")} /> + + ); diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index d3d9cb2e..9634a3de 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -3,7 +3,7 @@ import { GetIdpResponse } from "@server/routers/idp"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; @@ -28,7 +28,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { redirect("/admin/idp"); } - const navItems: HorizontalTabs = [ + const navItems: TabItem[] = [ { title: t("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 dbcd5f00..acb2f358 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -208,27 +208,23 @@ export default function Page() { - - - - - - {t("idpType")} - - - {t("idpTypeDescription")} - - - - { - form.setValue("type", value as "oidc"); - }} - cols={3} - /> + {/*
*/} + {/*
*/} + {/* */} + {/* {t("idpType")} */} + {/* */} + {/*
*/} + {/* */} + {/* { */} + {/* form.setValue("type", value as "oidc"); */} + {/* }} */} + {/* cols={3} */} + {/* /> */} + {/*
*/}
diff --git a/src/app/admin/license/page.tsx b/src/app/admin/license/page.tsx index ac6d3e67..4e0586bd 100644 --- a/src/app/admin/license/page.tsx +++ b/src/app/admin/license/page.tsx @@ -315,7 +315,7 @@ export default function LicensePage() { setSelectedLicenseKey(null); }} dialog={ -
+

{t("licenseQuestionRemove")}

{t("licenseMessageRemove")} @@ -360,7 +360,8 @@ export default function LicensePage() {

- {t("licensed")} + {t("licensed") + + `${licenseStatus?.tier === "personal" ? ` (${t("personalUseOnly")})` : ""}`}
) : ( diff --git a/src/app/admin/users/AdminUsersTable.tsx b/src/app/admin/users/AdminUsersTable.tsx index efcf9484..1c7d1b7f 100644 --- a/src/app/admin/users/AdminUsersTable.tsx +++ b/src/app/admin/users/AdminUsersTable.tsx @@ -243,7 +243,7 @@ export default function UsersTable({ users }: Props) { setSelected(null); }} dialog={ -
+

{t("userQuestionRemove")}

{t("userMessageRemove")}

diff --git a/src/app/auth/(private)/org/page.tsx b/src/app/auth/(private)/org/page.tsx deleted file mode 100644 index 5b68708a..00000000 --- a/src/app/auth/(private)/org/page.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { formatAxiosError, priv } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { cache } from "react"; -import { verifySession } from "@app/lib/auth/verifySession"; -import { redirect } from "next/navigation"; -import { pullEnv } from "@app/lib/pullEnv"; -import { LoginFormIDP } from "@app/components/LoginForm"; -import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; -import { build } from "@server/build"; -import { headers } from "next/headers"; -import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; -import IdpLoginButtons from "@app/components/private/IdpLoginButtons"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from "@app/components/ui/card"; -import { Button } from "@app/components/ui/button"; -import Link from "next/link"; -import { getTranslations } from "next-intl/server"; -import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types"; -import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken"; -import { GetOrgTierResponse } from "@server/routers/billing/types"; -import { TierId } from "@server/lib/billing/tiers"; - -export const dynamic = "force-dynamic"; - -export default async function OrgAuthPage(props: { - params: Promise<{}>; - searchParams: Promise<{ token?: string }>; -}) { - const params = await props.params; - const searchParams = await props.searchParams; - - const env = pullEnv(); - - const authHeader = await authCookieHeader(); - - if (searchParams.token) { - return ; - } - - const getUser = cache(verifySession); - const user = await getUser({ skipCheckVerifyEmail: true }); - - const allHeaders = await headers(); - const host = allHeaders.get("host"); - - const t = await getTranslations(); - - const expectedHost = env.app.dashboardUrl.split("//")[1]; - - let redirectToUrl: string | undefined; - let loginPage: LoadLoginPageResponse | undefined; - if (host !== expectedHost) { - try { - const res = await priv.get>( - `/login-page?fullDomain=${host}` - ); - - if (res && res.status === 200) { - loginPage = res.data.data; - } - } catch (e) {} - - if (!loginPage) { - console.debug( - `No login page found for host ${host}, redirecting to dashboard` - ); - redirect(env.app.dashboardUrl); - } - - let subscriptionStatus: GetOrgTierResponse | null = null; - if (build === "saas") { - try { - const getSubscription = cache(() => - priv.get>( - `/org/${loginPage!.orgId}/billing/tier` - ) - ); - const subRes = await getSubscription(); - subscriptionStatus = subRes.data.data; - } catch {} - } - const subscribed = - build === "enterprise" - ? true - : subscriptionStatus?.tier === TierId.STANDARD; - - if (build === "saas" && !subscribed) { - console.log( - `Org ${loginPage.orgId} is not subscribed, redirecting to dashboard` - ); - redirect(env.app.dashboardUrl); - } - - if (user) { - let redirectToken: string | undefined; - try { - const res = await priv.post< - AxiosResponse - >(`/get-session-transfer-token`, {}, authHeader); - - if (res && res.status === 200) { - const newToken = res.data.data.token; - redirectToken = newToken; - } - } catch (e) { - console.error( - formatAxiosError(e, "Failed to get transfer token") - ); - } - - if (redirectToken) { - redirectToUrl = `${env.app.dashboardUrl}/auth/org?token=${redirectToken}`; - redirect(redirectToUrl); - } - } - } else { - console.log(`Host ${host} is the same`); - redirect(env.app.dashboardUrl); - } - - let loginIdps: LoginFormIDP[] = []; - if (build === "saas") { - const idpsRes = await cache( - async () => - await priv.get>( - `/org/${loginPage!.orgId}/idp` - ) - )(); - loginIdps = idpsRes.data.data.idps.map((idp) => ({ - idpId: idp.idpId, - name: idp.name, - variant: idp.variant - })) as LoginFormIDP[]; - } - - return ( -
-
- - {t("poweredBy")}{" "} - - {env.branding.appName || "Pangolin"} - - -
- - - {t("orgAuthSignInTitle")} - - {loginIdps.length > 0 - ? t("orgAuthChooseIdpDescription") - : ""} - - - - {loginIdps.length > 0 ? ( - - ) : ( -
-

- {t("orgAuthNoIdpConfigured")} -

- - - -
- )} -
-
-
- ); -} diff --git a/src/app/auth/2fa/setup/page.tsx b/src/app/auth/2fa/setup/page.tsx index 944731b9..c74628cc 100644 --- a/src/app/auth/2fa/setup/page.tsx +++ b/src/app/auth/2fa/setup/page.tsx @@ -32,7 +32,6 @@ export default function Setup2FAPage() { console.log("2FA setup complete", redirect, email); if (redirect) { const cleanUrl = cleanRedirect(redirect); - console.log("Redirecting to:", cleanUrl); router.push(cleanUrl); } else { router.push("/"); diff --git a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx index 3d646084..7b3ccabf 100644 --- a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx @@ -14,8 +14,11 @@ export const dynamic = "force-dynamic"; export default async function Page(props: { params: Promise<{ orgId: string; idpId: string }>; searchParams: Promise<{ - code: string; - state: string; + code?: string; + state?: string; + error?: string; + error_description?: string; + error_uri?: string; }>; }) { const params = await props.params; @@ -61,6 +64,14 @@ export default async function Page(props: { } } + const providerError = searchParams.error + ? { + error: searchParams.error, + description: searchParams.error_description, + uri: searchParams.error_uri + } + : undefined; + return ( <> ); diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index 70439824..6a72006b 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -23,6 +23,7 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { const t = await getTranslations(); let hideFooter = false; + let licenseStatus: GetLicenseStatusResponse | null = null; if (build == "enterprise") { const licenseStatusRes = await cache( async () => @@ -30,10 +31,12 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { "/license/status" ) )(); + licenseStatus = licenseStatusRes.data.data; if ( env.branding.hideAuthLayoutFooter && licenseStatusRes.data.data.isHostLicensed && - licenseStatusRes.data.data.isLicenseValid + licenseStatusRes.data.data.isLicenseValid && + licenseStatusRes.data.data.tier !== "personal" ) { hideFooter = true; } @@ -83,6 +86,23 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { ? t("enterpriseEdition") : t("pangolinCloud")} + {build === "enterprise" && + licenseStatus?.isHostLicensed && + licenseStatus?.isLicenseValid && + licenseStatus?.tier === "personal" ? ( + <> + + {t("personalUseOnly")} + + ) : null} + {build === "enterprise" && + (!licenseStatus?.isHostLicensed || + !licenseStatus?.isLicenseValid) ? ( + <> + + {t("unlicensed")} + + ) : null} {build === "saas" && ( <> diff --git a/src/app/auth/login/device/success/page.tsx b/src/app/auth/login/device/success/page.tsx index 81e62fd6..f725a867 100644 --- a/src/app/auth/login/device/success/page.tsx +++ b/src/app/auth/login/device/success/page.tsx @@ -6,6 +6,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { CheckCircle2 } from "lucide-react"; import { useTranslations } from "next-intl"; +import Link from "next/link"; export default function DeviceAuthSuccessPage() { const { env } = useEnvContext(); @@ -20,30 +21,38 @@ export default function DeviceAuthSuccessPage() { : 58; return ( - - -
- -
-
-

- {t("deviceActivation")} -

-
-
- -
- -
-

- {t("deviceConnected")} -

-

- {t("deviceAuthorizedMessage")} + <> + + +

+ +
+
+

+ {t("deviceActivation")}

-
- - + + +
+ +
+

+ {t("deviceConnected")} +

+

+ {t("deviceAuthorizedMessage")} +

+
+
+
+ + +

+ + {t("backToHome")} + +

+ ); } diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index f632a88c..bd6327fd 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -66,6 +66,7 @@ export default async function Page(props: { let redirectUrl: string | undefined = undefined; if (searchParams.redirect) { redirectUrl = cleanRedirect(searchParams.redirect as string); + searchParams.redirect = redirectUrl; } let loginIdps: LoginFormIDP[] = []; @@ -119,6 +120,35 @@ export default async function Page(props: {

)} + + {!isInvite && build === "saas" ? ( +
+ {t("needToSignInToOrg")} + + {t("orgAuthSignInToOrg")} + +
+ ) : null} ); } + +function buildQueryString(searchParams: { + [key: string]: string | string[] | undefined; +}): string { + const params = new URLSearchParams(); + const redirect = searchParams.redirect; + const forceLogin = searchParams.forceLogin; + + if (redirect && typeof redirect === "string") { + params.set("redirect", redirect); + } + if (forceLogin && typeof forceLogin === "string") { + params.set("forceLogin", forceLogin); + } + const queryString = params.toString(); + return queryString ? `?${queryString}` : ""; +} diff --git a/src/app/auth/org/[orgId]/page.tsx b/src/app/auth/org/[orgId]/page.tsx new file mode 100644 index 00000000..1958a388 --- /dev/null +++ b/src/app/auth/org/[orgId]/page.tsx @@ -0,0 +1,85 @@ +import { priv } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { cache } from "react"; +import { verifySession } from "@app/lib/auth/verifySession"; +import { LoginFormIDP } from "@app/components/LoginForm"; +import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; +import { build } from "@server/build"; +import { + LoadLoginPageBrandingResponse, + LoadLoginPageResponse +} from "@server/routers/loginPage/types"; +import { redirect } from "next/navigation"; +import OrgLoginPage from "@app/components/OrgLoginPage"; + +export const dynamic = "force-dynamic"; + +export default async function OrgAuthPage(props: { + params: Promise<{ orgId: string }>; + searchParams: Promise<{ forceLogin?: string; redirect?: string }>; +}) { + const searchParams = await props.searchParams; + const params = await props.params; + + if (build !== "saas") { + const queryString = new URLSearchParams(searchParams as any).toString(); + redirect(`/auth/login${queryString ? `?${queryString}` : ""}`); + } + + const forceLoginParam = searchParams?.forceLogin; + const forceLogin = forceLoginParam === "true"; + const orgId = params.orgId; + + const getUser = cache(verifySession); + const user = await getUser({ skipCheckVerifyEmail: true }); + + if (user && !forceLogin) { + redirect("/"); + } + + let loginPage: LoadLoginPageResponse | undefined; + + try { + const res = await priv.get>( + `/login-page?orgId=${orgId}` + ); + + if (res && res.status === 200) { + loginPage = res.data.data; + } + } catch (e) {} + + let loginIdps: LoginFormIDP[] = []; + if (build === "saas") { + const idpsRes = await priv.get>( + `/org/${orgId}/idp` + ); + + loginIdps = idpsRes.data.data.idps.map((idp) => ({ + idpId: idp.idpId, + name: idp.name, + variant: idp.variant + })) as LoginFormIDP[]; + } + + let branding: LoadLoginPageBrandingResponse | null = null; + if (build === "saas") { + try { + const res = await priv.get< + AxiosResponse + >(`/login-page-branding?orgId=${orgId}`); + if (res.status === 200) { + branding = res.data.data; + } + } catch (error) {} + } + + return ( + + ); +} diff --git a/src/app/auth/org/page.tsx b/src/app/auth/org/page.tsx new file mode 100644 index 00000000..8fc9bbb7 --- /dev/null +++ b/src/app/auth/org/page.tsx @@ -0,0 +1,158 @@ +import { formatAxiosError, priv } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { cache } from "react"; +import { verifySession } from "@app/lib/auth/verifySession"; +import { redirect } from "next/navigation"; +import { pullEnv } from "@app/lib/pullEnv"; +import { LoginFormIDP } from "@app/components/LoginForm"; +import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; +import { build } from "@server/build"; +import { headers } from "next/headers"; +import { + LoadLoginPageBrandingResponse, + LoadLoginPageResponse +} from "@server/routers/loginPage/types"; +import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types"; +import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken"; +import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed"; +import { OrgSelectionForm } from "@app/components/OrgSelectionForm"; +import OrgLoginPage from "@app/components/OrgLoginPage"; + +export const dynamic = "force-dynamic"; + +export default async function OrgAuthPage(props: { + params: Promise<{}>; + searchParams: Promise<{ + token?: string; + redirect?: string; + forceLogin?: string; + }>; +}) { + const searchParams = await props.searchParams; + const forceLoginParam = searchParams.forceLogin; + const forceLogin = forceLoginParam === "true"; + + const env = pullEnv(); + + const authHeader = await authCookieHeader(); + + if (searchParams.token) { + return ( + + ); + } + + const getUser = cache(verifySession); + const user = await getUser({ skipCheckVerifyEmail: true }); + + const allHeaders = await headers(); + const host = allHeaders.get("host"); + + const expectedHost = env.app.dashboardUrl.split("//")[1]; + + let redirectToUrl: string | undefined; + let loginPage: LoadLoginPageResponse | undefined; + if (host !== expectedHost) { + try { + const res = await priv.get>( + `/login-page?fullDomain=${host}` + ); + + if (res && res.status === 200) { + loginPage = res.data.data; + } + } catch (e) {} + + if (!loginPage) { + console.debug( + `No login page found for host ${host}, redirecting to dashboard` + ); + redirect(env.app.dashboardUrl); + } + + const subscribed = await isOrgSubscribed(loginPage.orgId); + + if (build === "saas" && !subscribed) { + console.log( + `Org ${loginPage.orgId} is not subscribed, redirecting to dashboard` + ); + redirect(env.app.dashboardUrl); + } + + console.log(user, forceLogin); + + if (user && !forceLogin) { + let redirectToken: string | undefined; + try { + const res = await priv.post< + AxiosResponse + >(`/get-session-transfer-token`, {}, authHeader); + + if (res && res.status === 200) { + const newToken = res.data.data.token; + redirectToken = newToken; + } + } catch (e) { + console.error( + formatAxiosError(e, "Failed to get transfer token") + ); + } + + if (redirectToken) { + // redirectToUrl = `${env.app.dashboardUrl}/auth/org?token=${redirectToken}`; + // include redirect param if exists + redirectToUrl = `${env.app.dashboardUrl}/auth/org?token=${redirectToken}${ + searchParams.redirect + ? `&redirect=${encodeURIComponent( + searchParams.redirect + )}` + : "" + }`; + console.log( + `Redirecting logged in user to org auth callback: ${redirectToUrl}` + ); + redirect(redirectToUrl); + } + } + } else { + return ; + } + + let loginIdps: LoginFormIDP[] = []; + if (build === "saas") { + const idpsRes = await priv.get>( + `/org/${loginPage.orgId}/idp` + ); + + loginIdps = idpsRes.data.data.idps.map((idp) => ({ + idpId: idp.idpId, + name: idp.name, + variant: idp.variant + })) as LoginFormIDP[]; + } + + let branding: LoadLoginPageBrandingResponse | null = null; + if (build === "saas") { + try { + const res = await priv.get< + AxiosResponse + >(`/login-page-branding?orgId=${loginPage.orgId}`); + if (res.status === 200) { + branding = res.data.data; + } + } catch (error) {} + } + + return ( + + ); +} diff --git a/src/app/auth/resource/[resourceGuid]/page.tsx b/src/app/auth/resource/[resourceGuid]/page.tsx index 26d8bbea..e6d87075 100644 --- a/src/app/auth/resource/[resourceGuid]/page.tsx +++ b/src/app/auth/resource/[resourceGuid]/page.tsx @@ -19,11 +19,15 @@ import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; import AutoLoginHandler from "@app/components/AutoLoginHandler"; import { build } from "@server/build"; import { headers } from "next/headers"; -import { GetLoginPageResponse } from "@server/routers/loginPage/types"; +import type { + LoadLoginPageBrandingResponse, + LoadLoginPageResponse +} from "@server/routers/loginPage/types"; import { GetOrgTierResponse } from "@server/routers/billing/types"; import { TierId } from "@server/lib/billing/tiers"; import { CheckOrgUserAccessResponse } from "@server/routers/org"; import OrgPolicyRequired from "@app/components/OrgPolicyRequired"; +import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed"; export const dynamic = "force-dynamic"; @@ -52,8 +56,7 @@ export default async function ResourceAuthPage(props: { } } catch (e) {} - const getUser = cache(verifySession); - const user = await getUser({ skipCheckVerifyEmail: true }); + const user = await verifySession({ skipCheckVerifyEmail: true }); if (!authInfo) { return ( @@ -63,22 +66,7 @@ export default async function ResourceAuthPage(props: { ); } - let subscriptionStatus: GetOrgTierResponse | null = null; - if (build == "saas") { - try { - const getSubscription = cache(() => - priv.get>( - `/org/${authInfo.orgId}/billing/tier` - ) - ); - const subRes = await getSubscription(); - subscriptionStatus = subRes.data.data; - } catch {} - } - const subscribed = - build === "enterprise" - ? true - : subscriptionStatus?.tier === TierId.STANDARD; + const subscribed = await isOrgSubscribed(authInfo.orgId); const allHeaders = await headers(); const host = allHeaders.get("host"); @@ -89,9 +77,9 @@ export default async function ResourceAuthPage(props: { redirect(env.app.dashboardUrl); } - let loginPage: GetLoginPageResponse | undefined; + let loginPage: LoadLoginPageResponse | undefined; try { - const res = await priv.get>( + const res = await priv.get>( `/login-page?resourceId=${authInfo.resourceId}&fullDomain=${host}` ); @@ -106,6 +94,7 @@ export default async function ResourceAuthPage(props: { } let redirectUrl = authInfo.url; + if (searchParams.redirect) { try { const serverResourceHost = new URL(authInfo.url).host; @@ -230,9 +219,7 @@ export default async function ResourceAuthPage(props: { })) as LoginFormIDP[]; } } else { - const idpsRes = await cache( - async () => await priv.get>("/idp") - )(); + const idpsRes = await priv.get>("/idp"); loginIdps = idpsRes.data.data.idps.map((idp) => ({ idpId: idp.idpId, name: idp.name, @@ -253,12 +240,24 @@ export default async function ResourceAuthPage(props: { resourceId={authInfo.resourceId} skipToIdpId={authInfo.skipToIdpId} redirectUrl={redirectUrl} - orgId={build == "saas" ? authInfo.orgId : undefined} + orgId={build === "saas" ? authInfo.orgId : undefined} /> ); } } + let branding: LoadLoginPageBrandingResponse | null = null; + try { + if (subscribed) { + const res = await priv.get< + AxiosResponse + >(`/login-page-branding?orgId=${authInfo.orgId}`); + if (res.status === 200) { + branding = res.data.data; + } + } + } catch (error) {} + return ( <> {userIsUnauthorized && isSSOOnly ? ( @@ -281,6 +280,19 @@ export default async function ResourceAuthPage(props: { redirect={redirectUrl} idps={loginIdps} orgId={build === "saas" ? authInfo.orgId : undefined} + branding={ + !branding || build === "oss" + ? undefined + : { + logoHeight: branding.logoHeight, + logoUrl: branding.logoUrl, + logoWidth: branding.logoWidth, + primaryColor: branding.primaryColor, + resourceTitle: branding.resourceTitle, + resourceSubtitle: + branding.resourceSubtitle + } + } />
)} diff --git a/src/app/globals.css b/src/app/globals.css index a2a98a2c..bd5860a6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -5,8 +5,8 @@ @custom-variant dark (&:is(.dark *)); :root { - --radius: 0.65rem; - --background: oklch(0.99 0 0); + --radius: 0.75rem; + --background: oklch(0.985 0 0); --foreground: oklch(0.141 0.005 285.823); --card: oklch(1 0 0); --card-foreground: oklch(0.141 0.005 285.823); @@ -21,7 +21,7 @@ --accent: oklch(0.967 0.001 286.375); --accent-foreground: oklch(0.21 0.006 285.885); --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.92 0.004 286.32); + --border: oklch(0.91 0.004 286.32); --input: oklch(0.92 0.004 286.32); --ring: oklch(0.705 0.213 47.604); --chart-1: oklch(0.646 0.222 41.116); @@ -40,7 +40,7 @@ } .dark { - --background: oklch(0.2 0.006 285.885); + --background: oklch(0.19 0.006 285.885); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); --card-foreground: oklch(0.985 0 0); @@ -55,8 +55,8 @@ --accent: oklch(0.274 0.006 286.033); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.5382 0.1949 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); + --border: oklch(1 0 0 / 13%); + --input: oklch(1 0 0 / 18%); --ring: oklch(0.646 0.222 41.116); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 36d5d2ce..e76a5d2f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,6 @@ import type { Metadata } from "next"; import "./globals.css"; -import { Inter } from "next/font/google"; +import { Geist, Inter, Manrope, Open_Sans } from "next/font/google"; import { ThemeProvider } from "@app/providers/ThemeProvider"; import EnvProvider from "@app/providers/EnvProvider"; import { pullEnv } from "@app/lib/pullEnv"; @@ -30,7 +30,9 @@ export const metadata: Metadata = { export const dynamic = "force-dynamic"; -const font = Inter({ subsets: ["latin"] }); +const font = Inter({ + subsets: ["latin"] +}); export default async function RootLayout({ children diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index fabc24b7..54576c0c 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -82,6 +82,11 @@ export const orgNavSections = (): SidebarNavSection[] => [ } ] }, + { + title: "sidebarDomains", + href: "/{orgId}/settings/domains", + icon: + }, ...(build == "saas" ? [ { @@ -91,17 +96,7 @@ export const orgNavSections = (): SidebarNavSection[] => [ showEE: true } ] - : []), - { - title: "sidebarDomains", - href: "/{orgId}/settings/domains", - icon: - }, - { - title: "sidebarBluePrints", - href: "/{orgId}/settings/blueprints", - icon: - } + : []) ] }, { @@ -200,6 +195,17 @@ export const orgNavSections = (): SidebarNavSection[] => [ href: "/{orgId}/settings/api-keys", icon: }, + { + title: "sidebarBluePrints", + href: "/{orgId}/settings/blueprints", + icon: + }, + { + title: "sidebarSettings", + href: "/{orgId}/settings/general", + icon: + }, + ...(build == "saas" ? [ { @@ -217,12 +223,7 @@ export const orgNavSections = (): SidebarNavSection[] => [ icon: } ] - : []), - { - title: "sidebarSettings", - href: "/{orgId}/settings/general", - icon: - } + : []) ] } ]; diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 36853e5c..10a8b14e 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -41,13 +41,14 @@ export default function StepperForm() { const [loading, setLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); - const [error, setError] = useState(null); + // Removed error state, now using toast for API errors const [orgCreated, setOrgCreated] = useState(false); const orgSchema = z.object({ orgName: z.string().min(1, { message: t("orgNameRequired") }), orgId: z.string().min(1, { message: t("orgIdRequired") }), - subnet: z.string().min(1, { message: t("subnetRequired") }) + subnet: z.string().min(1, { message: t("subnetRequired") }), + utilitySubnet: z.string().min(1, { message: t("subnetRequired") }) }); const orgForm = useForm({ @@ -55,7 +56,8 @@ export default function StepperForm() { defaultValues: { orgName: "", orgId: "", - subnet: "" + subnet: "", + utilitySubnet: "" } }); @@ -72,6 +74,7 @@ export default function StepperForm() { const res = await api.get(`/pick-org-defaults`); if (res && res.data && res.data.data) { orgForm.setValue("subnet", res.data.data.subnet); + orgForm.setValue("utilitySubnet", res.data.data.utilitySubnet); } } catch (e) { console.error("Failed to fetch default subnet:", e); @@ -129,7 +132,8 @@ export default function StepperForm() { const res = await api.put(`/org`, { orgId: values.orgId, name: values.orgName, - subnet: values.subnet + subnet: values.subnet, + utilitySubnet: values.utilitySubnet }); if (res && res.status === 201) { @@ -138,7 +142,11 @@ export default function StepperForm() { } } catch (e) { console.error(e); - setError(formatAxiosError(e, t("orgErrorCreate"))); + toast({ + title: t("error"), + description: formatAxiosError(e, t("orgErrorCreate")), + variant: "destructive" + }); } setLoading(false); @@ -320,6 +328,30 @@ export default function StepperForm() { )} /> + ( + + + {t("setupUtilitySubnet")} + + + + + + + {t( + "setupUtilitySubnetDescription" + )} + + + )} + /> + {orgIdTaken && !orgCreated ? ( @@ -328,20 +360,13 @@ export default function StepperForm() { ) : null} - {error && ( - - - {error} - - - )} + {/* Error Alert removed, errors now shown as toast */}
- {r.type !== "internal" && ( + {r.type === "internal" && ( { generatePasswordResetCode(r.id); @@ -313,7 +318,7 @@ export default function UsersTable({ users }: Props) { setSelected(null); }} dialog={ -
+

{t("userQuestionRemove", { selectedUser: diff --git a/src/components/ApiKeysTable.tsx b/src/components/ApiKeysTable.tsx index c3202277..8987fa2c 100644 --- a/src/components/ApiKeysTable.tsx +++ b/src/components/ApiKeysTable.tsx @@ -182,7 +182,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { setSelected(null); }} dialog={ -

+

{t("apiKeysQuestionRemove")}

{t("apiKeysMessageRemove")}

diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx new file mode 100644 index 00000000..67e2232f --- /dev/null +++ b/src/components/AuthPageBrandingForm.tsx @@ -0,0 +1,432 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useActionState, useState } from "react"; +import { useForm } from "react-hook-form"; +import z from "zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "./Settings"; +import { useTranslations } from "next-intl"; + +import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types"; +import { Input } from "./ui/input"; +import { ExternalLink, InfoIcon, XIcon } from "lucide-react"; +import { Button } from "./ui/button"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useRouter } from "next/navigation"; +import { toast } from "@app/hooks/useToast"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { build } from "@server/build"; +import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; +import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; + +export type AuthPageCustomizationProps = { + orgId: string; + branding: GetLoginPageBrandingResponse | null; +}; + +const AuthPageFormSchema = z.object({ + logoUrl: z.url().refine( + async (url) => { + try { + const response = await fetch(url); + return ( + response.status === 200 && + (response.headers.get("content-type") ?? "").startsWith( + "image/" + ) + ); + } catch (error) { + return false; + } + }, + { + error: "Invalid logo URL, must be a valid image URL" + } + ), + logoWidth: z.coerce.number().min(1), + logoHeight: z.coerce.number().min(1), + orgTitle: z.string().optional(), + orgSubtitle: z.string().optional(), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional(), + primaryColor: z + .string() + .regex(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i) + .optional() +}); + +export default function AuthPageBrandingForm({ + orgId, + branding +}: AuthPageCustomizationProps) { + const env = useEnvContext(); + const api = createApiClient(env); + const { isPaidUser } = usePaidStatus(); + + const router = useRouter(); + + const [, updateFormAction, isUpdatingBranding] = useActionState( + updateBranding, + null + ); + const [, deleteFormAction, isDeletingBranding] = useActionState( + deleteBranding, + null + ); + const [setIsDeleteModalOpen] = useState(false); + + const t = useTranslations(); + + const form = useForm({ + resolver: zodResolver(AuthPageFormSchema), + defaultValues: { + logoUrl: branding?.logoUrl ?? "", + logoWidth: branding?.logoWidth ?? 100, + logoHeight: branding?.logoHeight ?? 100, + orgTitle: branding?.orgTitle ?? `Log in to {{orgName}}`, + orgSubtitle: branding?.orgSubtitle ?? `Log in to {{orgName}}`, + resourceTitle: + branding?.resourceTitle ?? + `Authenticate to access {{resourceName}}`, + resourceSubtitle: + branding?.resourceSubtitle ?? + `Choose your preferred authentication method for {{resourceName}}`, + primaryColor: branding?.primaryColor ?? `#f36117` // default pangolin primary color + }, + disabled: !isPaidUser + }); + + async function updateBranding() { + const isValid = await form.trigger(); + const brandingData = form.getValues(); + + if (!isValid || !isPaidUser) return; + try { + const updateRes = await api.put( + `/org/${orgId}/login-page-branding`, + { + ...brandingData + } + ); + + if (updateRes.status === 200 || updateRes.status === 201) { + router.refresh(); + toast({ + variant: "default", + title: t("success"), + description: t("authPageBrandingUpdated") + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: t("authPageErrorUpdate"), + description: formatAxiosError( + error, + t("authPageErrorUpdateMessage") + ) + }); + } + } + + async function deleteBranding() { + if (!isPaidUser) return; + + try { + const updateRes = await api.delete( + `/org/${orgId}/login-page-branding` + ); + + if (updateRes.status === 200) { + router.refresh(); + form.reset(); + + toast({ + variant: "default", + title: t("success"), + description: t("authPageBrandingRemoved") + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: t("authPageErrorUpdate"), + description: formatAxiosError( + error, + t("authPageErrorUpdateMessage") + ) + }); + } + } + + return ( + <> + + + + {t("authPageBranding")} + + + {t("authPageBrandingDescription")} + + + + + + + +
+ + ( + + + {t("brandingPrimaryColor")} + + +
+ + + + +
+ + +
+ )} + /> + +
+ ( + + + {t("brandingLogoURL")} + + + + + + + )} + /> +
+ ( + + + {t("brandingLogoWidth")} + + + + + + + )} + /> + + + + + + ( + + + {t( + "brandingLogoHeight" + )} + + + + + + + )} + /> +
+
+ + {build === "saas" && ( + <> +
+ + {t( + "organizationLoginPageTitle" + )} + + + {t( + "organizationLoginPageDescription" + )} + +
+ +
+ ( + + + {t( + "brandingOrgTitle" + )} + + + + + + + + )} + /> + ( + + + {t( + "brandingOrgSubtitle" + )} + + + + + + + + )} + /> +
+ + )} + +
+ + {t("resourceLoginPageTitle")} + + + {t("resourceLoginPageDescription")} + +
+ +
+ ( + + + {t("brandingResourceTitle")} + + + + + + + + )} + /> + ( + + + {t( + "brandingResourceSubtitle" + )} + + + + + + + )} + /> +
+ + +
+
+ +
+ {branding && ( + + )} + +
+
+ + ); +} diff --git a/src/components/BlueprintDetailsForm.tsx b/src/components/BlueprintDetailsForm.tsx index ae6d5cb1..92b6a304 100644 --- a/src/components/BlueprintDetailsForm.tsx +++ b/src/components/BlueprintDetailsForm.tsx @@ -52,8 +52,14 @@ export default function BlueprintDetailsForm({
- - + + + + {t("name")} + + {blueprint.name} + + {t("status")} @@ -121,6 +127,8 @@ export default function BlueprintDetailsForm({ + + {blueprint.message && ( @@ -138,60 +146,39 @@ export default function BlueprintDetailsForm({ - - - {t("blueprintInfo")} - - - - ( - - {t("name")} - - - - - - )} - /> - - ( - - - {t("parsedContents")} - - -
- -
-
- -
- )} - /> -
+ ( + + + {t("parsedContents")} + + +
+ +
+
+ +
+ )} + />
diff --git a/src/components/BlueprintsTable.tsx b/src/components/BlueprintsTable.tsx index 0fa05d75..8031e506 100644 --- a/src/components/BlueprintsTable.tsx +++ b/src/components/BlueprintsTable.tsx @@ -32,35 +32,6 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) { const router = useRouter(); const columns: ExtendedColumnDef[] = [ - { - accessorKey: "createdAt", - friendlyName: t("appliedAt"), - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return ( - - ); - } - }, { accessorKey: "name", enableHiding: false, @@ -79,7 +50,32 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) { ); } }, - + { + accessorKey: "createdAt", + friendlyName: t("appliedAt"), + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return ( + + ); + } + }, { accessorKey: "source", friendlyName: t("source"), @@ -104,7 +100,7 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) { API - + ); @@ -114,7 +110,7 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) { Newt CLI - + ); @@ -174,7 +170,7 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) { href={`/${orgId}/settings/blueprints/${row.original.blueprintId}`} > diff --git a/src/components/BrandingLogo.tsx b/src/components/BrandingLogo.tsx index b4472365..139d76b4 100644 --- a/src/components/BrandingLogo.tsx +++ b/src/components/BrandingLogo.tsx @@ -7,6 +7,7 @@ import Image from "next/image"; import { useEffect, useState } from "react"; type BrandingLogoProps = { + logoPath?: string; width: number; height: number; }; @@ -41,13 +42,16 @@ export default function BrandingLogo(props: BrandingLogoProps) { return "/logo/word_mark_white.png"; } - const path = getPath(); - setPath(path); - }, [theme, env]); + setPath(props.logoPath ?? getPath()); + }, [theme, env, props.logoPath]); + + // we use `img` tag here because the `logoPath` could be any URL + // and next.js `Image` component only accepts a restricted number of domains + const Component = props.logoPath ? "img" : Image; return ( path && ( - Logo { + const t = useTranslations(); + + return ( + } + description={t("downloadClientBannerDescription")} + > + + + + + + + + + + + ); +}; + +export default ClientDownloadBanner; + diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index a5e257c7..4cd02743 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -41,6 +41,9 @@ export type InternalResourceRow = { // destinationPort: number | null; alias: string | null; niceId: string; + tcpPortRangeString: string | null; + udpPortRangeString: string | null; + disableIcmp: boolean; }; type ClientResourcesTableProps = { @@ -97,7 +100,7 @@ export default function ClientResourcesTable({ ) => { try { await api - .delete(`/org/${orgId}/site/${siteId}/resource/${resourceId}`) + .delete(`/site-resource/${resourceId}`) .then(() => { startTransition(() => { router.refresh(); @@ -284,7 +287,7 @@ export default function ClientResourcesTable({ setSelectedInternalResource(null); }} dialog={ -
+

{t("resourceQuestionRemove")}

{t("resourceMessageRemove")}

@@ -314,6 +317,9 @@ export default function ClientResourcesTable({ defaultSort={defaultSort} enableColumnVisibility={true} persistColumnVisibility="internal-resources" + columnVisibility={{ + niceId: false + }} stickyLeftColumn="name" stickyRightColumn="actions" /> @@ -324,9 +330,13 @@ export default function ClientResourcesTable({ setOpen={setIsEditDialogOpen} resource={editingResource} orgId={orgId} + sites={sites} onSuccess={() => { - router.refresh(); - setEditingResource(null); + // Delay refresh to allow modal to close smoothly + setTimeout(() => { + router.refresh(); + setEditingResource(null); + }, 150); }} /> )} @@ -337,7 +347,10 @@ export default function ClientResourcesTable({ orgId={orgId} sites={sites} onSuccess={() => { - router.refresh(); + // Delay refresh to allow modal to close smoothly + setTimeout(() => { + router.refresh(); + }, 150); }} /> diff --git a/src/components/ConfirmDeleteDialog.tsx b/src/components/ConfirmDeleteDialog.tsx index e2bc271f..86b25bb2 100644 --- a/src/components/ConfirmDeleteDialog.tsx +++ b/src/components/ConfirmDeleteDialog.tsx @@ -6,43 +6,22 @@ import { 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 { useToast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; -import { - InviteUserBody, - InviteUserResponse, - ListUsersResponse -} from "@server/routers/user"; -import { AxiosResponse } from "axios"; -import React, { useState } from "react"; +import React, { useActionState } 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 { Description } from "@radix-ui/react-toast"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; import CopyToClipboard from "./CopyToClipboard"; @@ -57,7 +36,7 @@ type InviteUserFormProps = { warningText?: string; }; -export default function InviteUserForm({ +export default function ConfirmDeleteDialog({ open, setOpen, string, @@ -67,9 +46,7 @@ export default function InviteUserForm({ dialog, warningText }: InviteUserFormProps) { - const [loading, setLoading] = useState(false); - - const api = createApiClient(useEnvContext()); + const [, formAction, loading] = useActionState(onSubmit, null); const t = useTranslations(); @@ -86,21 +63,14 @@ export default function InviteUserForm({ } }); - function reset() { - form.reset(); - } - - async function onSubmit(values: z.infer) { - setLoading(true); + async function onSubmit() { try { await onConfirm(); setOpen(false); - reset(); + form.reset(); } catch (error) { // Handle error if needed console.error("Confirmation failed:", error); - } finally { - setLoading(false); } } @@ -110,7 +80,7 @@ export default function InviteUserForm({ open={open} onOpenChange={(val) => { setOpen(val); - reset(); + form.reset(); }} > @@ -136,7 +106,7 @@ export default function InviteUserForm({
@@ -146,7 +116,12 @@ export default function InviteUserForm({ render={({ field }) => ( - + diff --git a/src/components/CreateBlueprintForm.tsx b/src/components/CreateBlueprintForm.tsx index 617f3026..2b58f34f 100644 --- a/src/components/CreateBlueprintForm.tsx +++ b/src/components/CreateBlueprintForm.tsx @@ -127,7 +127,7 @@ export default function CreateBlueprintForm({ - + )} /> - - ( - - - {t("contents")} - - - {t( - "blueprintContentsDescription" - )} - - -
- -
-
- -
- )} - />
+ + ( + + {t("contents")} + + {t("blueprintContentsDescription")} + + +
+ +
+
+ +
+ )} + />
diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 91ef26da..afdaa77e 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -42,15 +42,14 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; +import { Switch } from "@app/components/ui/switch"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; import { orgQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; -import { ListClientsResponse } from "@server/routers/client/listClients"; import { ListSitesResponse } from "@server/routers/site"; -import { ListUsersResponse } from "@server/routers/user"; import { UserType } from "@server/types/UserTypes"; import { useQuery } from "@tanstack/react-query"; import { AxiosResponse } from "axios"; @@ -59,6 +58,84 @@ import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs"; +// import { InfoPopup } from "@app/components/ui/info-popup"; + +// Helper to validate port range string format +const isValidPortRangeString = (val: string | undefined | null): boolean => { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; + } + + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { + return false; + } + + if (startPort > endPort) { + return false; + } + } else { + const port = parseInt(part, 10); + if (isNaN(port)) { + return false; + } + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; +}; + +// Port range string schema for client-side validation +// Note: This schema is defined outside the component, so we'll use a function to get the message +const getPortRangeValidationMessage = (t: (key: string) => string) => + t("editInternalResourceDialogPortRangeValidationError"); + +const createPortRangeStringSchema = (t: (key: string) => string) => + z + .string() + .optional() + .nullable() + .refine((val) => isValidPortRangeString(val), { + message: getPortRangeValidationMessage(t) + }); + +// Helper to determine the port mode from a port range string +type PortMode = "all" | "blocked" | "custom"; +const getPortModeFromString = (val: string | undefined | null): PortMode => { + if (val === "*") return "all"; + if (!val || val.trim() === "") return "blocked"; + return "custom"; +}; + +// Helper to get the port string for API from mode and custom value +const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => { + if (mode === "all") return "*"; + if (mode === "blocked") return ""; + return customValue; +}; type Site = ListSitesResponse["sites"][0]; @@ -86,23 +163,19 @@ export default function CreateInternalResourceDialog({ .string() .min(1, t("createInternalResourceDialogNameRequired")) .max(255, t("createInternalResourceDialogNameMaxLength")), - // mode: z.enum(["host", "cidr", "port"]), - mode: z.enum(["host", "cidr"]), - destination: z.string().min(1), siteId: z .int() .positive(t("createInternalResourceDialogPleaseSelectSite")), - // protocol: z.enum(["tcp", "udp"]), - // proxyPort: z.int() - // .positive() - // .min(1, t("createInternalResourceDialogProxyPortMin")) - // .max(65535, t("createInternalResourceDialogProxyPortMax")), - // destinationPort: z.int() - // .positive() - // .min(1, t("createInternalResourceDialogDestinationPortMin")) - // .max(65535, t("createInternalResourceDialogDestinationPortMax")) - // .nullish(), + // mode: z.enum(["host", "cidr", "port"]), + mode: z.enum(["host", "cidr"]), + // protocol: z.enum(["tcp", "udp"]).nullish(), + // proxyPort: z.int().positive().min(1, t("createInternalResourceDialogProxyPortMin")).max(65535, t("createInternalResourceDialogProxyPortMax")).nullish(), + destination: z.string().min(1), + // destinationPort: z.int().positive().min(1, t("createInternalResourceDialogDestinationPortMin")).max(65535, t("createInternalResourceDialogDestinationPortMax")).nullish(), alias: z.string().nullish(), + tcpPortRangeString: createPortRangeStringSchema(t), + udpPortRangeString: createPortRangeStringSchema(t), + disableIcmp: z.boolean().optional(), roles: z .array( z.object({ @@ -209,6 +282,12 @@ export default function CreateInternalResourceDialog({ number | null >(null); + // Port restriction UI state - default to "all" (*) for new resources + const [tcpPortMode, setTcpPortMode] = useState("all"); + const [udpPortMode, setUdpPortMode] = useState("all"); + const [tcpCustomPorts, setTcpCustomPorts] = useState(""); + const [udpCustomPorts, setUdpCustomPorts] = useState(""); + const availableSites = sites.filter( (site) => site.type === "newt" && site.subnet ); @@ -224,6 +303,9 @@ export default function CreateInternalResourceDialog({ destination: "", // destinationPort: undefined, alias: "", + tcpPortRangeString: "*", + udpPortRangeString: "*", + disableIcmp: false, roles: [], users: [], clients: [] @@ -232,6 +314,17 @@ export default function CreateInternalResourceDialog({ const mode = form.watch("mode"); + // Update form values when port mode or custom ports change + useEffect(() => { + const tcpValue = getPortStringFromMode(tcpPortMode, tcpCustomPorts); + form.setValue("tcpPortRangeString", tcpValue); + }, [tcpPortMode, tcpCustomPorts, form]); + + useEffect(() => { + const udpValue = getPortStringFromMode(udpPortMode, udpCustomPorts); + form.setValue("udpPortRangeString", udpValue); + }, [udpPortMode, udpCustomPorts, form]); + // Helper function to check if destination contains letters (hostname vs IP) const isHostname = (destination: string): boolean => { return /[a-zA-Z]/.test(destination); @@ -258,10 +351,18 @@ export default function CreateInternalResourceDialog({ destination: "", // destinationPort: undefined, alias: "", + tcpPortRangeString: "*", + udpPortRangeString: "*", + disableIcmp: false, roles: [], users: [], clients: [] }); + // Reset port mode state + setTcpPortMode("all"); + setUdpPortMode("all"); + setTcpCustomPorts(""); + setUdpCustomPorts(""); } }, [open]); @@ -289,9 +390,10 @@ export default function CreateInternalResourceDialog({ } const response = await api.put>( - `/org/${orgId}/site/${data.siteId}/resource`, + `/org/${orgId}/site-resource`, { name: data.name, + siteId: data.siteId, mode: data.mode, // protocol: data.protocol, // proxyPort: data.mode === "port" ? data.proxyPort : undefined, @@ -304,6 +406,9 @@ export default function CreateInternalResourceDialog({ data.alias.trim() ? data.alias : undefined, + tcpPortRangeString: data.tcpPortRangeString, + udpPortRangeString: data.udpPortRangeString, + disableIcmp: data.disableIcmp ?? false, roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [], @@ -343,8 +448,8 @@ export default function CreateInternalResourceDialog({ variant: "default" }); - onSuccess?.(); setOpen(false); + onSuccess?.(); } catch (error) { console.error("Error creating internal resource:", error); toast({ @@ -388,7 +493,7 @@ export default function CreateInternalResourceDialog({ return ( - + {t("createInternalResourceDialogCreateClientResource")} @@ -406,179 +511,180 @@ export default function CreateInternalResourceDialog({ className="space-y-6" id="create-internal-resource-form" > - {/* Resource Properties Form */} -
-

- {t( - "createInternalResourceDialogResourceProperties" + {/* Name and Site - Side by Side */} +
+ ( + + + {t( + "createInternalResourceDialogName" + )} + + + + + + )} -

-
- ( - - - {t( - "createInternalResourceDialogName" - )} - - - - - - - )} - /> + /> - ( - - - {t( - "createInternalResourceDialogSite" - )} - - - - - - - - - - - - - {t( - "createInternalResourceDialogNoSitesFound" - )} - - - {availableSites.map( - ( - site - ) => ( - { - field.onChange( - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - - )} - /> - - ( - - - {t( - "createInternalResourceDialogMode" - )} - - - - - )} - /> - {/* - {mode === "port" && ( - <> -
+ /> + + + {t( + "noSitesFound" + )} + + + {availableSites.map( + (site) => ( + { + field.onChange( + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + + )} + /> +
+ + {/* Tabs for Network Settings and Access Control */} + + {/* Network Settings Tab */} +
+
+
+ +
+ {t( + "editInternalResourceDialogDestinationDescription" + )} +
+
+ +
+ {/* Mode - Smaller select */} +
( - {t("createInternalResourceDialogProtocol")} + {t( + "createInternalResourceDialogMode" + )} @@ -598,22 +708,29 @@ export default function CreateInternalResourceDialog({ )} /> +
+ {/* Destination - Larger input */} +
( - {t("createInternalResourceDialogSitePort")} + + {t( + "createInternalResourceDialogDestination" + )} + - field.onChange( - e.target.value === "" ? undefined : parseInt(e.target.value) - ) - } + {...field} /> @@ -621,261 +738,396 @@ export default function CreateInternalResourceDialog({ )} />
- - )} */} -
-
- {/* Target Configuration Form */} -
-

- {t( - "createInternalResourceDialogTargetConfiguration" - )} -

-
- ( - - - {t( - "createInternalResourceDialogDestination" - )} - - - - - - {mode === "host" && - t( - "createInternalResourceDialogDestinationHostDescription" + {/* Alias - Equally sized input (if allowed) */} + {mode !== "cidr" && ( +
+ ( + + + {t( + "createInternalResourceDialogAlias" + )} + + + + + + )} - {mode === "cidr" && - t( - "createInternalResourceDialogDestinationCidrDescription" - )} - {/* {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} */} - - - - )} - /> - - {/* {mode === "port" && ( - ( - - - {t("targetPort")} - - - - field.onChange( - e.target.value === "" ? undefined : parseInt(e.target.value) - ) - } - /> - - - {t("createInternalResourceDialogDestinationPortDescription")} - - - + /> +
)} - /> - )} */} -
-
+
+
- {/* Alias */} - {mode !== "cidr" && ( -
- ( - - + {/* Ports and Restrictions */} +
+ {/* TCP Ports */} +
+ +
+ {t( + "editInternalResourceDialogPortRestrictionsDescription" + )} +
+
+
+
+ {t( - "createInternalResourceDialogAlias" + "editInternalResourceDialogTcp" )} - - - - - {t( - "createInternalResourceDialogAliasDescription" +
+
+ ( + +
+ {/**/} + + {tcpPortMode === + "custom" ? ( + + + setTcpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
)} - - - - )} - /> -
- )} + /> +
+
- {/* Access Control Section */} -
-

- {t("resourceUsersRoles")} -

-
- ( - - - {t("roles")} - - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allRoles - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - + {/* UDP Ports */} +
+
+ {t( - "resourceRoleDescription" + "editInternalResourceDialogUdp" )} - - - )} - /> - ( - - - {t("users")} - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allUsers - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - {hasMachineClients && ( +
+
+ ( + +
+ {/**/} + + {udpPortMode === + "custom" ? ( + + + setUdpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ + {/* ICMP Toggle */} +
+
+ + {t( + "editInternalResourceDialogIcmp" + )} + +
+
+ ( + +
+ + + field.onChange( + !checked + ) + } + /> + + + {field.value + ? t( + "blocked" + ) + : t( + "allowed" + )} + +
+ +
+ )} + /> +
+
+
+
+ + {/* Access Control Tab */} +
+
+ +
+ {t( + "editInternalResourceDialogAccessControlDescription" + )} +
+
+
+ {/* Roles */} ( - {t("machineClients")} + {t("roles")} { form.setValue( - "clients", - newClients as [ + "roles", + newRoles as [ Tag, ...Tag[] ] @@ -885,7 +1137,70 @@ export default function CreateInternalResourceDialog({ true } autocompleteOptions={ - allClients + allRoles + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t( + "resourceRoleDescription" + )} + + + )} + /> + + {/* Users */} + ( + + + {t("users")} + + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers } allowDuplicates={ false @@ -900,9 +1215,76 @@ export default function CreateInternalResourceDialog({ )} /> - )} + + {/* Clients (Machines) */} + {hasMachineClients && ( + ( + + + {t( + "machineClients" + )} + + + { + form.setValue( + "clients", + newClients as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allClients + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={ + true + } + /> + + + + )} + /> + )} +
-
+ diff --git a/src/components/Credenza.tsx b/src/components/Credenza.tsx index 6a48fc54..0446500c 100644 --- a/src/components/Credenza.tsx +++ b/src/components/Credenza.tsx @@ -179,7 +179,7 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => { return ( ({ href="https://docs.pangolin.net/manage/domains" target="_blank" rel="noopener noreferrer" + className="hidden sm:block" > + +
+
+

+ {titleIcon} + {title} +

+

+ {description} +

+
+ {children && ( +
+ {children} +
+ )} +
+
+ + ); +}; + +export default DismissableBanner; + diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 2d17e39f..5c99fa7e 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -1,8 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Command, @@ -13,45 +11,40 @@ import { CommandList, CommandSeparator } from "@/components/ui/command"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { - AlertCircle, - CheckCircle2, - Building2, - Zap, - Check, - ChevronsUpDown, - ArrowUpDown -} from "lucide-react"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { createApiClient, formatAxiosError } from "@/lib/api"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { useEnvContext } from "@/hooks/useEnvContext"; import { toast } from "@/hooks/useToast"; -import { ListDomainsResponse } from "@server/routers/domain/listDomains"; -import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; -import { AxiosResponse } from "axios"; +import { createApiClient } from "@/lib/api"; import { cn } from "@/lib/cn"; -import { useTranslations } from "next-intl"; -import { build } from "@server/build"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { - sanitizeInputRaw, finalizeSubdomainSanitize, - validateByDomainType, - isValidSubdomainStructure + isValidSubdomainStructure, + sanitizeInputRaw, + validateByDomainType } from "@/lib/subdomain-utils"; +import { orgQueries } from "@app/lib/queries"; +import { build } from "@server/build"; +import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; +import { useQuery } from "@tanstack/react-query"; +import { AxiosResponse } from "axios"; +import { + AlertCircle, + Building2, + Check, + CheckCircle2, + ChevronsUpDown, + Zap +} from "lucide-react"; +import { useTranslations } from "next-intl"; import { toUnicode } from "punycode"; - -type OrganizationDomain = { - domainId: string; - baseDomain: string; - verified: boolean; - type: "ns" | "cname" | "wildcard"; -}; +import { useCallback, useEffect, useMemo, useState } from "react"; type AvailableOption = { domainNamespaceId: string; @@ -69,128 +62,152 @@ type DomainOption = { domainNamespaceId?: string; }; -interface DomainPicker2Props { +interface DomainPickerProps { orgId: string; - onDomainChange?: (domainInfo: { - domainId: string; - domainNamespaceId?: string; - type: "organization" | "provided"; - subdomain?: string; - fullDomain: string; - baseDomain: string; - }) => void; + onDomainChange?: ( + domainInfo: { + domainId: string; + domainNamespaceId?: string; + type: "organization" | "provided"; + subdomain?: string; + fullDomain: string; + baseDomain: string; + } | null + ) => void; cols?: number; hideFreeDomain?: boolean; + defaultFullDomain?: string | null; + defaultSubdomain?: string | null; + defaultDomainId?: string | null; } -export default function DomainPicker2({ +export default function DomainPicker({ orgId, onDomainChange, cols = 2, - hideFreeDomain = false -}: DomainPicker2Props) { + hideFreeDomain = false, + defaultSubdomain, + defaultFullDomain, + defaultDomainId +}: DomainPickerProps) { const { env } = useEnvContext(); const api = createApiClient({ env }); const t = useTranslations(); + console.log({ + defaultFullDomain, + defaultSubdomain, + defaultDomainId + }); + + const { data = [], isLoading: loadingDomains } = useQuery( + orgQueries.domains({ orgId }) + ); + if (!env.flags.usePangolinDns) { hideFreeDomain = true; } - const [subdomainInput, setSubdomainInput] = useState(""); + const [subdomainInput, setSubdomainInput] = useState( + defaultSubdomain ?? "" + ); + const [selectedBaseDomain, setSelectedBaseDomain] = useState(null); const [availableOptions, setAvailableOptions] = useState( [] ); - const [organizationDomains, setOrganizationDomains] = useState< - OrganizationDomain[] - >([]); - const [loadingDomains, setLoadingDomains] = useState(false); + + // memoized to prevent reruning the effect that selects the initial domain indefinitely + // removing this will break and cause an infinite rerender + const organizationDomains = useMemo(() => { + return data + .filter( + (domain) => + domain.type === "ns" || + domain.type === "cname" || + domain.type === "wildcard" + ) + .map((domain) => ({ + ...domain, + baseDomain: toUnicode(domain.baseDomain), + type: domain.type as "ns" | "cname" | "wildcard" + })); + }, [data]); + const [open, setOpen] = useState(false); // Provided domain search states - const [userInput, setUserInput] = useState(""); + const [userInput, setUserInput] = useState(defaultSubdomain ?? ""); const [isChecking, setIsChecking] = useState(false); - const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [providedDomainsShown, setProvidedDomainsShown] = useState(3); const [selectedProvidedDomain, setSelectedProvidedDomain] = useState(null); useEffect(() => { - const loadOrganizationDomains = async () => { - setLoadingDomains(true); - try { - const response = await api.get< - AxiosResponse - >(`/org/${orgId}/domains`); - if (response.status === 200) { - const domains = response.data.data.domains - .filter( - (domain) => - domain.type === "ns" || - domain.type === "cname" || - domain.type === "wildcard" - ) - .map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain), - type: domain.type as "ns" | "cname" | "wildcard" - })); - setOrganizationDomains(domains); - - // Auto-select first available domain - if (domains.length > 0) { - // Select the first organization domain - const firstOrgDomain = domains[0]; - const domainOption: DomainOption = { - id: `org-${firstOrgDomain.domainId}`, - domain: firstOrgDomain.baseDomain, - type: "organization", - verified: firstOrgDomain.verified, - domainType: firstOrgDomain.type, - domainId: firstOrgDomain.domainId - }; - setSelectedBaseDomain(domainOption); - - onDomainChange?.({ - domainId: firstOrgDomain.domainId, - type: "organization", - subdomain: undefined, - fullDomain: firstOrgDomain.baseDomain, - baseDomain: firstOrgDomain.baseDomain - }); - } else if ( - (build === "saas" || build === "enterprise") && - !hideFreeDomain - ) { - // If no organization domains, select the provided domain option - const domainOptionText = - build === "enterprise" - ? t("domainPickerProvidedDomain") - : t("domainPickerFreeProvidedDomain"); - const freeDomainOption: DomainOption = { - id: "provided-search", - domain: domainOptionText, - type: "provided-search" - }; - setSelectedBaseDomain(freeDomainOption); - } + if (!loadingDomains) { + let domainOptionToSelect: DomainOption | null = null; + if (organizationDomains.length > 0) { + // Select the first organization domain or the one provided from props + let firstOrExistingDomain = organizationDomains.find( + (domain) => domain.domainId === defaultDomainId + ); + // if no default Domain + if (!defaultDomainId) { + firstOrExistingDomain = organizationDomains[0]; } - } catch (error) { - console.error("Failed to load organization domains:", error); - toast({ - variant: "destructive", - title: t("domainPickerError"), - description: t("domainPickerErrorLoadDomains") - }); - } finally { - setLoadingDomains(false); - } - }; - loadOrganizationDomains(); - }, [orgId, api, hideFreeDomain]); + if (firstOrExistingDomain) { + domainOptionToSelect = { + id: `org-${firstOrExistingDomain.domainId}`, + domain: firstOrExistingDomain.baseDomain, + type: "organization", + verified: firstOrExistingDomain.verified, + domainType: firstOrExistingDomain.type, + domainId: firstOrExistingDomain.domainId + }; + + onDomainChange?.({ + domainId: firstOrExistingDomain.domainId, + type: "organization", + subdomain: + firstOrExistingDomain.type !== "cname" + ? defaultSubdomain || undefined + : undefined, + fullDomain: firstOrExistingDomain.baseDomain, + baseDomain: firstOrExistingDomain.baseDomain + }); + } + } + + if ( + !domainOptionToSelect && + build !== "oss" && + !hideFreeDomain && + defaultDomainId !== undefined + ) { + // If no organization domains, select the provided domain option + const domainOptionText = + build === "enterprise" + ? t("domainPickerProvidedDomain") + : t("domainPickerFreeProvidedDomain"); + // free domain option + domainOptionToSelect = { + id: "provided-search", + domain: domainOptionText, + type: "provided-search" + }; + } + + setSelectedBaseDomain(domainOptionToSelect); + } + }, [ + loadingDomains, + organizationDomains, + defaultSubdomain, + hideFreeDomain, + defaultDomainId + ]); const checkAvailability = useCallback( async (input: string) => { @@ -256,37 +273,6 @@ export default function DomainPicker2({ } }, [userInput, debouncedCheckAvailability, selectedBaseDomain]); - const generateDropdownOptions = (): DomainOption[] => { - const options: DomainOption[] = []; - - organizationDomains.forEach((orgDomain) => { - options.push({ - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: orgDomain.verified, - domainType: orgDomain.type, - domainId: orgDomain.domainId - }); - }); - - if ((build === "saas" || build === "enterprise") && !hideFreeDomain) { - const domainOptionText = - build === "enterprise" - ? t("domainPickerProvidedDomain") - : t("domainPickerFreeProvidedDomain"); - options.push({ - id: "provided-search", - domain: domainOptionText, - type: "provided-search" - }); - } - - return options; - }; - - const dropdownOptions = generateDropdownOptions(); - const finalizeSubdomain = (sub: string, base: DomainOption): string => { const sanitized = finalizeSubdomainSanitize(sub); @@ -383,6 +369,9 @@ export default function DomainPicker2({ setSelectedProvidedDomain(null); } + console.log({ + setSelectedBaseDomain: option + }); setSelectedBaseDomain(option); setOpen(false); @@ -393,15 +382,21 @@ export default function DomainPicker2({ const fullDomain = sub ? `${sub}.${option.domain}` : option.domain; - onDomainChange?.({ - domainId: option.domainId || "", - domainNamespaceId: option.domainNamespaceId, - type: - option.type === "provided-search" ? "provided" : "organization", - subdomain: sub || undefined, - fullDomain, - baseDomain: option.domain - }); + if (option.type === "provided-search") { + onDomainChange?.(null); // prevent the modal from closing with `.Free Provided domain` + } else { + onDomainChange?.({ + domainId: option.domainId || "", + domainNamespaceId: option.domainNamespaceId, + type: "organization", + subdomain: + option.domainType !== "cname" + ? sub || undefined + : undefined, + fullDomain, + baseDomain: option.domain + }); + } }; const handleProvidedDomainSelect = (option: AvailableOption) => { @@ -440,14 +435,22 @@ export default function DomainPicker2({ selectedBaseDomain?.type === "provided-search"; const sortedAvailableOptions = [...availableOptions].sort((a, b) => { - const comparison = a.fullDomain.localeCompare(b.fullDomain); - return sortOrder === "asc" ? comparison : -comparison; + return a.fullDomain.localeCompare(b.fullDomain); }); const displayedProvidedOptions = sortedAvailableOptions.slice( 0, providedDomainsShown ); + console.log({ + displayedProvidedOptions + }); + + const selectedDomainNamespaceId = + selectedProvidedDomain?.domainNamespaceId ?? + displayedProvidedOptions.find( + (opt) => opt.fullDomain === defaultFullDomain + )?.domainNamespaceId; const hasMoreProvided = sortedAvailableOptions.length > providedDomainsShown; @@ -495,16 +498,6 @@ export default function DomainPicker2({ {t("domainPickerInvalidSubdomainStructure")}

)} - {showSubdomainInput && !subdomainInput && ( -

- {t("domainPickerEnterSubdomainOrLeaveBlank")} -

- )} - {showProvidedDomainSearch && !userInput && ( -

- {t("domainPickerEnterSubdomainToSearch")} -

- )}
@@ -518,16 +511,16 @@ export default function DomainPicker2({ className="w-full justify-between" > {selectedBaseDomain ? ( -
+
{selectedBaseDomain.type === "organization" ? null : ( - + )} {selectedBaseDomain.domain} {selectedBaseDomain.verified && ( - + )}
) : ( @@ -733,10 +726,8 @@ export default function DomainPicker2({ {!isChecking && sortedAvailableOptions.length > 0 && (
{ const option = displayedProvidedOptions.find( @@ -747,49 +738,56 @@ export default function DomainPicker2({ handleProvidedDomainSelect(option); } }} - className={`grid gap-2 grid-cols-1 sm:grid-cols-${cols}`} + style={{ + // @ts-expect-error CSS variable + "--cols": `repeat(${cols}, minmax(0, 1fr))` + }} + className="grid gap-2 grid-cols-1 sm:grid-cols-(--cols)" > - {displayedProvidedOptions.map((option) => ( -